Compare commits

..
Author SHA1 Message Date
ohif-bot 1ad5acae9d chore(version): Update package versions [skip ci] 2025-06-09 15:06:36 +00:00
ohif-bot c19e32e0d1 chore(version): version.json [skip ci] 2025-06-09 15:06:30 +00:00
36813e0c2f chore: recovery 2 (#5014)
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
Co-authored-by: Tang Cheng <45505657+tctco@users.noreply.github.com>
2025-06-09 10:58:06 -04:00
ohif-bot 9a4a40468f chore(version): Update package versions [skip ci] 2025-04-11 20:02:21 +00:00
ohif-bot 466a867e6a chore(version): version.json [skip ci] 2025-04-11 20:02:14 +00:00
aaba8bf1b0 fix: make datasource selection work and UI fixes (#4952)
Co-authored-by: Pedro Köhler <pedrokohlerbh@gmail.com>
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
2025-04-11 15:57:29 -04:00
ohif-bot 0b6e9cba76 chore(version): Update package versions [skip ci] 2025-04-10 02:48:32 +00:00
ohif-bot 963664cb63 chore(version): version.json [skip ci] 2025-04-10 02:48:27 +00:00
sedghi 12fe1e107d chore: release 3p10 2025-04-09 22:41:26 -04:00
1302 changed files with 25354 additions and 29490 deletions

No files matched your search

-108
View File
@@ -1,108 +0,0 @@
name: Build and Deploy Docs
on:
push:
branches: [master]
env:
ACTIONS_STEP_DEBUG: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-deploy-docs:
timeout-minutes: 60
runs-on: ubuntu-latest
# Need permissions to read actions and pull requests
permissions:
actions: read
contents: read
pull-requests: read
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: 20 # Or your desired Node version
- name: Install root dependencies
run: bun install --frozen-lockfile
# Removed Playwright tests and coverage generation steps
- name: Find PR and associated workflow run
id: find_pr_run
env:
GH_TOKEN: ${{ github.token }}
MERGE_COMMIT_SHA: ${{ github.sha }}
run: |
# Find the PR associated with the merge commit SHA
# Note: This relies on the merge commit being directly pushed to main
PR_DATA=$(gh pr list --state merged --search "$MERGE_COMMIT_SHA" --json number,headRefOid --jq '.[0]')
if [ -z "$PR_DATA" ]; then
echo "Could not find merged PR for commit $MERGE_COMMIT_SHA."
# Decide how to handle - fail, or maybe generate coverage now?
# For now, let's fail.
exit 1
fi
PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid')
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number')
echo "Found PR Number: $PR_NUMBER"
echo "Found PR Head SHA: $PR_HEAD_SHA"
# Find the latest workflow run ID for the playwright workflow on the PR head commit
# Uses the workflow file name 'playwright.yml'
# Remove the --status success flag to find any run
RUN_ID=$(gh run list --workflow playwright.yml --commit "$PR_HEAD_SHA" --event pull_request --json databaseId --jq '.[0].databaseId')
if [ -z "$RUN_ID" ]; then
echo "Could not find any 'playwright.yml' run for PR $PR_NUMBER (Head SHA: $PR_HEAD_SHA)."
# Decide how to handle - maybe try finding the artifact from the merge commit run if that exists?
# For now, let's fail.
exit 1
fi
echo "Found Run ID: $RUN_ID"
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
- name: Download coverage artifact from PR run
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p ./coverage-artifact
gh run download ${{ steps.find_pr_run.outputs.run_id }} -n coverage-report-pr --dir ./coverage-artifact
# Check if download was successful (e.g., check if files exist)
if [ ! -f ./coverage-artifact/base.css ]; then
echo "Failed to download or find expected files in artifact 'coverage-report-pr' from run ${{ steps.find_pr_run.outputs.run_id }}."
exit 1
fi
echo "Artifact downloaded successfully."
- name: Install docs dependencies
run: cd platform/docs && bun install
- name: Copy coverage to docs static directory
run: |
# Copy files from the downloaded artifact directory
mkdir -p platform/docs/static/coverage
cp -r ./coverage-artifact/* platform/docs/static/coverage/
# Copy specific asset files from the downloaded artifact root to static root
cp ./coverage-artifact/base.css platform/docs/static/
cp ./coverage-artifact/block-navigation.js platform/docs/static/
cp ./coverage-artifact/prettify.css platform/docs/static/
cp ./coverage-artifact/prettify.js platform/docs/static/
cp ./coverage-artifact/favicon.png platform/docs/static/
cp ./coverage-artifact/sort-arrow-sprite.png platform/docs/static/
cp ./coverage-artifact/sorter.js platform/docs/static/
- name: Build docs
run: cd platform/docs && bun run build
- name: Deploy to Netlify
run: |
cd platform/docs
npx netlify-cli deploy --dir=./build --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
-98
View File
@@ -1,98 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
pull_request:
branches: [ "main" ]
schedule:
- cron: '15 1 * * 5'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+54 -20
View File
@@ -1,41 +1,75 @@
name: Playwright Tests
on:
push:
branches: [main, master]
pull_request:
branches: [master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
branches: [main, master]
jobs:
playwright-tests:
timeout-minutes: 60
runs-on: self-hosted
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.51.1-noble
strategy:
fail-fast: false
matrix:
node-version: [20]
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: ${{ matrix.node-version }}
node-version: 20
- name: Install dependencies
run: bun install --frozen-lockfile
# - name: Install Playwright browsers
# run: bun x playwright install-deps chromium
- 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=10192"
bun run test:e2e:coverage
- name: create the coverage report
run: |
bun nyc report --reporter=lcov --reporter=text
- name: Upload the coverage report to GitHub Actions Artifacts
NODE_OPTIONS="--max_old_space_size=8192" TEST_ENV=true 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: coverage-report-pr
path: coverage
retention-days: 3
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
+1 -4
View File
@@ -1,7 +1,7 @@
# Packages
node_modules
.cursor/
.nyc_output/
# Output
build
dist
@@ -13,7 +13,6 @@ coverage/
.yarn/
.nx/
addOns/yarn.lock
playwright-report/
# YALC (for Erik)
.yalc
@@ -58,5 +57,3 @@ tests/test-results/
tests/playwright-report/
/blob-report/
/playwright/.cache/
**/.claude/settings.local.json
-24
View File
@@ -1,24 +0,0 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"instrument": true,
"sourceMap": true,
"cache": false,
"all": true,
"include": [
"platform/*/src/**/*.ts",
"platform/*/src/**/*.js",
"extensions/*/src/**/*.ts",
"extensions/*/src/**/*.js",
"modes/*/src/**/*.ts",
"modes/*/src/**/*.js"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/test/**",
"**/tests/**",
"**/examples/**",
"**/stories/**",
"platform/docs/**"
]
}
+1 -3
View File
@@ -109,8 +109,6 @@
"wado",
"wadors",
"wadouri",
"workerpool",
"Colorbar",
"Colorbars"
"workerpool"
]
}
+8 -25
View File
@@ -26,7 +26,6 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'
const NODE_ENV = process.env.NODE_ENV;
const QUICK_BUILD = process.env.QUICK_BUILD;
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
const IS_COVERAGE = process.env.COVERAGE === 'true';
// read from ../version.txt
const VERSION_NUMBER = fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || '';
@@ -101,30 +100,14 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
...(isProdBuild
? []
: [
...(IS_COVERAGE
? [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
plugins: ['istanbul'],
},
},
},
]
: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: isProdBuild ? [] : ['react-refresh/babel'],
},
},
]),
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: isProdBuild ? [] : ['react-refresh/babel'],
},
},
]),
{
test: /\.svg?$/,
+4 -318
View File
@@ -3,117 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.11.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.32...v3.11.0-beta.33) (2025-05-15)
### Features
* **actions:** simplify action corner api through the toolbarService ([#5033](https://github.com/OHIF/Viewers/issues/5033)) ([7c15bb8](https://github.com/OHIF/Viewers/commit/7c15bb8901001eefedc72498176ac24f6662bfab))
# [3.11.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.31...v3.11.0-beta.32) (2025-05-13)
### Bug Fixes
* Added missing export for LayoutSelector in ui-next ([#5040](https://github.com/OHIF/Viewers/issues/5040)) ([da51e6e](https://github.com/OHIF/Viewers/commit/da51e6e3c185e4d11d50679fd0deaa804b07511c))
# [3.11.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.30...v3.11.0-beta.31) (2025-05-12)
### Bug Fixes
* **NotFoundStudy:** Fixed the viewer not-found page when showStudyList: false ([#5035](https://github.com/OHIF/Viewers/issues/5035)) ([950453f](https://github.com/OHIF/Viewers/commit/950453fd70ed2b5fec51cc9b6d34e94538ed15cd))
# [3.11.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.29...v3.11.0-beta.30) (2025-05-09)
### Bug Fixes
* **DicomWebProxyDataSource:** bug fix to save Segmentation files back to Kheops ([#5028](https://github.com/OHIF/Viewers/issues/5028)) ([349306d](https://github.com/OHIF/Viewers/commit/349306da4013602141fecefd095d95e8dc538c30))
# [3.11.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.28...v3.11.0-beta.29) (2025-05-08)
### Features
* **app:** Decouple colorbar UI from service and enable action menu programatic control ([#5018](https://github.com/OHIF/Viewers/issues/5018)) ([1445e12](https://github.com/OHIF/Viewers/commit/1445e1212415e6e6957fce4a8fc6908aed69a93c))
# [3.11.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.27...v3.11.0-beta.28) (2025-05-08)
### Features
* **thumbnails:** Enhance DICOM Thumbnail Rendering with different strategies ([#5004](https://github.com/OHIF/Viewers/issues/5004)) ([578c393](https://github.com/OHIF/Viewers/commit/578c3939f0271f762d6a1290216dbc4bf59dd88b))
# [3.11.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.26...v3.11.0-beta.27) (2025-05-07)
### Bug Fixes
* **orientationMarker:** MPR orientation change should get reflected too ([#5020](https://github.com/OHIF/Viewers/issues/5020)) ([a5ea3e1](https://github.com/OHIF/Viewers/commit/a5ea3e15c70f6b8f2d16257f5f5c774c921d82cb))
# [3.11.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.25...v3.11.0-beta.26) (2025-05-07)
### Features
* add locale files for Russian language, translated texts in locale json files to Russian language. ([#4984](https://github.com/OHIF/Viewers/issues/4984)) ([c829270](https://github.com/OHIF/Viewers/commit/c829270c80c0899132aff0294b8e7386475dc71c))
# [3.11.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.24...v3.11.0-beta.25) (2025-05-06)
### Features
* **i18n:** add Arabic translations ([#5013](https://github.com/OHIF/Viewers/issues/5013)) ([8218a9d](https://github.com/OHIF/Viewers/commit/8218a9d0346fe9a5473420e26a0b8ab389f4ef58))
# [3.11.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.23...v3.11.0-beta.24) (2025-05-05)
### Features
* **data overlay:** add foreground background control plus orientation change in the UI and add RTDOSE support ([#5007](https://github.com/OHIF/Viewers/issues/5007)) ([cd4e640](https://github.com/OHIF/Viewers/commit/cd4e6408bc2964e5ee27427cdc328751eaf4d088))
# [3.11.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.22...v3.11.0-beta.23) (2025-05-05)
## [3.10.2](https://github.com/OHIF/Viewers/compare/v3.10.1...v3.10.2) (2025-06-09)
**Note:** Version bump only for package ohif-monorepo-root
@@ -121,222 +11,18 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.21...v3.11.0-beta.22) (2025-05-02)
### Features
* improve menu modals customizations ([#5011](https://github.com/OHIF/Viewers/issues/5011)) ([7dbe5e3](https://github.com/OHIF/Viewers/commit/7dbe5e37b06056fdd5262a131417013d112c3d30))
# [3.11.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.20...v3.11.0-beta.21) (2025-05-02)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.19...v3.11.0-beta.20) (2025-05-01)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.18...v3.11.0-beta.19) (2025-05-01)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.17...v3.11.0-beta.18) (2025-04-30)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.16...v3.11.0-beta.17) (2025-04-29)
## [3.10.1](https://github.com/OHIF/Viewers/compare/v3.10.0...v3.10.1) (2025-04-11)
### Bug Fixes
* **referencelines:** and bring back the notification for worker updates but disallow duplication ([#5005](https://github.com/OHIF/Viewers/issues/5005)) ([98f8187](https://github.com/OHIF/Viewers/commit/98f8187dfeb8aec6eca145da75c855aec0da4e1d))
* make datasource selection work and UI fixes ([#4952](https://github.com/OHIF/Viewers/issues/4952)) ([aaba8bf](https://github.com/OHIF/Viewers/commit/aaba8bf1b04be6c00a88f1f5f29cb24133b0c39e))
# [3.11.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.15...v3.11.0-beta.16) (2025-04-29)
### Bug Fixes
* **template:** Update mode template to match current implementation (OHI-1833) ([#4998](https://github.com/OHIF/Viewers/issues/4998)) ([0f39496](https://github.com/OHIF/Viewers/commit/0f394962904808a140221dbae063ca5dcf0df36b))
# [3.11.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.14...v3.11.0-beta.15) (2025-04-22)
### Bug Fixes
* **docs:** Fix SSR build errors by dynamically importing UI components and resolving other issues ([#4991](https://github.com/OHIF/Viewers/issues/4991)) ([09ac43f](https://github.com/OHIF/Viewers/commit/09ac43f297fa81101d3a3eaa9b5cfef8e01ea214))
# [3.11.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.13...v3.11.0-beta.14) (2025-04-22)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.12...v3.11.0-beta.13) (2025-04-22)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.11...v3.11.0-beta.12) (2025-04-22)
### Features
* Improve unsupported displayset messages ([#4979](https://github.com/OHIF/Viewers/issues/4979)) ([3b8faa8](https://github.com/OHIF/Viewers/commit/3b8faa8a76dde5088815431a947fec07db4a3c7f))
# [3.11.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.10...v3.11.0-beta.11) (2025-04-18)
### Bug Fixes
* **dicomWebClient:** use public getter for activeDataSourceName instead of accessing private property ([#4973](https://github.com/OHIF/Viewers/issues/4973)) ([5688a2b](https://github.com/OHIF/Viewers/commit/5688a2b4b8f872163ff6492bb80d12cade4385cd))
# [3.11.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.9...v3.11.0-beta.10) (2025-04-17)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.8...v3.11.0-beta.9) (2025-04-17)
### Bug Fixes
* **ExtensionManager:** getDataSources method ([#4959](https://github.com/OHIF/Viewers/issues/4959)) ([66a16cd](https://github.com/OHIF/Viewers/commit/66a16cd22f6cedc6e4287dd1304e1f87de4c0d08))
# [3.11.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.7...v3.11.0-beta.8) (2025-04-16)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.6...v3.11.0-beta.7) (2025-04-16)
### Bug Fixes
* github runner ([#4961](https://github.com/OHIF/Viewers/issues/4961)) ([9535422](https://github.com/OHIF/Viewers/commit/953542236116aea6f4dce6275b8cdbd4490f4520))
# [3.11.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.5...v3.11.0-beta.6) (2025-04-16)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.4...v3.11.0-beta.5) (2025-04-14)
### Bug Fixes
* making onnxruntime work with rsbuild ([#4957](https://github.com/OHIF/Viewers/issues/4957)) ([4a87302](https://github.com/OHIF/Viewers/commit/4a87302f06378649680370f10dd942d31c1c61fb))
# [3.11.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.3...v3.11.0-beta.4) (2025-04-11)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.2...v3.11.0-beta.3) (2025-04-11)
### Bug Fixes
* add back study browser scrollbar ([#4942](https://github.com/OHIF/Viewers/issues/4942)) ([ed94df0](https://github.com/OHIF/Viewers/commit/ed94df0dfe007748e51de959d9dd4101485d8df4))
* enable measurements panel scroll ([#4943](https://github.com/OHIF/Viewers/issues/4943)) ([7a36eba](https://github.com/OHIF/Viewers/commit/7a36eba67c6365f3f8cabb53a31ae1eee96069e8))
### Features
* expose scroll thumbnail into view ([#4941](https://github.com/OHIF/Viewers/issues/4941)) ([4926a95](https://github.com/OHIF/Viewers/commit/4926a9532c89011ef932fad1dbae643806e780a8))
# [3.11.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.1...v3.11.0-beta.2) (2025-04-11)
### Bug Fixes
* make datasource selection work ([#4944](https://github.com/OHIF/Viewers/issues/4944)) ([a9b180c](https://github.com/OHIF/Viewers/commit/a9b180c5d427ec5a6823991cf0c6604be7af1cc2))
# [3.11.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.0...v3.11.0-beta.1) (2025-04-10)
**Note:** Version bump only for package ohif-monorepo-root
# [3.11.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.11.0-beta.0) (2025-04-10)
# [3.10.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.10.0) (2025-04-10)
**Note:** Version bump only for package ohif-monorepo-root
+3 -254
View File
@@ -3,7 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.11.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.32...v3.11.0-beta.33) (2025-05-15)
## [3.10.2](https://github.com/OHIF/Viewers/compare/v3.10.1...v3.10.2) (2025-06-09)
**Note:** Version bump only for package @externals/devDependencies
@@ -11,7 +11,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.31...v3.11.0-beta.32) (2025-05-13)
## [3.10.1](https://github.com/OHIF/Viewers/compare/v3.10.0...v3.10.1) (2025-04-11)
**Note:** Version bump only for package @externals/devDependencies
@@ -19,258 +19,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.30...v3.11.0-beta.31) (2025-05-12)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.29...v3.11.0-beta.30) (2025-05-09)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.28...v3.11.0-beta.29) (2025-05-08)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.27...v3.11.0-beta.28) (2025-05-08)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.26...v3.11.0-beta.27) (2025-05-07)
### Bug Fixes
* **orientationMarker:** MPR orientation change should get reflected too ([#5020](https://github.com/OHIF/Viewers/issues/5020)) ([a5ea3e1](https://github.com/OHIF/Viewers/commit/a5ea3e15c70f6b8f2d16257f5f5c774c921d82cb))
# [3.11.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.25...v3.11.0-beta.26) (2025-05-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.24...v3.11.0-beta.25) (2025-05-06)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.23...v3.11.0-beta.24) (2025-05-05)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.22...v3.11.0-beta.23) (2025-05-05)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.21...v3.11.0-beta.22) (2025-05-02)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.20...v3.11.0-beta.21) (2025-05-02)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.19...v3.11.0-beta.20) (2025-05-01)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.18...v3.11.0-beta.19) (2025-05-01)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.17...v3.11.0-beta.18) (2025-04-30)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.16...v3.11.0-beta.17) (2025-04-29)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.15...v3.11.0-beta.16) (2025-04-29)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.14...v3.11.0-beta.15) (2025-04-22)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.13...v3.11.0-beta.14) (2025-04-22)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.12...v3.11.0-beta.13) (2025-04-22)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.11...v3.11.0-beta.12) (2025-04-22)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.10...v3.11.0-beta.11) (2025-04-18)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.9...v3.11.0-beta.10) (2025-04-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.8...v3.11.0-beta.9) (2025-04-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.7...v3.11.0-beta.8) (2025-04-16)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.6...v3.11.0-beta.7) (2025-04-16)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.5...v3.11.0-beta.6) (2025-04-16)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.4...v3.11.0-beta.5) (2025-04-14)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.3...v3.11.0-beta.4) (2025-04-11)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.2...v3.11.0-beta.3) (2025-04-11)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.1...v3.11.0-beta.2) (2025-04-11)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.0...v3.11.0-beta.1) (2025-04-10)
**Note:** Version bump only for package @externals/devDependencies
# [3.11.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.11.0-beta.0) (2025-04-10)
# [3.10.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.10.0) (2025-04-10)
**Note:** Version bump only for package @externals/devDependencies
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@externals/devDependencies",
"description": "External dev dependencies - put dev build dependencies here",
"version": "3.11.0-beta.33",
"version": "3.10.2",
"license": "MIT",
"private": true,
"engines": {
@@ -21,9 +21,9 @@
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@rsbuild/core": "^1.3.16",
"@rsbuild/plugin-node-polyfill": "^1.3.0",
"@rsbuild/plugin-react": "^1.3.1",
"@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",
+3 -251
View File
@@ -3,7 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.11.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.32...v3.11.0-beta.33) (2025-05-15)
## [3.10.2](https://github.com/OHIF/Viewers/compare/v3.10.1...v3.10.2) (2025-06-09)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
@@ -11,7 +11,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.31...v3.11.0-beta.32) (2025-05-13)
## [3.10.1](https://github.com/OHIF/Viewers/compare/v3.10.0...v3.10.1) (2025-04-11)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
@@ -19,255 +19,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.30...v3.11.0-beta.31) (2025-05-12)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.29...v3.11.0-beta.30) (2025-05-09)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.28...v3.11.0-beta.29) (2025-05-08)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.27...v3.11.0-beta.28) (2025-05-08)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.26...v3.11.0-beta.27) (2025-05-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.25...v3.11.0-beta.26) (2025-05-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.24...v3.11.0-beta.25) (2025-05-06)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.23...v3.11.0-beta.24) (2025-05-05)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.22...v3.11.0-beta.23) (2025-05-05)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.21...v3.11.0-beta.22) (2025-05-02)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.20...v3.11.0-beta.21) (2025-05-02)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.19...v3.11.0-beta.20) (2025-05-01)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.18...v3.11.0-beta.19) (2025-05-01)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.17...v3.11.0-beta.18) (2025-04-30)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.16...v3.11.0-beta.17) (2025-04-29)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.15...v3.11.0-beta.16) (2025-04-29)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.14...v3.11.0-beta.15) (2025-04-22)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.13...v3.11.0-beta.14) (2025-04-22)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.12...v3.11.0-beta.13) (2025-04-22)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.11...v3.11.0-beta.12) (2025-04-22)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.10...v3.11.0-beta.11) (2025-04-18)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.9...v3.11.0-beta.10) (2025-04-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.8...v3.11.0-beta.9) (2025-04-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.7...v3.11.0-beta.8) (2025-04-16)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.6...v3.11.0-beta.7) (2025-04-16)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.5...v3.11.0-beta.6) (2025-04-16)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.4...v3.11.0-beta.5) (2025-04-14)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.3...v3.11.0-beta.4) (2025-04-11)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.2...v3.11.0-beta.3) (2025-04-11)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.1...v3.11.0-beta.2) (2025-04-11)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.0...v3.11.0-beta.1) (2025-04-10)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.11.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.11.0-beta.0) (2025-04-10)
# [3.10.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.10.0) (2025-04-10)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@externals/dicom-microscopy-viewer",
"description": "External reference to dicom-microscopy-viewer",
"version": "3.11.0-beta.33",
"version": "3.10.2",
"license": "MIT",
"dependencies": {
"dicom-microscopy-viewer": "^0.46.1"
-1
View File
@@ -24,7 +24,6 @@ module.exports = {
'@babel/preset-typescript',
],
plugins: [
'babel-plugin-istanbul',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
+327 -544
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1 +1 @@
7c15bb8901001eefedc72498176ac24f6662bfab
36813e0c2f1ea48953714e8e48aff6e9d4516449
+4 -261
View File
@@ -3,18 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.11.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.32...v3.11.0-beta.33) (2025-05-15)
### Features
* **actions:** simplify action corner api through the toolbarService ([#5033](https://github.com/OHIF/Viewers/issues/5033)) ([7c15bb8](https://github.com/OHIF/Viewers/commit/7c15bb8901001eefedc72498176ac24f6662bfab))
# [3.11.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.31...v3.11.0-beta.32) (2025-05-13)
## [3.10.2](https://github.com/OHIF/Viewers/compare/v3.10.1...v3.10.2) (2025-06-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
@@ -22,264 +11,18 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.30...v3.11.0-beta.31) (2025-05-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.29...v3.11.0-beta.30) (2025-05-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.28...v3.11.0-beta.29) (2025-05-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.27...v3.11.0-beta.28) (2025-05-08)
### Features
* **thumbnails:** Enhance DICOM Thumbnail Rendering with different strategies ([#5004](https://github.com/OHIF/Viewers/issues/5004)) ([578c393](https://github.com/OHIF/Viewers/commit/578c3939f0271f762d6a1290216dbc4bf59dd88b))
# [3.11.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.26...v3.11.0-beta.27) (2025-05-07)
## [3.10.1](https://github.com/OHIF/Viewers/compare/v3.10.0...v3.10.1) (2025-04-11)
### Bug Fixes
* **orientationMarker:** MPR orientation change should get reflected too ([#5020](https://github.com/OHIF/Viewers/issues/5020)) ([a5ea3e1](https://github.com/OHIF/Viewers/commit/a5ea3e15c70f6b8f2d16257f5f5c774c921d82cb))
* make datasource selection work and UI fixes ([#4952](https://github.com/OHIF/Viewers/issues/4952)) ([aaba8bf](https://github.com/OHIF/Viewers/commit/aaba8bf1b04be6c00a88f1f5f29cb24133b0c39e))
# [3.11.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.25...v3.11.0-beta.26) (2025-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.24...v3.11.0-beta.25) (2025-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.23...v3.11.0-beta.24) (2025-05-05)
### Features
* **data overlay:** add foreground background control plus orientation change in the UI and add RTDOSE support ([#5007](https://github.com/OHIF/Viewers/issues/5007)) ([cd4e640](https://github.com/OHIF/Viewers/commit/cd4e6408bc2964e5ee27427cdc328751eaf4d088))
# [3.11.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.22...v3.11.0-beta.23) (2025-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.21...v3.11.0-beta.22) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.20...v3.11.0-beta.21) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.19...v3.11.0-beta.20) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.18...v3.11.0-beta.19) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.17...v3.11.0-beta.18) (2025-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.16...v3.11.0-beta.17) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.15...v3.11.0-beta.16) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.14...v3.11.0-beta.15) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.13...v3.11.0-beta.14) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.12...v3.11.0-beta.13) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.11...v3.11.0-beta.12) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.10...v3.11.0-beta.11) (2025-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.9...v3.11.0-beta.10) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.8...v3.11.0-beta.9) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.7...v3.11.0-beta.8) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.6...v3.11.0-beta.7) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.5...v3.11.0-beta.6) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.4...v3.11.0-beta.5) (2025-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.3...v3.11.0-beta.4) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.2...v3.11.0-beta.3) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.1...v3.11.0-beta.2) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.0...v3.11.0-beta.1) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.11.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.11.0-beta.0) (2025-04-10)
# [3.10.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.10.0) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.11.0-beta.33",
"version": "3.10.2",
"description": "DICOM Parametric Map read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.33",
"@ohif/extension-cornerstone": "3.11.0-beta.33",
"@ohif/extension-default": "3.11.0-beta.33",
"@ohif/i18n": "3.11.0-beta.33",
"@ohif/core": "3.10.2",
"@ohif/extension-cornerstone": "3.10.2",
"@ohif/extension-default": "3.10.2",
"@ohif/i18n": "3.10.2",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/adapters": "^3.11.6",
"@cornerstonejs/core": "^3.11.6",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}
@@ -1,9 +1,10 @@
import { utils, Types as OhifTypes } from '@ohif/core';
import i18n from '@ohif/i18n';
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, VOLUME_LOADER_SCHEME } from '@ohif/extension-cornerstone';
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(
@@ -56,8 +57,7 @@ function _getDisplaySetsFromSeries(
wadoRoot,
wadoUriRoot,
wadoUri,
supportsWindowLevel: true,
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('PMAP')}`,
isOverlayDisplaySet: true,
};
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
@@ -227,8 +227,7 @@ async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
return derivedVolume;
}
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
@@ -1,10 +1,16 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useViewportGrid } from '@ohif/ui-next';
import { OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const { displaySets, children, viewportOptions, displaySetOptions, servicesManager } = props;
const {
displaySets,
children,
viewportOptions,
displaySetOptions,
servicesManager,
extensionManager,
} = props;
const viewportId = viewportOptions.viewportId;
const { displaySetService, segmentationService, uiNotificationService, customizationService } =
servicesManager.services;
@@ -53,6 +59,9 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
displaySetOptions.unshift({});
const [pmapDisplaySetOptions] = displaySetOptions;
@@ -84,7 +93,7 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
});
return (
<OHIFCornerstoneViewport
<Component
{...props}
// Referenced + PMAP displaySets must be passed as parameter in this order
displaySets={[referencedDisplaySet, pmapDisplaySet]}
@@ -95,16 +104,15 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
presentationIds: viewportOptions.presentationIds,
}}
displaySetOptions={[{}, pmapDisplaySetOptions]}
/>
></Component>
);
}, [
extensionManager,
displaySetOptions,
props,
pmapDisplaySet,
viewportOptions.orientation,
viewportOptions.viewportId,
viewportOptions.presentationIds,
uiNotificationService,
]);
// Cleanup the PMAP viewport when the viewport is destroyed
+3 -257
View File
@@ -3,18 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.11.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.32...v3.11.0-beta.33) (2025-05-15)
### Features
* **actions:** simplify action corner api through the toolbarService ([#5033](https://github.com/OHIF/Viewers/issues/5033)) ([7c15bb8](https://github.com/OHIF/Viewers/commit/7c15bb8901001eefedc72498176ac24f6662bfab))
# [3.11.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.31...v3.11.0-beta.32) (2025-05-13)
## [3.10.2](https://github.com/OHIF/Viewers/compare/v3.10.1...v3.10.2) (2025-06-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
@@ -22,7 +11,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.30...v3.11.0-beta.31) (2025-05-12)
## [3.10.1](https://github.com/OHIF/Viewers/compare/v3.10.0...v3.10.1) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
@@ -30,250 +19,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.29...v3.11.0-beta.30) (2025-05-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.28...v3.11.0-beta.29) (2025-05-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.27...v3.11.0-beta.28) (2025-05-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.26...v3.11.0-beta.27) (2025-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.25...v3.11.0-beta.26) (2025-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.24...v3.11.0-beta.25) (2025-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.23...v3.11.0-beta.24) (2025-05-05)
### Features
* **data overlay:** add foreground background control plus orientation change in the UI and add RTDOSE support ([#5007](https://github.com/OHIF/Viewers/issues/5007)) ([cd4e640](https://github.com/OHIF/Viewers/commit/cd4e6408bc2964e5ee27427cdc328751eaf4d088))
# [3.11.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.22...v3.11.0-beta.23) (2025-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.21...v3.11.0-beta.22) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.20...v3.11.0-beta.21) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.19...v3.11.0-beta.20) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.18...v3.11.0-beta.19) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.17...v3.11.0-beta.18) (2025-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.16...v3.11.0-beta.17) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.15...v3.11.0-beta.16) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.14...v3.11.0-beta.15) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.13...v3.11.0-beta.14) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.12...v3.11.0-beta.13) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.11...v3.11.0-beta.12) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.10...v3.11.0-beta.11) (2025-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.9...v3.11.0-beta.10) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.8...v3.11.0-beta.9) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.7...v3.11.0-beta.8) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.6...v3.11.0-beta.7) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.5...v3.11.0-beta.6) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.4...v3.11.0-beta.5) (2025-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.3...v3.11.0-beta.4) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.2...v3.11.0-beta.3) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.1...v3.11.0-beta.2) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.0...v3.11.0-beta.1) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.11.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.11.0-beta.0) (2025-04-10)
# [3.10.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.10.0) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.11.0-beta.33",
"version": "3.10.2",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.33",
"@ohif/extension-cornerstone": "3.11.0-beta.33",
"@ohif/extension-default": "3.11.0-beta.33",
"@ohif/i18n": "3.11.0-beta.33",
"@ohif/core": "3.10.2",
"@ohif/extension-cornerstone": "3.10.2",
"@ohif/extension-default": "3.10.2",
"@ohif/i18n": "3.10.2",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -27,7 +27,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
});
viewportGridService.setDisplaySetsForViewport({
@@ -1,5 +1,4 @@
import { utils, Types as OhifTypes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { utils } from '@ohif/core';
import { SOPClassHandlerId } from './id';
import loadRTStruct from './loadRTStruct';
@@ -54,7 +53,6 @@ function _getDisplaySetsFromSeries(
wadoUriRoot,
wadoUri,
isOverlayDisplaySet: true,
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('RTSTRUCT')}`,
};
let referencedSeriesSequence = instance.ReferencedSeriesSequence;
@@ -96,19 +94,12 @@ function _getDisplaySetsFromSeries(
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
}
displaySet.load = ({ headers, createSegmentation = true }) =>
_load(displaySet, servicesManager, extensionManager, headers, createSegmentation);
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
return [displaySet];
}
function _load(
rtDisplaySet,
servicesManager: AppTypes.ServicesManager,
extensionManager,
headers,
createSegmentation = true
) {
function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) {
const { SOPInstanceUID } = rtDisplaySet;
const { segmentationService } = servicesManager.services;
if (
@@ -130,21 +121,16 @@ function _load(
rtDisplaySet.structureSet = structureSet;
}
if (createSegmentation) {
segmentationService
.createSegmentationForRTDisplaySet(rtDisplaySet)
.then(() => {
rtDisplaySet.loading = false;
resolve();
})
.catch(error => {
rtDisplaySet.loading = false;
reject(error);
});
} else {
rtDisplaySet.loading = false;
resolve();
}
segmentationService
.createSegmentationForRTDisplaySet(rtDisplaySet)
.then(() => {
rtDisplaySet.loading = false;
resolve();
})
.catch(error => {
rtDisplaySet.loading = false;
reject(error);
});
});
return loadPromises[SOPInstanceUID];
@@ -185,14 +171,20 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
return ReferencedSeriesSequence;
}
function _segmentationExistsInCache() {
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(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
return [
{
name: 'dicom-rt',
@@ -1,4 +1,8 @@
import { utils, Types } from '@ohif/extension-cornerstone';
const RESPONSE = {
NO_NEVER: -1,
CANCEL: 0,
HYDRATE_SEG: 5,
};
function promptHydrateRT({
servicesManager,
@@ -6,20 +10,74 @@ function promptHydrateRT({
viewportId,
preHydrateCallbacks,
hydrateRTDisplaySet,
}: {
servicesManager: AppTypes.ServicesManager;
rtDisplaySet: AppTypes.DisplaySet;
viewportId: string;
preHydrateCallbacks?: Types.HydrationCallback[];
hydrateRTDisplaySet: Types.HydrationCallback;
}) {
return utils.promptHydrationDialog({
servicesManager,
viewportId,
displaySet: rtDisplaySet,
preHydrateCallbacks,
hydrateCallback: hydrateRTDisplaySet,
type: 'RTSTRUCT',
}: withAppTypes) {
const { uiViewportDialogService, customizationService } = 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, customizationService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
servicesManager,
});
resolve(isHydrated);
}
});
}
function _askHydrate(
uiViewportDialogService: AppTypes.UIViewportDialogService,
customizationService: AppTypes.CustomizationService,
viewportId
) {
return new Promise(function (resolve, reject) {
const message = customizationService.getCustomization('viewportNotification.hydrateRTMessage');
const actions = [
{
id: 'no-hydrate',
type: 'secondary',
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
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);
}
},
});
});
}
@@ -1,27 +1,35 @@
import React, { Component, useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { ViewportActionArrows } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import {
utils,
usePositionPresentationStore,
OHIFCornerstoneViewport,
} from '@ohif/extension-cornerstone';
import { utils } from '@ohif/extension-cornerstone';
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import { useSystem } from '@ohif/core/src';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
function OHIFCornerstoneRTViewport(props: withAppTypes) {
const { servicesManager, commandsManager } = useSystem();
const { children, displaySets, viewportOptions } = props as {
children: React.ReactNode;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportOptions;
};
const {
children,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { displaySetService, toolGroupService, segmentationService, customizationService } =
servicesManager.services;
const {
displaySetService,
toolGroupService,
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@@ -38,19 +46,30 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
const rtDisplaySet = displaySets[0];
const [{ viewports, activeViewportId }, viewportGridService] = useViewportGrid();
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
@@ -61,31 +80,100 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
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 (
<Component
{...props}
displaySets={[referencedDisplaySet, rtDisplaySet]}
viewportOptions={{
viewportType: 'stack',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
onElementDisabled={onElementDisabled}
></Component>
);
}, [viewportId, rtDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
utils.handleSegmentChange({
direction,
segDisplaySet: rtDisplaySet,
viewportId,
selectedSegmentObjectIndex,
segmentationService,
});
},
[selectedSegmentObjectIndex]
);
useEffect(() => {
if (rtIsLoading) {
return;
}
// if not active viewport, return
if (viewportId !== activeViewportId) {
return;
}
promptHydrateRT({
servicesManager,
viewportId,
rtDisplaySet,
hydrateRTDisplaySet: async () => {
return commandsManager.runCommand('hydrateSecondaryDisplaySet', {
displaySet: rtDisplaySet,
viewportId,
});
},
preHydrateCallbacks: [storePresentationState],
hydrateRTDisplaySet,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
});
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading, commandsManager, activeViewportId]);
}, [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 => {
@@ -105,6 +193,14 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
viewPresentation: {},
});
}
if (evt.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
message: 'Overlapping segments detected which is not currently supported',
type: 'warning',
});
}
}
);
@@ -114,7 +210,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
}, [rtDisplaySet]);
useEffect(() => {
const segmentLoadingSubscription = segmentationService.subscribe(
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
({ percentComplete, numSegments }) => {
setProcessingProgress({
@@ -124,7 +220,16 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
}
);
const displaySetsRemovedSubscription = displaySetService.subscribe(
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);
@@ -138,10 +243,9 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
);
return () => {
segmentLoadingSubscription.unsubscribe();
displaySetsRemovedSubscription.unsubscribe();
onDisplaySetsRemovedSubscription.unsubscribe();
};
}, [rtDisplaySet, displaySetService, viewports, activeViewportId, viewportGridService]);
}, []);
useEffect(() => {
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
@@ -155,33 +259,38 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentations(viewportId);
referencedDisplaySetRef.current = null;
toolGroupService.destroyToolGroup(toolGroupId);
};
}, []);
const getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
useEffect(() => {
setIsHydrated(rtDisplaySet.isHydrated);
// Todo: jump to the center of the first segment
return (
<OHIFCornerstoneViewport
{...props}
displaySets={[referencedDisplaySet, rtDisplaySet]}
viewportOptions={{
viewportType: 'stack',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
}}
/>
);
}, [viewportId, rtDisplaySet, toolGroupId]);
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 (
@@ -204,6 +313,43 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}
useEffect(() => {
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">
@@ -0,0 +1,59 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui-next';
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 = () => <Icons.ByName name="status-alert" />;
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
break;
case false:
StatusIcon = () => (
<Icons.ByName
className="text-muted-foreground h-4 w-4"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">RTSTRUCT</span>
</div>
{!isHydrated && (
<ViewportActionButton onInteraction={onStatusClick}>{loadStr}</ViewportActionButton>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}
+4 -264
View File
@@ -3,18 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.11.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.32...v3.11.0-beta.33) (2025-05-15)
### Features
* **actions:** simplify action corner api through the toolbarService ([#5033](https://github.com/OHIF/Viewers/issues/5033)) ([7c15bb8](https://github.com/OHIF/Viewers/commit/7c15bb8901001eefedc72498176ac24f6662bfab))
# [3.11.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.31...v3.11.0-beta.32) (2025-05-13)
## [3.10.2](https://github.com/OHIF/Viewers/compare/v3.10.1...v3.10.2) (2025-06-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
@@ -22,267 +11,18 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.30...v3.11.0-beta.31) (2025-05-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.29...v3.11.0-beta.30) (2025-05-09)
## [3.10.1](https://github.com/OHIF/Viewers/compare/v3.10.0...v3.10.1) (2025-04-11)
### Bug Fixes
* **DicomWebProxyDataSource:** bug fix to save Segmentation files back to Kheops ([#5028](https://github.com/OHIF/Viewers/issues/5028)) ([349306d](https://github.com/OHIF/Viewers/commit/349306da4013602141fecefd095d95e8dc538c30))
* make datasource selection work and UI fixes ([#4952](https://github.com/OHIF/Viewers/issues/4952)) ([aaba8bf](https://github.com/OHIF/Viewers/commit/aaba8bf1b04be6c00a88f1f5f29cb24133b0c39e))
# [3.11.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.28...v3.11.0-beta.29) (2025-05-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.27...v3.11.0-beta.28) (2025-05-08)
### Features
* **thumbnails:** Enhance DICOM Thumbnail Rendering with different strategies ([#5004](https://github.com/OHIF/Viewers/issues/5004)) ([578c393](https://github.com/OHIF/Viewers/commit/578c3939f0271f762d6a1290216dbc4bf59dd88b))
# [3.11.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.26...v3.11.0-beta.27) (2025-05-07)
### Bug Fixes
* **orientationMarker:** MPR orientation change should get reflected too ([#5020](https://github.com/OHIF/Viewers/issues/5020)) ([a5ea3e1](https://github.com/OHIF/Viewers/commit/a5ea3e15c70f6b8f2d16257f5f5c774c921d82cb))
# [3.11.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.25...v3.11.0-beta.26) (2025-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.24...v3.11.0-beta.25) (2025-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.23...v3.11.0-beta.24) (2025-05-05)
### Features
* **data overlay:** add foreground background control plus orientation change in the UI and add RTDOSE support ([#5007](https://github.com/OHIF/Viewers/issues/5007)) ([cd4e640](https://github.com/OHIF/Viewers/commit/cd4e6408bc2964e5ee27427cdc328751eaf4d088))
# [3.11.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.22...v3.11.0-beta.23) (2025-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.21...v3.11.0-beta.22) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.20...v3.11.0-beta.21) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.19...v3.11.0-beta.20) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.18...v3.11.0-beta.19) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.17...v3.11.0-beta.18) (2025-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.16...v3.11.0-beta.17) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.15...v3.11.0-beta.16) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.14...v3.11.0-beta.15) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.13...v3.11.0-beta.14) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.12...v3.11.0-beta.13) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.11...v3.11.0-beta.12) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.10...v3.11.0-beta.11) (2025-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.9...v3.11.0-beta.10) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.8...v3.11.0-beta.9) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.7...v3.11.0-beta.8) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.6...v3.11.0-beta.7) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.5...v3.11.0-beta.6) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.4...v3.11.0-beta.5) (2025-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.3...v3.11.0-beta.4) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.2...v3.11.0-beta.3) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.1...v3.11.0-beta.2) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.0...v3.11.0-beta.1) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.11.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.11.0-beta.0) (2025-04-10)
# [3.10.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.10.0) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.11.0-beta.33",
"version": "3.10.2",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.33",
"@ohif/extension-cornerstone": "3.11.0-beta.33",
"@ohif/extension-default": "3.11.0-beta.33",
"@ohif/i18n": "3.11.0-beta.33",
"@ohif/core": "3.10.2",
"@ohif/extension-cornerstone": "3.10.2",
"@ohif/extension-default": "3.10.2",
"@ohif/i18n": "3.10.2",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/adapters": "^3.11.6",
"@cornerstonejs/core": "^3.11.6",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}
@@ -220,7 +220,7 @@ const commandsModule = ({
}
const { label } = segmentation;
const defaultDataSource = dataSource ?? extensionManager.getActiveDataSource()[0];
const defaultDataSource = dataSource ?? extensionManager.getActiveDataSource();
const {
value: reportName,
@@ -251,11 +251,6 @@ const commandsModule = ({
const { dataset: naturalizedReport } = generatedData;
// DCMJS assigns a dummy study id during creation, and this can cause problems, so clearing it out
if (naturalizedReport.StudyID === 'No Study ID') {
naturalizedReport.StudyID = '';
}
await selectedDataSourceConfig.store.dicom(naturalizedReport);
// add the information for where we stored it to the instance as well
@@ -1,6 +1,5 @@
import { utils, Types as OhifTypes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { metaData, eventTarget } from '@cornerstonejs/core';
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';
@@ -58,7 +57,6 @@ function _getDisplaySetsFromSeries(
wadoUriRoot,
wadoUri,
isOverlayDisplaySet: true,
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SEG')}`,
};
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
@@ -234,8 +232,7 @@ function _segmentationExists(segDisplaySet) {
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
}
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
@@ -1,4 +1,8 @@
import { utils, Types } from '@ohif/extension-cornerstone';
const RESPONSE = {
NO_NEVER: -1,
CANCEL: 0,
HYDRATE_SEG: 5,
};
function promptHydrateSEG({
servicesManager,
@@ -6,20 +10,75 @@ function promptHydrateSEG({
viewportId,
preHydrateCallbacks,
hydrateCallback,
}: {
servicesManager: AppTypes.ServicesManager;
segDisplaySet: AppTypes.DisplaySet;
viewportId: string;
preHydrateCallbacks?: Types.HydrationCallback[];
hydrateCallback: Types.HydrationCallback;
}) {
return utils.promptHydrationDialog({
servicesManager,
viewportId,
displaySet: segDisplaySet as AppTypes.DisplaySet,
preHydrateCallbacks,
hydrateCallback,
type: 'SEG',
}: withAppTypes) {
const { uiViewportDialogService, customizationService } = 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, customizationService, 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: AppTypes.UIViewportDialogService,
customizationService: AppTypes.CustomizationService,
viewportId
) {
return new Promise(function (resolve, reject) {
const message = customizationService.getCustomization('viewportNotification.hydrateSEGMessage');
const actions = [
{
id: 'no-hydrate',
type: 'secondary',
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
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);
}
},
});
});
}
@@ -1,24 +1,36 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionArrows } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import { usePositionPresentationStore, OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
import _getStatusComponent from './_getStatusComponent';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import { useSystem } from '@ohif/core/src/contextProviders/SystemProvider';
import { utils } from '@ohif/extension-cornerstone';
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const { servicesManager, commandsManager } = useSystem();
const { children, displaySets, viewportOptions } = props as {
children: React.ReactNode;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportOptions;
};
const {
children,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { t } = useTranslation('SEGViewport');
const viewportId = viewportOptions.viewportId;
const { displaySetService, toolGroupService, segmentationService, customizationService } =
servicesManager.services;
const {
displaySetService,
toolGroupService,
segmentationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
'ui.loadingIndicatorTotalPercent'
@@ -35,6 +47,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
const selectedSegmentObjectIndex: number = 0;
const { setPositionPresentation } = usePositionPresentationStore();
// Hydration means that the SEG is opened and segments are loaded into the
@@ -42,7 +55,9 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
// 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,
@@ -67,10 +82,36 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
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 (
<OHIFCornerstoneViewport
<Component
{...props}
displaySets={[segDisplaySet]}
viewportOptions={{
@@ -82,35 +123,66 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
/>
onElementDisabled={onElementDisabled}
></Component>
);
}, [viewportId, segDisplaySet, toolGroupId, props, viewportOptions]);
}, [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,
});
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
}, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]);
useEffect(() => {
if (segIsLoading) {
return;
}
// if not active viewport, return
if (viewportId !== activeViewportId) {
return;
}
promptHydrateSEG({
servicesManager,
viewportId,
segDisplaySet,
hydrateCallback: async () => {
await commandsManager.runCommand('hydrateSecondaryDisplaySet', {
displaySet: segDisplaySet,
viewportId,
});
return true;
},
preHydrateCallbacks: [storePresentationState],
hydrateCallback: hydrateSEG,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
});
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, commandsManager, activeViewportId]);
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
useEffect(() => {
// on new seg display set, remove all segmentations from all viewports
@@ -211,6 +283,54 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
};
}, []);
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: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -0,0 +1,59 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui-next';
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 = () => <Icons.ByName name="status-alert" />;
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
break;
case false:
StatusIcon = () => (
<Icons.ByName
className="text-muted-foreground h-4 w-4"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SEG</span>
</div>
{!isHydrated && (
<ViewportActionButton onInteraction={onStatusClick}>{loadStr}</ViewportActionButton>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}
+4 -261
View File
@@ -3,18 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.11.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.32...v3.11.0-beta.33) (2025-05-15)
### Features
* **actions:** simplify action corner api through the toolbarService ([#5033](https://github.com/OHIF/Viewers/issues/5033)) ([7c15bb8](https://github.com/OHIF/Viewers/commit/7c15bb8901001eefedc72498176ac24f6662bfab))
# [3.11.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.31...v3.11.0-beta.32) (2025-05-13)
## [3.10.2](https://github.com/OHIF/Viewers/compare/v3.10.1...v3.10.2) (2025-06-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
@@ -22,264 +11,18 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.30...v3.11.0-beta.31) (2025-05-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.29...v3.11.0-beta.30) (2025-05-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.28...v3.11.0-beta.29) (2025-05-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.27...v3.11.0-beta.28) (2025-05-08)
### Features
* **thumbnails:** Enhance DICOM Thumbnail Rendering with different strategies ([#5004](https://github.com/OHIF/Viewers/issues/5004)) ([578c393](https://github.com/OHIF/Viewers/commit/578c3939f0271f762d6a1290216dbc4bf59dd88b))
# [3.11.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.26...v3.11.0-beta.27) (2025-05-07)
## [3.10.1](https://github.com/OHIF/Viewers/compare/v3.10.0...v3.10.1) (2025-04-11)
### Bug Fixes
* **orientationMarker:** MPR orientation change should get reflected too ([#5020](https://github.com/OHIF/Viewers/issues/5020)) ([a5ea3e1](https://github.com/OHIF/Viewers/commit/a5ea3e15c70f6b8f2d16257f5f5c774c921d82cb))
* make datasource selection work and UI fixes ([#4952](https://github.com/OHIF/Viewers/issues/4952)) ([aaba8bf](https://github.com/OHIF/Viewers/commit/aaba8bf1b04be6c00a88f1f5f29cb24133b0c39e))
# [3.11.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.25...v3.11.0-beta.26) (2025-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.24...v3.11.0-beta.25) (2025-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.23...v3.11.0-beta.24) (2025-05-05)
### Features
* **data overlay:** add foreground background control plus orientation change in the UI and add RTDOSE support ([#5007](https://github.com/OHIF/Viewers/issues/5007)) ([cd4e640](https://github.com/OHIF/Viewers/commit/cd4e6408bc2964e5ee27427cdc328751eaf4d088))
# [3.11.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.22...v3.11.0-beta.23) (2025-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.21...v3.11.0-beta.22) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.20...v3.11.0-beta.21) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.19...v3.11.0-beta.20) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.18...v3.11.0-beta.19) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.17...v3.11.0-beta.18) (2025-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.16...v3.11.0-beta.17) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.15...v3.11.0-beta.16) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.14...v3.11.0-beta.15) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.13...v3.11.0-beta.14) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.12...v3.11.0-beta.13) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.11...v3.11.0-beta.12) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.10...v3.11.0-beta.11) (2025-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.9...v3.11.0-beta.10) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.8...v3.11.0-beta.9) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.7...v3.11.0-beta.8) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.6...v3.11.0-beta.7) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.5...v3.11.0-beta.6) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.4...v3.11.0-beta.5) (2025-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.3...v3.11.0-beta.4) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.2...v3.11.0-beta.3) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.1...v3.11.0-beta.2) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.0...v3.11.0-beta.1) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.11.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.11.0-beta.0) (2025-04-10)
# [3.10.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.10.0) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.11.0-beta.33",
"version": "3.10.2",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@@ -34,10 +34,10 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.33",
"@ohif/extension-cornerstone": "3.11.0-beta.33",
"@ohif/extension-measurement-tracking": "3.11.0-beta.33",
"@ohif/ui": "3.11.0-beta.33",
"@ohif/core": "3.10.2",
"@ohif/extension-cornerstone": "3.10.2",
"@ohif/extension-measurement-tracking": "3.10.2",
"@ohif/ui": "3.10.2",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/tools": "^3.15.1",
"@cornerstonejs/adapters": "^3.11.6",
"@cornerstonejs/core": "^3.11.6",
"@cornerstonejs/tools": "^3.11.6",
"classnames": "^2.3.2"
}
}
@@ -160,18 +160,36 @@ const commandsModule = (props: withAppTypes) => {
* Loads measurements by hydrating and loading the SR for the given display set instance UID
* and displays it in the active viewport.
*/
hydrateStructuredReport: ({ displaySetInstanceUID }) => {
return hydrateStructuredReport(
loadSRMeasurements: ({ displaySetInstanceUID }) => {
const { SeriesInstanceUIDs } = hydrateStructuredReport(
{ servicesManager, extensionManager, commandsManager },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
commandsManager.run('setDisplaySetsForViewports', {
viewportsToUpdate: [
{
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
},
],
});
}
},
};
const definitions = {
downloadReport: actions.downloadReport,
storeMeasurements: actions.storeMeasurements,
hydrateStructuredReport: actions.hydrateStructuredReport,
downloadReport: {
commandFn: actions.downloadReport,
},
storeMeasurements: {
commandFn: actions.storeMeasurements,
},
loadSRMeasurements: {
commandFn: actions.loadSRMeasurements,
},
};
return {
@@ -1,24 +1,25 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useState } from 'react';
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-next';
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
import { usePositionPresentationStore, OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { useViewportGrid } from '@ohif/ui-next';
import { useSystem } from '@ohif/core/src/contextProviders/SystemProvider';
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) {
const { servicesManager } = useSystem();
const { children, dataSource, displaySets, viewportOptions } = props as {
children: React.ReactNode;
dataSource: unknown;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportOptions;
};
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
const { displaySetService } = servicesManager.services;
const { displaySetService, viewportActionCornersService } = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@@ -33,11 +34,40 @@ function OHIFCornerstoneSRMeasurementViewport(props) {
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;
@@ -75,6 +105,10 @@ function OHIFCornerstoneSRMeasurementViewport(props) {
console.warn('More than one SOPClassUID in the same series is not yet supported.');
}
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
// return;
// }
_getViewportReferencedDisplaySetData(
srDisplaySet,
newMeasurementSelected,
@@ -106,6 +140,10 @@ function OHIFCornerstoneSRMeasurementViewport(props) {
return null;
}
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const { measurements } = srDisplaySet;
const measurement = measurements[measurementSelected];
@@ -114,7 +152,7 @@ function OHIFCornerstoneSRMeasurementViewport(props) {
}
return (
<OHIFCornerstoneViewport
<Component
{...props}
// should be passed second since we don't want SR displaySet to
// override the activeImageDisplaySetData
@@ -138,10 +176,27 @@ function OHIFCornerstoneSRMeasurementViewport(props) {
onElementEnabled(evt);
}}
isJumpToMeasurementDisabled={true}
/>
></Component>
);
}, [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
*/
@@ -176,6 +231,8 @@ function OHIFCornerstoneSRMeasurementViewport(props) {
if (!srDisplaySet.isLoaded) {
await srDisplaySet.load();
}
const numMeasurements = srDisplaySet.measurements.length;
setMeasurementCount(numMeasurements);
updateViewport(measurementSelected);
};
loadSR();
@@ -198,6 +255,42 @@ function OHIFCornerstoneSRMeasurementViewport(props) {
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: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onMeasurementChange}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [isLocked, onMeasurementChange, srDisplaySet, t, viewportActionCornersService, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -234,6 +327,8 @@ OHIFCornerstoneSRMeasurementViewport.propTypes = {
children: PropTypes.node,
viewportLabel: PropTypes.string,
viewportOptions: PropTypes.object,
servicesManager: PropTypes.object.isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
async function _getViewportReferencedDisplaySetData(
@@ -277,4 +372,130 @@ async function _getViewportReferencedDisplaySetData(
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 = () => (
<Icons.ByName
name="status-alert"
className="h-4 w-4"
/>
);
ToolTipMessage = () => (
<div>
This structured report is not compatible
<br />
with this application.
</div>
);
break;
case 2:
StatusIcon = () => (
<Icons.ByName
name="status-locked"
className="h-4 w-4"
/>
);
ToolTipMessage = () => (
<div>
This structured report is currently read-only
<br />
because you are tracking measurements in
<br />
another viewport.
</div>
);
break;
case 3:
StatusIcon = () => (
<Icons.ByName
className="text-muted-foreground h-4 w-4"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>{`Click ${loadStr} to restore measurements.`}</div>;
}
const StatusArea = () => {
const { toolbarButtons: loadSRMeasurementsButtons, onInteraction } = useToolbar({
servicesManager,
buttonSection: 'loadSRMeasurements',
});
const commandOptions = {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportId,
};
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon className="h-4 w-4" />
<span className="ml-1">SR</span>
</div>
{state === 3 && (
<>
{loadSRMeasurementsButtons.map(toolDef => {
if (!toolDef) {
return null;
}
const { id, Component, componentProps } = toolDef;
const tool = (
<Component
key={id}
id={id}
onInteraction={args => onInteraction({ ...args, ...commandOptions })}
{...componentProps}
/>
);
return <div key={id}>{tool}</div>;
})}
</>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="start"
>
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}
export default OHIFCornerstoneSRMeasurementViewport;
@@ -1,5 +1,4 @@
import { utils, classes, DisplaySetService, Types as OhifTypes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import { adaptersSR } from '@cornerstonejs/adapters';
@@ -123,7 +122,6 @@ function _getDisplaySetsFromSeries(
sopClassUids,
instance,
addInstances,
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SR')}`,
};
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
@@ -356,8 +354,7 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
* @param {Object} options.extensionManager - The extension manager.
* @returns {Array} An array containing the SOP class handler module.
*/
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
@@ -1,7 +1,11 @@
import React from 'react';
import { SOPClassHandlerId, SOPClassHandlerId3D } from './id';
import { ViewportActionButton } from '@ohif/ui-next';
import i18n from '@ohif/i18n';
export default function onModeEnter({ servicesManager }) {
const { displaySetService } = servicesManager.services;
const { displaySetService, toolbarService } = servicesManager.services;
const displaySetCache = displaySetService.getDisplaySetCache();
const srDisplaySets = [...displaySetCache.values()].filter(
@@ -12,4 +16,24 @@ export default function onModeEnter({ servicesManager }) {
// 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 => (
<ViewportActionButton {...props}>{i18n.t('Common:LOAD')}</ViewportActionButton>
),
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']);
}
@@ -63,4 +63,5 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
* was not triggering annotation_added events.
*/
annotation.state.addAnnotation(SRAnnotation);
console.debug('Adding SR annotation:', SRAnnotation);
}
@@ -3,18 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.11.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.32...v3.11.0-beta.33) (2025-05-15)
### Features
* **actions:** simplify action corner api through the toolbarService ([#5033](https://github.com/OHIF/Viewers/issues/5033)) ([7c15bb8](https://github.com/OHIF/Viewers/commit/7c15bb8901001eefedc72498176ac24f6662bfab))
# [3.11.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.31...v3.11.0-beta.32) (2025-05-13)
## [3.10.2](https://github.com/OHIF/Viewers/compare/v3.10.1...v3.10.2) (2025-06-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
@@ -22,264 +11,18 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.30...v3.11.0-beta.31) (2025-05-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.29...v3.11.0-beta.30) (2025-05-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.28...v3.11.0-beta.29) (2025-05-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.27...v3.11.0-beta.28) (2025-05-08)
### Features
* **thumbnails:** Enhance DICOM Thumbnail Rendering with different strategies ([#5004](https://github.com/OHIF/Viewers/issues/5004)) ([578c393](https://github.com/OHIF/Viewers/commit/578c3939f0271f762d6a1290216dbc4bf59dd88b))
# [3.11.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.26...v3.11.0-beta.27) (2025-05-07)
## [3.10.1](https://github.com/OHIF/Viewers/compare/v3.10.0...v3.10.1) (2025-04-11)
### Bug Fixes
* **orientationMarker:** MPR orientation change should get reflected too ([#5020](https://github.com/OHIF/Viewers/issues/5020)) ([a5ea3e1](https://github.com/OHIF/Viewers/commit/a5ea3e15c70f6b8f2d16257f5f5c774c921d82cb))
* make datasource selection work and UI fixes ([#4952](https://github.com/OHIF/Viewers/issues/4952)) ([aaba8bf](https://github.com/OHIF/Viewers/commit/aaba8bf1b04be6c00a88f1f5f29cb24133b0c39e))
# [3.11.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.25...v3.11.0-beta.26) (2025-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.24...v3.11.0-beta.25) (2025-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.23...v3.11.0-beta.24) (2025-05-05)
### Features
* **data overlay:** add foreground background control plus orientation change in the UI and add RTDOSE support ([#5007](https://github.com/OHIF/Viewers/issues/5007)) ([cd4e640](https://github.com/OHIF/Viewers/commit/cd4e6408bc2964e5ee27427cdc328751eaf4d088))
# [3.11.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.22...v3.11.0-beta.23) (2025-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.21...v3.11.0-beta.22) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.20...v3.11.0-beta.21) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.19...v3.11.0-beta.20) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.18...v3.11.0-beta.19) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.17...v3.11.0-beta.18) (2025-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.16...v3.11.0-beta.17) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.15...v3.11.0-beta.16) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.14...v3.11.0-beta.15) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.13...v3.11.0-beta.14) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.12...v3.11.0-beta.13) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.11...v3.11.0-beta.12) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.10...v3.11.0-beta.11) (2025-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.9...v3.11.0-beta.10) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.8...v3.11.0-beta.9) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.7...v3.11.0-beta.8) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.6...v3.11.0-beta.7) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.5...v3.11.0-beta.6) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.4...v3.11.0-beta.5) (2025-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.3...v3.11.0-beta.4) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.2...v3.11.0-beta.3) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.1...v3.11.0-beta.2) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.0...v3.11.0-beta.1) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.11.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.11.0-beta.0) (2025-04-10)
# [3.10.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.10.0) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.11.0-beta.33",
"version": "3.10.2",
"description": "OHIF extension for 4D volumes data",
"author": "OHIF",
"license": "MIT",
@@ -29,11 +29,11 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.33",
"@ohif/extension-cornerstone": "3.11.0-beta.33",
"@ohif/extension-default": "3.11.0-beta.33",
"@ohif/i18n": "3.11.0-beta.33",
"@ohif/ui": "3.11.0-beta.33",
"@ohif/core": "3.10.2",
"@ohif/extension-cornerstone": "3.10.2",
"@ohif/extension-default": "3.10.2",
"@ohif/i18n": "3.10.2",
"@ohif/ui": "3.10.2",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/tools": "^3.15.1",
"@cornerstonejs/core": "^3.11.6",
"@cornerstonejs/tools": "^3.11.6",
"classnames": "^2.3.2"
}
}
+4 -267
View File
@@ -3,18 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.11.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.32...v3.11.0-beta.33) (2025-05-15)
### Features
* **actions:** simplify action corner api through the toolbarService ([#5033](https://github.com/OHIF/Viewers/issues/5033)) ([7c15bb8](https://github.com/OHIF/Viewers/commit/7c15bb8901001eefedc72498176ac24f6662bfab))
# [3.11.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.31...v3.11.0-beta.32) (2025-05-13)
## [3.10.2](https://github.com/OHIF/Viewers/compare/v3.10.1...v3.10.2) (2025-06-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
@@ -22,270 +11,18 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# [3.11.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.30...v3.11.0-beta.31) (2025-05-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.29...v3.11.0-beta.30) (2025-05-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.28...v3.11.0-beta.29) (2025-05-08)
### Features
* **app:** Decouple colorbar UI from service and enable action menu programatic control ([#5018](https://github.com/OHIF/Viewers/issues/5018)) ([1445e12](https://github.com/OHIF/Viewers/commit/1445e1212415e6e6957fce4a8fc6908aed69a93c))
# [3.11.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.27...v3.11.0-beta.28) (2025-05-08)
### Features
* **thumbnails:** Enhance DICOM Thumbnail Rendering with different strategies ([#5004](https://github.com/OHIF/Viewers/issues/5004)) ([578c393](https://github.com/OHIF/Viewers/commit/578c3939f0271f762d6a1290216dbc4bf59dd88b))
# [3.11.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.26...v3.11.0-beta.27) (2025-05-07)
## [3.10.1](https://github.com/OHIF/Viewers/compare/v3.10.0...v3.10.1) (2025-04-11)
### Bug Fixes
* **orientationMarker:** MPR orientation change should get reflected too ([#5020](https://github.com/OHIF/Viewers/issues/5020)) ([a5ea3e1](https://github.com/OHIF/Viewers/commit/a5ea3e15c70f6b8f2d16257f5f5c774c921d82cb))
* make datasource selection work and UI fixes ([#4952](https://github.com/OHIF/Viewers/issues/4952)) ([aaba8bf](https://github.com/OHIF/Viewers/commit/aaba8bf1b04be6c00a88f1f5f29cb24133b0c39e))
# [3.11.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.25...v3.11.0-beta.26) (2025-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.24...v3.11.0-beta.25) (2025-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.23...v3.11.0-beta.24) (2025-05-05)
### Features
* **data overlay:** add foreground background control plus orientation change in the UI and add RTDOSE support ([#5007](https://github.com/OHIF/Viewers/issues/5007)) ([cd4e640](https://github.com/OHIF/Viewers/commit/cd4e6408bc2964e5ee27427cdc328751eaf4d088))
# [3.11.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.22...v3.11.0-beta.23) (2025-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.21...v3.11.0-beta.22) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.20...v3.11.0-beta.21) (2025-05-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.19...v3.11.0-beta.20) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.18...v3.11.0-beta.19) (2025-05-01)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.17...v3.11.0-beta.18) (2025-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.16...v3.11.0-beta.17) (2025-04-29)
### Bug Fixes
* **referencelines:** and bring back the notification for worker updates but disallow duplication ([#5005](https://github.com/OHIF/Viewers/issues/5005)) ([98f8187](https://github.com/OHIF/Viewers/commit/98f8187dfeb8aec6eca145da75c855aec0da4e1d))
# [3.11.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.15...v3.11.0-beta.16) (2025-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.14...v3.11.0-beta.15) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.13...v3.11.0-beta.14) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.12...v3.11.0-beta.13) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.11...v3.11.0-beta.12) (2025-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.10...v3.11.0-beta.11) (2025-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.9...v3.11.0-beta.10) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.8...v3.11.0-beta.9) (2025-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.7...v3.11.0-beta.8) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.6...v3.11.0-beta.7) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.5...v3.11.0-beta.6) (2025-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.4...v3.11.0-beta.5) (2025-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.3...v3.11.0-beta.4) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.2...v3.11.0-beta.3) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.1...v3.11.0-beta.2) (2025-04-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.11.0-beta.0...v3.11.0-beta.1) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.11.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.11.0-beta.0) (2025-04-10)
# [3.10.0](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.159...v3.10.0) (2025-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.11.0-beta.33",
"version": "3.10.2",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -38,9 +38,9 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.15.1",
"@ohif/core": "3.11.0-beta.33",
"@ohif/ui": "3.11.0-beta.33",
"@cornerstonejs/dicom-image-loader": "^3.11.6",
"@ohif/core": "3.10.2",
"@ohif/ui": "3.10.2",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@@ -51,12 +51,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/ai": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/labelmap-interpolation": "^3.15.1",
"@cornerstonejs/polymorphic-segmentation": "^3.15.1",
"@cornerstonejs/tools": "^3.15.1",
"@cornerstonejs/adapters": "^3.11.6",
"@cornerstonejs/ai": "^3.11.6",
"@cornerstonejs/core": "^3.11.6",
"@cornerstonejs/labelmap-interpolation": "^3.11.6",
"@cornerstonejs/polymorphic-segmentation": "^3.11.6",
"@cornerstonejs/tools": "^3.11.6",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",
@@ -1,7 +1,9 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { useResizeDetector } from 'react-resize-detector';
import * as cs3DTools from '@cornerstonejs/tools';
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
import { MeasurementService, useViewportRef } from '@ohif/core';
import { MeasurementService } from '@ohif/core';
import { AllInOneMenu } from '@ohif/ui-next';
import { useViewportDialog } from '@ohif/ui-next';
import type { Types as csTypes } from '@cornerstonejs/core';
@@ -13,7 +15,8 @@ import CinePlayer from '../components/CinePlayer';
import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import ViewportColorbarsContainer from '../components/ViewportColorbar';
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
@@ -21,9 +24,6 @@ import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewport
const STACK = 'stack';
// Cache for viewport dimensions, persists across component remounts
const viewportDimensions = new Map<string, { width: number; height: number }>();
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
// Then we don't need to worry about the re-renders if the props change.
const OHIFCornerstoneViewport = React.memo(
@@ -41,6 +41,7 @@ const OHIFCornerstoneViewport = React.memo(
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
// eslint-disable-next-line react/prop-types
onElementDisabled,
@@ -82,7 +83,6 @@ const OHIFCornerstoneViewport = React.memo(
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
const viewportRef = useViewportRef(viewportId);
const {
displaySetService,
@@ -92,6 +92,7 @@ const OHIFCornerstoneViewport = React.memo(
cornerstoneViewportService,
segmentationService,
cornerstoneCacheService,
viewportActionCornersService,
customizationService,
measurementService,
} = servicesManager.services;
@@ -104,44 +105,12 @@ const OHIFCornerstoneViewport = React.memo(
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(
(entries: ResizeObserverEntry[]) => {
if (elementRef.current && entries?.length) {
const entry = entries[0];
const { width, height } = entry.contentRect;
const prevDimensions = viewportDimensions.get(viewportId) || { width: 0, height: 0 };
// Check if dimensions actually changed and then only resize if they have changed
const hasDimensionsChanged =
prevDimensions.width !== width || prevDimensions.height !== height;
if (width > 0 && height > 0 && hasDimensionsChanged) {
viewportDimensions.set(viewportId, { width, height });
// Perform resize operations
cornerstoneViewportService.resize();
setImageScrollBarHeight();
}
}
},
[viewportId, elementRef, cornerstoneViewportService, setImageScrollBarHeight]
);
useEffect(() => {
const element = elementRef.current;
if (!element) {
return;
const onResize = useCallback(() => {
if (elementRef.current) {
cornerstoneViewportService.resize();
setImageScrollBarHeight();
}
const resizeObserver = new ResizeObserver(onResize);
resizeObserver.observe(element);
// Cleanup function
return () => {
resizeObserver.unobserve(element);
resizeObserver.disconnect();
};
}, [onResize]);
}, [elementRef]);
const cleanUpServices = useCallback(
viewportInfo => {
@@ -152,8 +121,16 @@ const OHIFCornerstoneViewport = React.memo(
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
segmentationService.clearSegmentationRepresentations(viewportId);
viewportActionCornersService.clear(viewportId);
},
[viewportId, segmentationService, syncGroupService, toolGroupService]
[
viewportId,
segmentationService,
syncGroupService,
toolGroupService,
viewportActionCornersService,
]
);
const elementEnabledHandler = useCallback(
@@ -221,7 +198,6 @@ const OHIFCornerstoneViewport = React.memo(
}
cornerstoneViewportService.disableElement(viewportId);
viewportRef.unregister();
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
@@ -330,6 +306,54 @@ const OHIFCornerstoneViewport = React.memo(
};
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
const windowLevelActionMenu = customizationService.getCustomization(
'viewportActionMenu.windowLevelActionMenu'
);
const segmentationOverlay = customizationService.getCustomization(
'viewportActionMenu.segmentationOverlay'
);
if (windowLevelActionMenu?.enabled) {
viewportActionCornersService.addComponent({
viewportId,
id: 'windowLevelActionMenu',
component: getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
location: windowLevelActionMenu.location,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
}),
location: windowLevelActionMenu.location,
});
}
if (segmentationOverlay?.enabled) {
viewportActionCornersService.addComponent({
viewportId,
id: 'segmentation',
component: getViewportDataOverlaySettingsMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
location: segmentationOverlay.location,
}),
location: segmentationOverlay.location,
});
}
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
const { ref: resizeRef } = useResizeDetector({
onResize,
});
const Notification = customizationService.getCustomization('ui.notificationComponent');
return (
@@ -340,10 +364,9 @@ const OHIFCornerstoneViewport = React.memo(
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
data-viewportId={viewportId}
ref={el => {
resizeRef.current = el;
elementRef.current = el;
if (el) viewportRef.register(el);
}}
></div>
<CornerstoneOverlays
@@ -493,18 +516,22 @@ OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
function areEqual(prevProps, nextProps) {
if (nextProps.needsRerendering) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering');
return false;
}
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change');
return false;
}
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: orientation change');
return false;
}
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: toolGroupId change');
return false;
}
@@ -512,10 +539,12 @@ function areEqual(prevProps, nextProps) {
nextProps.viewportOptions.viewportType &&
prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType
) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportType change');
return false;
}
if (nextProps.viewportOptions.needsRerendering) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportOptions.needsRerendering');
return false;
}
@@ -523,6 +552,7 @@ function areEqual(prevProps, nextProps) {
const nextDisplaySets = nextProps.displaySets;
if (prevDisplaySets.length !== nextDisplaySets.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length mismatch');
return false;
}
@@ -535,11 +565,13 @@ function areEqual(prevProps, nextProps) {
);
if (!foundDisplaySet) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySet not found');
return false;
}
// check they contain the same image
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: images length mismatch');
return false;
}
@@ -547,6 +579,7 @@ function areEqual(prevProps, nextProps) {
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: imageId mismatch');
return false;
}
}
@@ -63,7 +63,7 @@ function CornerstoneImageScrollbar({
}
const { viewportType } = viewportData;
const eventId =
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_NEW_IMAGE) ||
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_VIEWPORT_SCROLL) ||
(viewportType === Enums.ViewportType.ORTHOGRAPHIC && Enums.Events.VOLUME_NEW_IMAGE) ||
Enums.Events.IMAGE_RENDERED;
@@ -72,11 +72,11 @@ function CornerstoneImageScrollbar({
if (!viewport || viewport instanceof VolumeViewport3D) {
return;
}
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
const numberOfSlices = viewport.getNumberOfSlices();
// find the index of imageId in the imageIds
setImageSliceData({
imageIndex: newImageIdIndex ?? imageIdIndex,
imageIndex: newImageIdIndex,
numberOfSlices,
});
};
@@ -11,22 +11,18 @@
.ViewportOrientationMarkers .top-mid {
top: 0.38rem;
left: 50%;
transform: translateX(-50%);
}
.ViewportOrientationMarkers .left-mid {
top: 50%;
top: 47%;
left: 0.38rem;
transform: translateY(-50%);
}
.ViewportOrientationMarkers .right-mid {
top: 50%;
top: 47%;
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
transform: translateY(-50%);
}
.ViewportOrientationMarkers .bottom-mid {
top: calc(100% - var(--marker-height) - 0.6rem);
left: 50%;
transform: translateX(-50%);
left: 47%;
}
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
display: flex;
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useMemo, useRef } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import classNames from 'classnames';
import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
@@ -22,31 +22,6 @@ function ViewportOrientationMarkers({
const [flipVertical, setFlipVertical] = useState(false);
const { cornerstoneViewportService } = servicesManager.services;
// Store initial viewUp and viewRight for volume viewports
const initialVolumeOrientationRef = useRef<{
initialViewUp: number[] | null;
initialViewRight: number[] | null;
}>({
initialViewUp: null,
initialViewRight: null,
});
useEffect(() => {
initialVolumeOrientationRef.current.initialViewUp = null;
initialVolumeOrientationRef.current.initialViewRight = null;
if (viewportData?.viewportType !== 'stack' && element && getEnabledElement(element)) {
const { viewport } = getEnabledElement(element);
const { viewUp, viewPlaneNormal } = viewport.getCamera();
const viewRight = vec3.create();
vec3.cross(viewRight, viewUp, viewPlaneNormal);
initialVolumeOrientationRef.current.initialViewUp = [...viewUp];
initialVolumeOrientationRef.current.initialViewRight = [...viewRight];
}
}, [element, viewportData]);
useEffect(() => {
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
const { previousCamera, camera } = evt.detail;
@@ -104,21 +79,14 @@ function ViewportOrientationMarkers({
return '';
}
if (
initialVolumeOrientationRef.current.initialViewUp &&
initialVolumeOrientationRef.current.initialViewRight
) {
// Use initial orientation values for consistency, even as the camera changes
columnCosines = [
-initialVolumeOrientationRef.current.initialViewUp[0],
-initialVolumeOrientationRef.current.initialViewUp[1],
-initialVolumeOrientationRef.current.initialViewUp[2],
];
rowCosines = initialVolumeOrientationRef.current.initialViewRight;
} else {
console.warn('ViewportOrientationMarkers::No initial orientation values');
return '';
}
const { viewport } = getEnabledElement(element);
const { viewUp, viewPlaneNormal } = viewport.getCamera();
const viewRight = vec3.create();
vec3.cross(viewRight, viewUp, viewPlaneNormal);
columnCosines = [-viewUp[0], -viewUp[1], -viewUp[2]];
rowCosines = viewRight;
}
if (
+22 -166
View File
@@ -3,7 +3,6 @@ import {
StackViewport,
VolumeViewport,
utilities as csUtils,
Enums as CoreEnums,
Types as CoreTypes,
BaseVolumeViewport,
} from '@cornerstonejs/core';
@@ -37,7 +36,6 @@ import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownload
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const toggleSyncFunctions = {
@@ -128,68 +126,6 @@ function commandsModule({
}
const actions = {
hydrateSecondaryDisplaySet: async ({ displaySet, viewportId }) => {
if (!displaySet) {
return;
}
if (displaySet.isOverlayDisplaySet) {
// 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,
type:
displaySet.Modality === 'SEG'
? SegmentationRepresentations.Labelmap
: SegmentationRepresentations.Contour,
});
}
const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID;
const storePositionPresentation = refDisplaySet => {
// 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,
displaySetInstanceUIDs: [refDisplaySet.displaySetInstanceUID],
});
};
if (displaySet.Modality === 'SEG' || displaySet.Modality === 'RTSTRUCT') {
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
storePositionPresentation(referencedDisplaySet);
return commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
} else if (displaySet.Modality === 'SR') {
const results = commandsManager.runCommand('hydrateStructuredReport', {
displaySetInstanceUID: displaySet.displaySetInstanceUID,
});
const { SeriesInstanceUIDs } = results;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
SeriesInstanceUIDs[0]
);
referencedDisplaySets.forEach(storePositionPresentation);
if (referencedDisplaySets.length) {
actions.setDisplaySetsForViewports({
viewportsToUpdate: [
{
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs: [referencedDisplaySets[0].displaySetInstanceUID],
},
],
});
}
return results;
}
},
runSegmentBidirectional: async ({ segmentationId, segmentIndex } = {}) => {
// Get active segmentation if not specified
const targetSegmentation =
@@ -312,7 +248,7 @@ function commandsModule({
},
updateStoredPositionPresentation: ({
viewportId,
displaySetInstanceUIDs,
displaySetInstanceUID,
referencedImageId,
options,
}) => {
@@ -320,24 +256,11 @@ function commandsModule({
const { positionPresentationStore, setPositionPresentation, getPositionPresentationId } =
usePositionPresentationStore.getState();
// Look inside positionPresentationStore and find the key that includes ALL the displaySetInstanceUIDs
// Look inside positionPresentationStore and find the key that includes the displaySetInstanceUID
// and the value has viewportId as activeViewportId.
let previousReferencedDisplaySetStoreKey;
if (
displaySetInstanceUIDs &&
Array.isArray(displaySetInstanceUIDs) &&
displaySetInstanceUIDs.length > 0
) {
previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find(
([key, value]) => {
return (
displaySetInstanceUIDs.every(uid => key.includes(uid)) &&
value.viewportId === viewportId
);
}
)?.[0];
}
const previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find(
([key, value]) => key.includes(displaySetInstanceUID) && value.viewportId === viewportId
)?.[0];
// Create presentation data with referencedImageId and options if provided
const presentationData = referencedImageId
@@ -359,7 +282,7 @@ function commandsModule({
// so we need to grab the positionPresentationId directly from the store,
// Todo: this is really hacky, we should have a better way for this
const positionPresentationId = getPositionPresentationId({
displaySetInstanceUIDs,
displaySetInstanceUIDs: [displaySetInstanceUID],
viewportId,
});
@@ -639,20 +562,10 @@ function commandsModule({
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
},
setViewportWindowLevel({
viewportId,
windowWidth,
windowCenter,
displaySetInstanceUID,
}: {
viewportId: string;
windowWidth: number;
windowCenter: number;
displaySetInstanceUID?: string;
}) {
setViewportWindowLevel({ viewportId, window, level }) {
// convert to numbers
const windowWidthNum = Number(windowWidth);
const windowCenterNum = Number(windowCenter);
const windowWidthNum = Number(window);
const windowCenterNum = Number(level);
// get actor from the viewport
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
@@ -660,28 +573,12 @@ function commandsModule({
const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum);
if (viewport instanceof BaseVolumeViewport) {
const volumeId = actions.getVolumeIdForDisplaySet({
viewportId,
displaySetInstanceUID,
});
viewport.setProperties(
{
voiRange: {
upper,
lower,
},
},
volumeId
);
} else {
viewport.setProperties({
voiRange: {
upper,
lower,
},
});
}
viewport.setProperties({
voiRange: {
upper,
lower,
},
});
viewport.render();
},
toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => {
@@ -730,19 +627,10 @@ function commandsModule({
actions.setViewportWindowLevel({
viewportId: activeViewport,
windowWidth: windowLevelPreset.window,
windowCenter: windowLevelPreset.level,
window: windowLevelPreset.window,
level: windowLevelPreset.level,
});
},
getVolumeIdForDisplaySet: ({ viewportId, displaySetInstanceUID }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof BaseVolumeViewport) {
const volumeIds = viewport.getAllVolumeIds();
const volumeId = volumeIds.find(id => id.includes(displaySetInstanceUID));
return volumeId;
}
return null;
},
setToolEnabled: ({ toolName, toggle, toolGroupId }) => {
const { viewports } = viewportGridService.getState();
@@ -831,7 +719,7 @@ function commandsModule({
return;
}
if (!toolGroup?.hasTool(toolName)) {
if (!toolGroup.hasTool(toolName)) {
return;
}
@@ -1131,7 +1019,7 @@ function commandsModule({
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup?.hasTool(toolName)) {
if (!toolGroup.hasTool(toolName)) {
return;
}
@@ -1301,9 +1189,6 @@ function commandsModule({
},
/**
* Creates a labelmap for the active viewport
*
* The created labelmap will be registered as a display set and also added
* as a segmentation representation to the viewport.
*/
createLabelmapForViewport: async ({ viewportId, options = {} }) => {
const { viewportGridService, displaySetService, segmentationService } =
@@ -1324,7 +1209,6 @@ function commandsModule({
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
// This will create the segmentation and register it as a display set
const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet(
displaySet,
{
@@ -1341,7 +1225,6 @@ function commandsModule({
}
);
// Also add the segmentation representation to the viewport
await segmentationService.addSegmentationRepresentation(viewportId, {
segmentationId,
type: Enums.SegmentationRepresentations.Labelmap,
@@ -1851,37 +1734,13 @@ function commandsModule({
displaySetInstanceUIDs,
});
actions.setDisplaySetsForViewports({
viewportsToUpdate: updatedViewports.map(viewport => ({
updatedViewports.forEach(viewport => {
viewportGridService.setDisplaySetsForViewport({
viewportId: viewport.viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
})),
});
});
},
setViewportOrientation: ({ viewportId, orientation }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport.type !== CoreEnums.ViewportType.ORTHOGRAPHIC) {
console.warn('Orientation can only be set on volume viewports');
return;
}
// Get display sets for this viewport to verify at least one is reconstructable
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const displaySets = displaySetUIDs.map(uid => displaySetService.getDisplaySetByUID(uid));
if (!displaySets.some(ds => ds.isReconstructable)) {
console.warn('Cannot change orientation: No reconstructable display sets in viewport');
return;
}
viewport.setOrientation(orientation);
viewport.render();
// update the orientation in the viewport info
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
viewportInfo.setOrientation(orientation);
},
};
const definitions = {
@@ -2162,9 +2021,6 @@ function commandsModule({
decreaseBrushSize: actions.decreaseBrushSize,
addNewSegment: actions.addNewSegment,
loadSegmentationDisplaySetsForViewport: actions.loadSegmentationDisplaySetsForViewport,
setViewportOrientation: actions.setViewportOrientation,
hydrateSecondaryDisplaySet: actions.hydrateSecondaryDisplaySet,
getVolumeIdForDisplaySet: actions.getVolumeIdForDisplaySet,
};
return {
@@ -1,116 +0,0 @@
import { useToolbar, useViewportMousePosition } from '@ohif/core/src/hooks';
import React, { useState, useEffect, useRef } from 'react';
import { useViewportRendering } from '../../hooks';
function AdvancedRenderingControls({
viewportId,
location,
buttonSection,
}: {
viewportId: string;
location: number;
buttonSection: string;
}) {
const {
onInteraction,
toolbarButtons,
isItemOpen,
isItemLocked,
openItem,
closeItem,
toggleLock,
} = useToolbar({
buttonSection,
});
const mousePosition = useViewportMousePosition(viewportId);
const [isAtBottom, setIsAtBottom] = useState(false);
const [showAllIcons, setShowAllIcons] = useState(true);
const firstMountRef = useRef(true);
const { hasColorbar } = useViewportRendering(viewportId);
useEffect(() => {
if (firstMountRef.current) {
firstMountRef.current = false;
const timer = setTimeout(() => {
setShowAllIcons(false);
}, 3000);
return () => clearTimeout(timer);
}
}, []);
useEffect(() => {
if (!showAllIcons && mousePosition.isInViewport) {
if (mousePosition.isInBottomPercentage(10)) {
setIsAtBottom(true);
} else {
setIsAtBottom(false);
}
}
}, [mousePosition, showAllIcons]);
if (!toolbarButtons?.length) {
return null;
}
if (!hasColorbar) {
return null;
}
return (
<div className="flex flex-row gap-2">
{toolbarButtons.map(toolDef => {
if (!toolDef) {
return null;
}
const { id, Component, componentProps } = toolDef;
// Enhanced props with state and actions - respecting viewport specificity
const enhancedProps = {
...componentProps,
isOpen: isItemOpen(id, viewportId),
isLocked: isItemLocked(id, viewportId),
onOpen: () => openItem(id, viewportId),
onClose: () => closeItem(id, viewportId),
onToggleLock: () => toggleLock(id, viewportId),
viewportId,
};
const tool = (
<Component
key={id}
id={id}
location={location}
onInteraction={args => {
onInteraction({
...args,
itemId: id,
viewportId,
});
}}
{...enhancedProps}
/>
);
// Always show all icons on first mount for 3 seconds
// After that, always show Colorbar, show others only when mouse is at bottom
const shouldBeVisible = showAllIcons || id === 'Colorbar' || isAtBottom;
return (
<div
key={id}
className={shouldBeVisible ? 'opacity-100' : 'pointer-events-none opacity-0'}
style={{ transition: 'opacity 0.2s ease-in-out' }}
>
{tool}
</div>
);
})}
</div>
);
}
export default AdvancedRenderingControls;
@@ -1 +0,0 @@
export { default } from './AdvancedRenderingControls';
@@ -1,168 +0,0 @@
import React, { useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui-next';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
import { useSystem } from '@ohif/core/src';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import { useMeasurementTracking } from '../../hooks/useMeasurementTracking';
/**
* ModalityLoadBadge displays the status and actionable buttons for viewports containing
* special displaySets (SR, SEG, RTSTRUCT) or when tracking measurements
*/
function ModalityLoadBadge({ viewportId }: { viewportId: string }) {
const { commandsManager } = useSystem();
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
const { isTracked, isLocked } = useMeasurementTracking({ viewportId });
const { backgroundDisplaySet, overlayDisplaySets } = useViewportDisplaySets(viewportId);
const [specialDisplaySet, setSpecialDisplaySet] = useState(null);
const allDisplaySets = useMemo(() => {
return [backgroundDisplaySet, ...overlayDisplaySets].filter(Boolean);
}, [backgroundDisplaySet, overlayDisplaySets]);
useEffect(() => {
const displaySet = allDisplaySets.find(ds => ds.isOverlayDisplaySet || ds?.Modality === 'SR');
setSpecialDisplaySet(displaySet || null);
}, [allDisplaySets]);
const statusInfo = useMemo(() => {
if (isTracked && !specialDisplaySet) {
return {
type: 'SR',
isHydrated: true,
isTracked,
displaySet: null,
tooltip: 'Currently tracking measurements in this viewport',
};
}
if (!specialDisplaySet) {
return {
type: null,
};
}
const type = specialDisplaySet.Modality;
const isHydrated = specialDisplaySet.isHydrated || false;
let tooltip = null;
const isRehydratable = specialDisplaySet.isRehydratable || false;
if (type === 'SEG') {
tooltip = isHydrated
? 'This Segmentation is loaded in the segmentation panel'
: 'Click LOAD to load segmentation.';
} else if (type === 'RTSTRUCT') {
tooltip = isHydrated
? 'This RTSTRUCT is loaded in the segmentation panel'
: 'Click LOAD to load RTSTRUCT.';
} else if (type === 'SR') {
if (!isRehydratable) {
tooltip = 'This structured report is not compatible with this application.';
} else if (isRehydratable && isLocked) {
tooltip =
'This structured report is currently read-only because you are tracking measurements in another viewport.';
} else {
tooltip = `Click ${loadStr} to restore measurements.`;
}
}
return {
type,
isHydrated,
displaySet: specialDisplaySet,
isRehydratable,
tooltip,
};
}, [specialDisplaySet, loadStr, isLocked, isTracked]);
// Nothing to show if there's no special display set type or tracking
if (!statusInfo.type && !isTracked) {
return null;
}
const StatusIcon = () => {
if (statusInfo.type === 'SR') {
if (statusInfo.isRehydratable && !isLocked) {
return (
<Icons.ByName
className="text-muted-foreground h-4 w-4"
name="status-untracked"
/>
);
}
if (statusInfo.isRehydratable && isLocked) {
return (
<Icons.ByName
name="status-locked"
className="h-4 w-4"
/>
);
} else {
return (
<Icons.ByName
name="status-alert"
className="h-4 w-4"
/>
);
}
} else {
return <Icons.StatusUntracked className="h-4 w-4" />;
}
};
const StatusArea = () => {
if (!statusInfo.isHydrated) {
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">{statusInfo.type}</span>
</div>
{/* We don't show the load button for SRs because we handle it in the SR extension
via the tracked measurement context that works with state machine, this is not a regression right now */}
{statusInfo.type !== 'SR' && (
<ViewportActionButton
onInteraction={() => {
commandsManager.runCommand('hydrateSecondaryDisplaySet', {
displaySet: statusInfo.displaySet,
viewportId,
});
}}
>
{loadStr}
</ViewportActionButton>
)}
</div>
);
}
return null;
};
return (
<>
{statusInfo.tooltip && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<div>{statusInfo.tooltip}</div>
</TooltipContent>
</Tooltip>
)}
{!statusInfo.tooltip && <StatusArea />}
</>
);
}
export default ModalityLoadBadge;
@@ -1 +0,0 @@
export { default } from './ModalityLoadBadge';
@@ -1,151 +0,0 @@
import React, { useCallback, useState } from 'react';
import { ViewportActionArrows } from '@ohif/ui-next';
import { useSystem } from '@ohif/core/src';
import { utils } from '../..';
import { useViewportSegmentations } from '../../hooks';
import { useMeasurementTracking } from '../../hooks/useMeasurementTracking';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
/**
* NavigationComponent provides navigation controls for viewports containing
* special displaySets (SR, SEG, RTSTRUCT) to navigate between segments or measurements
*/
function NavigationComponent({ viewportId }: { viewportId: string }) {
const { servicesManager } = useSystem();
const { segmentationService, cornerstoneViewportService, measurementService } =
servicesManager.services;
// Get tracking information
const { isTracked, trackedMeasurementUIDs } = useMeasurementTracking({ viewportId });
const { viewportDisplaySets } = useViewportDisplaySets(viewportId);
const [measurementSelected, setMeasurementSelected] = useState(0);
const isSRDisplaySet = viewportDisplaySets.some(displaySet => displaySet?.Modality === 'SR');
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
// Get segmentation information
const { segmentationsWithRepresentations } = useViewportSegmentations({
viewportId,
});
const hasSegmentations = segmentationsWithRepresentations.length > 0;
// prefer segment navigation if available
const navigationMode = hasSegmentations
? 'segment'
: isSRDisplaySet
? 'measurement'
: isTracked
? 'measurement'
: null;
const handleMeasurementNavigation = useCallback(
(direction: number) => {
const measurementDisplaySet = viewportDisplaySets.find(
displaySet => displaySet?.Modality === 'SR'
);
if (measurementDisplaySet) {
const measurements = measurementDisplaySet.measurements;
if (measurements.length <= 0) {
return;
}
const newIndex = getNextIndex(measurementSelected, direction, measurements.length);
setMeasurementSelected(newIndex);
const measurement = measurements[newIndex];
cornerstoneViewport.setViewReference({
referencedImageId: measurement.imageId,
});
return;
}
if (isTracked && trackedMeasurementUIDs.length > 0) {
const newIndex = getNextIndex(
measurementSelected,
direction,
trackedMeasurementUIDs.length
);
setMeasurementSelected(newIndex);
measurementService.jumpToMeasurement(viewportId, trackedMeasurementUIDs[newIndex]);
}
},
[
viewportId,
cornerstoneViewport,
measurementSelected,
measurementService,
isTracked,
trackedMeasurementUIDs,
viewportDisplaySets,
]
);
const handleSegmentNavigation = useCallback(
(direction: number) => {
if (!segmentationsWithRepresentations.length) {
return;
}
const segmentationId = segmentationsWithRepresentations[0].segmentation.segmentationId;
utils.handleSegmentChange({
direction,
segmentationId,
viewportId,
selectedSegmentObjectIndex: 0,
segmentationService,
});
},
[segmentationsWithRepresentations, viewportId, segmentationService]
);
// Handle navigation between segments/measurements
const handleNavigate = useCallback(
(direction: number) => {
if (navigationMode === 'segment') {
handleSegmentNavigation(direction);
} else if (navigationMode === 'measurement') {
handleMeasurementNavigation(direction);
}
},
[navigationMode, handleSegmentNavigation, handleMeasurementNavigation]
);
// Only render if we have a navigation mode
if (!navigationMode) {
return null;
}
return (
<ViewportActionArrows
onArrowsClick={handleNavigate}
className="h-6"
/>
);
}
/**
* Calculate the next index with circular navigation support
* @param currentIndex Current index position
* @param direction Direction of movement (1 for next, -1 for previous)
* @param totalItems Total number of items to navigate through
* @returns The next index with wrap-around support
*/
function getNextIndex(currentIndex: number, direction: number, totalItems: number): number {
if (totalItems <= 0) {
return 0;
}
// Use modulo to handle circular navigation
let nextIndex = (currentIndex + direction) % totalItems;
// Handle negative index when going backwards from index 0
if (nextIndex < 0) {
nextIndex = totalItems - 1;
}
return nextIndex;
}
export default NavigationComponent;
@@ -1,3 +0,0 @@
import NavigationComponent from './NavigationComponent';
export default NavigationComponent;
@@ -1,94 +1,25 @@
import React, { memo } from 'react';
import { ViewportActionCorners, IconPresentationProvider, ToolButton } from '@ohif/ui-next';
import { Toolbar } from '@ohif/extension-default/src/Toolbar/Toolbar';
import { ButtonLocation } from '@ohif/core/src/services/ToolBarService/ToolbarService';
import { useViewportHover } from '../hooks';
import React from 'react';
import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
import { useSystem } from '@ohif/core';
export type OHIFViewportActionCornersProps = {
viewportId: string;
};
function OHIFViewportActionCornersComponent({ viewportId }: OHIFViewportActionCornersProps) {
// Use the viewport hover hook to track if viewport is hovered or active
const { isHovered, isActive } = useViewportHover(viewportId);
const shouldShowCorners = isHovered || isActive;
if (!shouldShowCorners) {
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
const { servicesManager } = useSystem();
const [viewportActionCornersState] = useViewportActionCornersContext();
const ViewportActionCorners =
servicesManager.services.customizationService.getCustomization('ui.viewportActionCorner');
if (!viewportActionCornersState[viewportId]) {
return null;
}
return (
<IconPresentationProvider
size="medium"
IconContainer={ToolButton}
containerProps={{
size: 'tiny',
className: 'font-normal text-primary hover:bg-primary/25',
}}
>
<ViewportActionCorners.Container>
<ViewportActionCorners.TopLeft>
<Toolbar
buttonSection="viewportActionMenu.topLeft"
viewportId={viewportId}
location={ButtonLocation.TopLeft}
/>
</ViewportActionCorners.TopLeft>
<ViewportActionCorners.TopMiddle>
<Toolbar
buttonSection="viewportActionMenu.topMiddle"
viewportId={viewportId}
location={ButtonLocation.TopMiddle}
/>
</ViewportActionCorners.TopMiddle>
<ViewportActionCorners.TopRight>
<Toolbar
buttonSection="viewportActionMenu.topRight"
viewportId={viewportId}
location={ButtonLocation.TopRight}
/>
</ViewportActionCorners.TopRight>
<ViewportActionCorners.LeftMiddle>
<Toolbar
buttonSection="viewportActionMenu.leftMiddle"
viewportId={viewportId}
location={ButtonLocation.LeftMiddle}
/>
</ViewportActionCorners.LeftMiddle>
<ViewportActionCorners.RightMiddle>
<Toolbar
buttonSection="viewportActionMenu.rightMiddle"
viewportId={viewportId}
location={ButtonLocation.RightMiddle}
/>
</ViewportActionCorners.RightMiddle>
<ViewportActionCorners.BottomLeft>
<Toolbar
buttonSection="viewportActionMenu.bottomLeft"
viewportId={viewportId}
location={ButtonLocation.BottomLeft}
/>
</ViewportActionCorners.BottomLeft>
<ViewportActionCorners.BottomMiddle>
<Toolbar
buttonSection="viewportActionMenu.bottomMiddle"
viewportId={viewportId}
location={ButtonLocation.BottomMiddle}
/>
</ViewportActionCorners.BottomMiddle>
<ViewportActionCorners.BottomRight>
<Toolbar
buttonSection="viewportActionMenu.bottomRight"
viewportId={viewportId}
location={ButtonLocation.BottomRight}
/>
</ViewportActionCorners.BottomRight>
</ViewportActionCorners.Container>
</IconPresentationProvider>
<ViewportActionCorners
cornerComponents={viewportActionCornersState[viewportId]}
></ViewportActionCorners>
);
}
const OHIFViewportActionCorners = memo(OHIFViewportActionCornersComponent);
export default OHIFViewportActionCorners;
@@ -1,54 +0,0 @@
import React from 'react';
import { Numeric } from '@ohif/ui-next';
import { useViewportDisplaySets, useViewportRendering } from '../../hooks';
interface OpacityMenuProps {
viewportId: string;
className?: string;
}
function OpacityMenu({ viewportId, className }: OpacityMenuProps) {
const { backgroundDisplaySet, foregroundDisplaySets } = useViewportDisplaySets(viewportId, {
includeForeground: true,
includeBackground: true,
});
const { opacityLinear, setOpacityLinear } = useViewportRendering(viewportId, {
displaySetInstanceUID: foregroundDisplaySets[0].displaySetInstanceUID,
});
const opacityValue = opacityLinear !== undefined ? opacityLinear : 0;
const backgroundModality = backgroundDisplaySet?.Modality || '';
const foregroundModality = foregroundDisplaySets[0]?.Modality || '';
return (
<div className={className}>
<div className="bg-popover w-72 rounded-lg p-3">
<div className="mb-2 flex items-center justify-center">
<span className="text-muted-foreground text-base">Opacity</span>
</div>
<div className="">
<Numeric.Container
mode="singleRange"
value={opacityValue}
onChange={(val: number | [number, number]) => {
if (typeof val === 'number') {
setOpacityLinear(val);
}
}}
min={0}
max={1}
step={0.0001}
>
<div className="flex items-center">
<span className="text-foreground mr-2 text-sm">{backgroundModality}</span>
<Numeric.SingleRange showNumberInput={false} />
<span className="text-foreground ml-2 text-sm">{foregroundModality}</span>
</div>
</Numeric.Container>
</div>
</div>
</div>
);
}
export default OpacityMenu;
@@ -1,85 +0,0 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useIconPresentation,
} from '@ohif/ui-next';
import OpacityMenu from './OpacityMenu';
type OpacityMenuWrapperProps = {
viewportId: string;
location: string;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
};
export function OpacityMenuWrapper(props: OpacityMenuWrapperProps): ReactNode {
const { viewportId, location, isOpen = false, onOpen, onClose, disabled, ...rest } = props;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const handleOpenChange = (openState: boolean) => {
if (openState) {
onOpen?.();
} else {
onClose?.();
}
};
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
const { align, side } = toolbarService.getAlignAndSide(location);
const Icon = <Icons.Opacity className={iconClassName} />;
return (
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="Opacity"
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<OpacityMenu
className="w-full"
viewportId={viewportId}
/>
</PopoverContent>
</Popover>
);
}
@@ -1,2 +0,0 @@
export { OpacityMenuWrapper } from './OpacityMenuWrapper';
export { default as OpacityMenu } from './OpacityMenu';
@@ -1,21 +0,0 @@
import React from 'react';
// should be used in a Select component
const SelectItemWithModality = ({
displaySet,
showModality = true,
}: {
displaySet: AppTypes.DisplaySet;
showModality?: boolean;
}): JSX.Element => (
<div className="flex w-[90%] items-center justify-between">
<span className="text-foreground truncate text-base">{displaySet.label}</span>
{showModality && displaySet.Modality && (
<span className="text-muted-foreground flex-shrink-0 whitespace-nowrap text-xs">
{displaySet.Modality}
</span>
)}
</div>
);
export default SelectItemWithModality;
@@ -1,104 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Numeric,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
Button,
} from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import SelectItemWithModality from '../SelectItemWithModality';
interface ThresholdMenuProps {
viewportId: string;
className?: string;
}
function ThresholdMenu({ viewportId, className }: ThresholdMenuProps) {
const { viewportDisplaySets, foregroundDisplaySets } = useViewportDisplaySets(viewportId);
const [selectedDisplaySetUID, setSelectedDisplaySetUID] = useState<string | undefined>(
foregroundDisplaySets.length > 0 ? foregroundDisplaySets[0].displaySetInstanceUID : undefined
);
const { threshold, setThreshold, pixelValueRange } = useViewportRendering(viewportId, {
displaySetInstanceUID: selectedDisplaySetUID,
});
const { min, max } = pixelValueRange;
const thresholdValue = threshold ?? min;
useEffect(() => {
if (foregroundDisplaySets.length > 0 && !selectedDisplaySetUID) {
setSelectedDisplaySetUID(foregroundDisplaySets[0].displaySetInstanceUID);
}
}, [foregroundDisplaySets, selectedDisplaySetUID]);
const selectedDisplaySet = viewportDisplaySets.find(
ds => ds.displaySetInstanceUID === selectedDisplaySetUID
);
return (
<div className={className}>
<div className="bg-popover w-72 rounded-lg p-4 shadow-md">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-muted-foreground text-base">Threshold</span>
{viewportDisplaySets.length > 1 && (
<div className="w-32">
<Select
value={selectedDisplaySetUID}
onValueChange={setSelectedDisplaySetUID}
>
<SelectTrigger>
<SelectValue>{selectedDisplaySet?.label || 'Select Display Set'}</SelectValue>
</SelectTrigger>
<SelectContent>
{viewportDisplaySets.map(ds => (
<SelectItem
key={ds.displaySetInstanceUID}
value={ds.displaySetInstanceUID}
>
<SelectItemWithModality displaySet={ds} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
<Button
size="sm"
variant="ghost"
onClick={() => setThreshold(min)}
className="text-sm"
>
Reset
</Button>
</div>
<Numeric.Container
mode="singleRange"
value={thresholdValue}
onChange={(val: number | [number, number]) => {
if (typeof val === 'number') {
setThreshold(val);
}
}}
min={min}
max={max}
step={0.01}
>
<Numeric.SingleRange />
<div className="mt-1 flex justify-between">
<span className="text-muted-foreground text-sm">{min.toFixed(0)}</span>
<span className="text-muted-foreground text-sm">{max.toFixed(0)}</span>
</div>
</Numeric.Container>
</div>
</div>
);
}
export default ThresholdMenu;
@@ -1,85 +0,0 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useIconPresentation,
} from '@ohif/ui-next';
import ThresholdMenu from './ThresholdMenu';
type ThresholdMenuWrapperProps = {
viewportId: string;
location: string;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
};
export function ThresholdMenuWrapper(props: ThresholdMenuWrapperProps): ReactNode {
const { viewportId, location, isOpen = false, onOpen, onClose, disabled, ...rest } = props;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const handleOpenChange = (openState: boolean) => {
if (openState) {
onOpen?.();
} else {
onClose?.();
}
};
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
const { align, side } = toolbarService.getAlignAndSide(location);
const Icon = <Icons.Threshold className={iconClassName} />;
return (
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="Threshold"
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<ThresholdMenu
className="w-full"
viewportId={viewportId}
/>
</PopoverContent>
</Popover>
);
}
@@ -1,2 +0,0 @@
export { ThresholdMenuWrapper } from './ThresholdMenuWrapper';
export { default as ThresholdMenu } from './ThresholdMenu';
@@ -1,32 +0,0 @@
import React from 'react';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
import { useMeasurementTracking } from '../../hooks/useMeasurementTracking';
/**
* TrackingStatus displays an indicator icon for viewports that have
* tracked measurements
*/
function TrackingStatus({ viewportId }: { viewportId: string }) {
const { isTracked } = useMeasurementTracking({ viewportId });
if (!isTracked) {
return null;
}
return (
<>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Icons.StatusTracking className="h-4 w-4" />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<div>Tracking</div>
</TooltipContent>
</Tooltip>
</>
);
}
export default TrackingStatus;
@@ -1 +0,0 @@
export { default } from './TrackingStatus';
@@ -1,190 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
Numeric,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from '@ohif/ui-next';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import { useSystem } from '@ohif/core';
import { cache } from '@cornerstonejs/core';
interface VOIManualControlMenuProps {
viewportId: string;
className?: string;
}
const TABS = {
MINMAX: 'minmax',
MANUAL: 'manual',
};
function VOIManualControlMenu({ viewportId, className }: VOIManualControlMenuProps) {
const { servicesManager } = useSystem();
const { displaySetService } = servicesManager.services;
const [activeTab, setActiveTab] = useState(TABS.MINMAX);
const { viewportDisplaySets } = useViewportDisplaySets(viewportId);
const [selectedDisplaySetUID, setSelectedDisplaySetUID] = useState<string | undefined>(
viewportDisplaySets.length > 0 ? viewportDisplaySets[0].displaySetInstanceUID : undefined
);
const { voiRange, setVOIRange, windowLevel, setWindowLevel } = useViewportRendering(viewportId, {
displaySetInstanceUID: selectedDisplaySetUID,
});
useEffect(() => {
if (viewportDisplaySets.length > 0 && !selectedDisplaySetUID) {
setSelectedDisplaySetUID(viewportDisplaySets[0].displaySetInstanceUID);
}
}, [viewportDisplaySets, selectedDisplaySetUID]);
if (!voiRange) {
return null;
}
const { upper, lower } = voiRange;
const { windowWidth, windowCenter } = windowLevel;
const selectedViewportImageIds =
displaySetService.getDisplaySetByUID(selectedDisplaySetUID)?.imageIds;
let min = Infinity;
let max = -Infinity;
if (selectedViewportImageIds) {
for (const imageId of selectedViewportImageIds) {
const image = cache.getImage(imageId);
if (image) {
min = Math.min(min, image.minPixelValue);
max = Math.max(max, image.maxPixelValue);
}
}
}
// Provide reasonable defaults if min/max couldn't be determined
if (min === Infinity || max === -Infinity) {
min = 0;
max = 255;
}
const selectedDisplaySet = viewportDisplaySets.find(
ds => ds.displaySetInstanceUID === selectedDisplaySetUID
);
return (
<div className={className}>
<div className="bg-popover w-72 rounded-lg p-4 shadow-md">
<Tabs
defaultValue={activeTab}
onValueChange={setActiveTab}
>
<div className="mb-4 flex items-center space-x-2">
{viewportDisplaySets.length > 1 && (
<Select
value={selectedDisplaySetUID}
onValueChange={setSelectedDisplaySetUID}
>
<SelectTrigger>
<SelectValue>{selectedDisplaySet?.label || 'Select Display Set'}</SelectValue>
</SelectTrigger>
<SelectContent>
{viewportDisplaySets.map(ds => (
<SelectItem
key={ds.displaySetInstanceUID}
value={ds.displaySetInstanceUID}
>
{`${ds.SeriesDescription || ''}`.trim()}
</SelectItem>
))}
</SelectContent>
</Select>
)}
<TabsList className="w-full flex-1">
<TabsTrigger
value={TABS.MINMAX}
className="flex-1"
>
Min/Max
</TabsTrigger>
<TabsTrigger
value={TABS.MANUAL}
className="flex-1"
>
Manual
</TabsTrigger>
</TabsList>
</div>
<TabsContent value={TABS.MINMAX}>
<Numeric.Container
mode="doubleRange"
min={min}
max={max}
values={[lower, upper]}
step={1}
className="space-y-1"
onChange={(vals: [number, number]) => {
const [newLower, newUpper] = vals;
if (newLower !== lower || newUpper !== upper) {
setVOIRange({ lower: newLower, upper: newUpper });
}
}}
>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
</TabsContent>
<TabsContent value={TABS.MANUAL}>
<div className="space-y-1">
<Numeric.Container
mode="singleRange"
min={0}
max={max}
step={1}
value={windowWidth}
className="space-y-1"
onChange={(val: number) => {
if (val !== windowWidth) {
setWindowLevel({ windowWidth: val, windowCenter });
}
}}
>
<div className="flex items-center space-x-2">
<Numeric.Label>W:</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</div>
</Numeric.Container>
<Numeric.Container
mode="singleRange"
min={min}
max={max}
step={1}
value={windowCenter}
className="space-y-1"
onChange={(val: number) => {
if (val !== windowCenter) {
setWindowLevel({ windowWidth, windowCenter: val });
}
}}
>
<div className="flex items-center space-x-2">
<Numeric.Label>L:</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</div>
</Numeric.Container>
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
}
export default VOIManualControlMenu;
@@ -1,85 +0,0 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useIconPresentation,
} from '@ohif/ui-next';
import VOIManualControlMenu from './VOIManualControlMenu';
type VOIManualControlMenuWrapperProps = {
viewportId: string;
location: string;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
};
export function VOIManualControlMenuWrapper(props: VOIManualControlMenuWrapperProps): ReactNode {
const { viewportId, location, isOpen = false, onOpen, onClose, disabled, ...rest } = props;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const handleOpenChange = (openState: boolean) => {
if (openState) {
onOpen?.();
} else {
onClose?.();
}
};
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
const { align, side } = toolbarService.getAlignAndSide(location);
const Icon = <Icons.WindowLevelAdvanced className={iconClassName} />;
return (
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="WindowLevelAdvanced"
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<VOIManualControlMenu
className="w-full"
viewportId={viewportId}
/>
</PopoverContent>
</Popover>
);
}
@@ -1,2 +0,0 @@
export { VOIManualControlMenuWrapper } from './VOIManualControlMenuWrapper';
export { default as VOIManualControlMenu } from './VOIManualControlMenu';
@@ -1,154 +0,0 @@
import React, { useEffect, useRef, useMemo, memo } from 'react';
import { utilities } from '@cornerstonejs/tools';
import { useSystem, useViewportRef, useViewportSize } from '@ohif/core';
import {
ColorbarPositionType,
TickPositionType,
ColorbarCustomization,
TickStyleType,
ContainerStyleType,
} from '../../types/Colorbar';
import { ColorbarRangeTextPosition } from '@cornerstonejs/tools/utilities/voi/colorbar/enums/ColorbarRangeTextPosition';
const { ViewportColorbar: CornerstoneViewportColorbar } = utilities.voi.colorbar;
type ColorbarProps = {
viewportId: string;
displaySetInstanceUID: string;
colormap?: any;
colormaps: any[];
activeColormapName: string;
volumeId?: string;
position: ColorbarPositionType;
tickPosition: TickPositionType;
tickStyles?: TickStyleType;
containerStyles?: ContainerStyleType;
viewportElementRef?: React.RefObject<HTMLDivElement>;
numColorbars: number;
};
/**
* ViewportColorbar Component
* A React wrapper for the cornerstone ViewportColorbar that adds a close button
* positioned appropriately based on the colorbar position.
*/
const ViewportColorbar = memo(function ViewportColorbar({
viewportId,
displaySetInstanceUID,
colormaps,
activeColormapName,
volumeId,
position,
tickPosition,
tickStyles,
numColorbars,
}: ColorbarProps) {
const containerRef = useRef<HTMLDivElement>(null);
const { servicesManager } = useSystem();
const { customizationService } = servicesManager.services;
const viewportElementRef = useViewportRef(viewportId);
const { height, width } = useViewportSize(viewportId);
// Memoize colorbar customization to prevent rerenders from unrelated customization changes
const colorbarCustomization = useMemo(() => {
return customizationService.getCustomization(
'cornerstone.colorbar'
) as unknown as ColorbarCustomization;
}, [customizationService]);
const appropriateTickPosition = useMemo(() => {
let tickPos = tickPosition;
if (position === 'left' || position === 'right') {
tickPos = position === 'left' ? 'right' : 'left';
} else {
tickPos = position === 'top' ? 'bottom' : 'top';
}
return tickPos;
}, [position, tickPosition]);
const positionTickStyles = useMemo(() => {
return colorbarCustomization?.positionTickStyles?.[position];
}, [colorbarCustomization, position]);
const positionStylesFromConfig = useMemo(() => {
return colorbarCustomization?.positionStyles?.[position] || {};
}, [colorbarCustomization, position]);
const mergedTickStyles = useMemo(() => {
return {
...(colorbarCustomization?.tickStyles || {}),
...(positionTickStyles?.style || {}),
...(tickStyles || {}),
};
}, [colorbarCustomization, positionTickStyles, tickStyles]);
const colorbarId = useMemo(() => {
return `Colorbar-${viewportId}-${displaySetInstanceUID}`;
}, [viewportId, displaySetInstanceUID]);
useEffect(() => {
if (!containerRef.current || !colormaps || !activeColormapName) {
return;
}
const viewportElement = viewportElementRef?.current;
if (!viewportElement || !colormaps?.length) {
return;
}
// Using stable references from memoized values
const csColorbar = new CornerstoneViewportColorbar({
id: colorbarId,
element: viewportElement,
container: containerRef.current,
colormaps: colormaps,
activeColormapName: activeColormapName,
volumeId,
ticks: {
position: appropriateTickPosition as ColorbarRangeTextPosition,
style: mergedTickStyles,
},
});
return () => {
if (csColorbar) {
csColorbar.destroy();
}
};
}, [
viewportId,
displaySetInstanceUID,
colormaps,
activeColormapName,
volumeId,
colorbarId,
appropriateTickPosition,
mergedTickStyles,
viewportElementRef,
]);
if (!height || !width) {
return null;
}
return (
<div
id={`colorbar-container-${viewportId}-${displaySetInstanceUID}`}
ref={containerRef}
style={{
position: 'relative',
zIndex: 1000,
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center',
pointerEvents: 'auto',
minWidth: position === 'bottom' ? width / 2.5 : '17px',
minHeight: position === 'bottom' ? '20px' : numColorbars === 1 ? height / 3 : height / 4,
...positionStylesFromConfig,
}}
></div>
);
});
export default ViewportColorbar;
@@ -1,128 +0,0 @@
import React, { useEffect, useState, useMemo, memo, useCallback } from 'react';
import { useSystem } from '@ohif/core';
import { ColorbarCustomization } from '../../types/Colorbar';
import type { ColorMapPreset } from '../../types/Colormap';
import ViewportColorbar from './ViewportColorbar';
import useViewportRendering from '../../hooks/useViewportRendering';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
type ViewportColorbarsContainerProps = {
viewportId: string;
location: number;
};
type ColorbarData = {
colorbar: {
activeColormapName: string;
colormaps: ColorMapPreset[];
volumeId?: string;
};
displaySetInstanceUID: string;
};
/**
* Container component that manages multiple colorbars for a viewport
* It interacts with the colorbarService to get/set colorbar states
*/
const ViewportColorbarsContainer = memo(function ViewportColorbarsContainer({
viewportId,
location,
}: ViewportColorbarsContainerProps) {
const [colorbars, setColorbars] = useState<ColorbarData[]>([]);
const { servicesManager } = useSystem();
const { colorbarService, customizationService, displaySetService } = servicesManager.services;
const { viewportDisplaySets, backgroundDisplaySet, foregroundDisplaySets } =
useViewportDisplaySets(viewportId);
const { colorbarPosition: position, opacity } = useViewportRendering(viewportId, {
location,
});
// Memoize the customization to prevent recomputation
const colorbarCustomization = useMemo(() => {
return customizationService.getCustomization(
'cornerstone.colorbar'
) as unknown as ColorbarCustomization;
}, [customizationService]);
// Memoize tick position
const tickPosition = useMemo(() => {
const defaultTickPosition = colorbarCustomization?.colorbarTickPosition;
return colorbarCustomization?.colorbarTickPosition || defaultTickPosition;
}, [colorbarCustomization]);
// Initial load of colorbars
useEffect(() => {
setColorbars(colorbarService.getViewportColorbar(viewportId) || []);
}, [viewportId, colorbarService]);
// Subscribe to colorbar state changes
useEffect(() => {
const { unsubscribe } = colorbarService.subscribe(
colorbarService.EVENTS.STATE_CHANGED,
(event: { viewportId: string; displaySetInstanceUID?: string; changeType: string }) => {
if (event.viewportId === viewportId) {
setColorbars(colorbarService.getViewportColorbar(viewportId) || []);
}
}
);
return () => {
unsubscribe();
};
}, [viewportId, colorbarService]);
if (!colorbars.length) {
return null;
}
const isBottom = position === 'bottom';
const isSingleViewport = viewportDisplaySets.length === 1;
const showFullList = isSingleViewport || !isBottom;
const colorbarsToUse = showFullList
? colorbars
: colorbars.filter(({ displaySetInstanceUID }) => {
const { displaySetInstanceUID: dsUID } =
displaySetService.getDisplaySetByUID(displaySetInstanceUID) ?? {};
const targetUID =
opacity === 0 || opacity == null
? backgroundDisplaySet?.displaySetInstanceUID
: foregroundDisplaySets[0].displaySetInstanceUID;
return dsUID === targetUID;
});
return (
<div
style={{
pointerEvents: 'auto',
}}
>
<div
className="flex h-full flex-col items-center justify-center"
style={{ pointerEvents: 'auto' }}
>
{colorbarsToUse.map(colorbarInfo => {
const { colorbar, displaySetInstanceUID } = colorbarInfo;
return (
<ViewportColorbar
key={`colorbar-${viewportId}-${displaySetInstanceUID}`}
viewportId={viewportId}
displaySetInstanceUID={displaySetInstanceUID}
colormaps={colorbar.colormaps}
activeColormapName={colorbar.activeColormapName}
volumeId={colorbar.volumeId}
position={position}
tickPosition={tickPosition}
tickStyles={colorbarCustomization?.tickStyles}
numColorbars={colorbars.length}
/>
);
})}
</div>
</div>
);
});
export default ViewportColorbarsContainer;
@@ -1,5 +0,0 @@
import ViewportColorbar from './ViewportColorbar';
import ViewportColorbarsContainer from './ViewportColorbarsContainer';
export { ViewportColorbar, ViewportColorbarsContainer };
export default ViewportColorbarsContainer;
@@ -1,486 +0,0 @@
import React, { useState } from 'react';
import {
Button,
Icons,
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Switch,
} from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import SelectItemWithModality from '../SelectItemWithModality';
import { useViewportRendering } from '../../hooks';
function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: string }>) {
const { commandsManager, servicesManager } = useSystem();
const [pendingForegrounds, setPendingForegrounds] = useState<string[]>([]);
const [pendingSegmentations, setPendingSegmentations] = useState<string[]>([]);
const { toggleColorbar } = useViewportRendering(viewportId);
const { hangingProtocolService, toolbarService } = servicesManager.services;
const {
backgroundDisplaySet,
potentialOverlayDisplaySets,
potentialForegroundDisplaySets,
potentialBackgroundDisplaySets,
overlayDisplaySets,
foregroundDisplaySets,
} = useViewportDisplaySets(viewportId);
const [thresholdOpacityEnabled, setThresholdOpacityEnabled] = useState(false);
/**
* Change the background display set
*/
const handleBackgroundSelection = (newBackgroundDisplaySet: AppTypes.DisplaySet) => {
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportId,
newBackgroundDisplaySet.displaySetInstanceUID
);
commandsManager.run('setDisplaySetsForViewports', {
viewportsToUpdate: updatedViewports,
});
};
/**
* Replace a display set layer with a new one
*/
const handleReplaceDisplaySetLayer = (
currentDisplaySetInstanceUID: string,
newDisplaySetInstanceUID: string
) => {
// Remove current display set
commandsManager.runCommand('removeDisplaySetLayer', {
viewportId,
displaySetInstanceUID: currentDisplaySetInstanceUID,
});
setTimeout(() => {
commandsManager.runCommand('addDisplaySetAsLayer', {
viewportId,
displaySetInstanceUID: newDisplaySetInstanceUID,
});
}, 0);
};
/**
* Remove a display set layer
*/
const handleRemoveDisplaySetLayer = (displaySetInstanceUID: string) => {
commandsManager.runCommand('removeDisplaySetLayer', {
viewportId,
displaySetInstanceUID,
});
};
/**
* Add a display set as a layer
*/
const handleAddDisplaySetAsLayer = (displaySetInstanceUID: string) => {
commandsManager.runCommand('addDisplaySetAsLayer', {
viewportId,
displaySetInstanceUID,
});
};
/**
* Handle overlay display set selection change
*/
const handleOverlaySelectionChange = (
currentDisplaySet: AppTypes.DisplaySet,
newDisplaySetInstanceUID: string
) => {
if (newDisplaySetInstanceUID === currentDisplaySet.displaySetInstanceUID) {
return;
}
// Find the selected display set
const selectedDisplaySet = potentialOverlayDisplaySets.find(
ds => ds.displaySetInstanceUID === newDisplaySetInstanceUID
);
if (selectedDisplaySet) {
handleReplaceDisplaySetLayer(
currentDisplaySet.displaySetInstanceUID,
selectedDisplaySet.displaySetInstanceUID
);
}
};
/**
* Handle foreground display set selection change
*/
const handleForegroundSelectionChange = (
currentDisplaySet: AppTypes.DisplaySet,
newDisplaySetInstanceUID: string
) => {
if (newDisplaySetInstanceUID === currentDisplaySet.displaySetInstanceUID) {
return;
}
// Find the selected display set
const selectedDisplaySet = potentialForegroundDisplaySets.find(
ds => ds.displaySetInstanceUID === newDisplaySetInstanceUID
);
if (selectedDisplaySet) {
handleReplaceDisplaySetLayer(
currentDisplaySet.displaySetInstanceUID,
selectedDisplaySet.displaySetInstanceUID
);
}
};
/**
* Handle pending segmentation selection
*/
const handlePendingSegmentationSelection = (pendingId: string, displaySetInstanceUID: string) => {
const selectedDisplaySet = potentialOverlayDisplaySets.find(
ds => ds.displaySetInstanceUID === displaySetInstanceUID
);
if (selectedDisplaySet) {
handleAddDisplaySetAsLayer(selectedDisplaySet.displaySetInstanceUID);
// Remove this pending segmentation from the list
setPendingSegmentations(pendingSegmentations.filter(id => id !== pendingId));
}
};
/**
* Handle pending foreground selection
*/
const handlePendingForegroundSelection = (pendingId: string, displaySetInstanceUID: string) => {
const selectedDisplaySet = potentialForegroundDisplaySets.find(
ds => ds.displaySetInstanceUID === displaySetInstanceUID
);
if (selectedDisplaySet) {
handleAddDisplaySetAsLayer(selectedDisplaySet.displaySetInstanceUID);
// Remove this pending foreground from the list
setPendingForegrounds(pendingForegrounds.filter(id => id !== pendingId));
}
};
// Check if the advanced window level components exist in toolbar
const hasAdvancedRenderingControls = !!toolbarService.getButton('advancedRenderingControls');
const hasOpacityMenu = !!toolbarService.getButton('opacityMenu');
const handleThresholdOpacityToggle = () => {
const newValue = !thresholdOpacityEnabled;
if (hasAdvancedRenderingControls) {
toggleColorbar();
}
setThresholdOpacityEnabled(newValue);
};
return (
<div className="bg-popover flex h-full w-[275px] flex-col rounded rounded-md p-1.5">
{/* Top buttons row */}
<div className={`flex`}>
<Button
variant="ghost"
className="text-primary flex items-center p-1"
onClick={() => {
// Add a new pending foreground slot with a unique ID
setPendingForegrounds([...pendingForegrounds, `pending-${Date.now()}`]);
}}
disabled={potentialForegroundDisplaySets.length === 0}
>
<Icons.Plus className="h-4 w-4" />
Foreground
</Button>
<Button
variant="ghost"
className="text-primary ml-2 flex items-center"
disabled={potentialOverlayDisplaySets.length === 0}
onClick={() => {
setPendingSegmentations([...pendingSegmentations, `seg-${Date.now()}`]);
}}
>
<Icons.Plus className="h-4 w-4" />
Segmentation
</Button>
</div>
<div className="">
{/* Overlays Segmentation section */}
<div className="my-2 ml-1">
{overlayDisplaySets.map((displaySet, index) => (
<div
key={displaySet.displaySetInstanceUID}
className="mb-1 flex items-center"
>
<Icons.LayerSegmentation className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value={displaySet.displaySetInstanceUID}
onValueChange={value => handleOverlaySelectionChange(displaySet, value)}
>
<SelectTrigger className="flex-1">
<SelectValue>{displaySet.label?.toUpperCase()}</SelectValue>
</SelectTrigger>
<SelectContent>
{/* Include both potential overlays and the current overlay */}
<SelectItem
key={displaySet.displaySetInstanceUID}
value={displaySet.displaySetInstanceUID}
className="pr-2"
>
<SelectItemWithModality displaySet={displaySet} />
</SelectItem>
{potentialOverlayDisplaySets.map(item => (
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
className="pr-2"
>
<SelectItemWithModality displaySet={item} />
</SelectItem>
))}
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-2 flex-shrink-0"
>
<Icons.More className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => handleRemoveDisplaySetLayer(displaySet.displaySetInstanceUID)}
>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
{pendingSegmentations.map(pendingId => (
<div
key={pendingId}
className="mb-2 flex items-center"
>
<Icons.LayerSegmentation className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value=""
onValueChange={value => handlePendingSegmentationSelection(pendingId, value)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="SELECT A SEGMENTATION" />
</SelectTrigger>
<SelectContent>
{potentialOverlayDisplaySets.map(item => (
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
className="pr-2"
>
<SelectItemWithModality displaySet={item} />
</SelectItem>
))}
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-2 flex-shrink-0"
>
<Icons.More className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => {
// Remove this pending segmentation
setPendingSegmentations(pendingSegmentations.filter(id => id !== pendingId));
}}
>
Cancel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</div>
{/* Foregrounds section */}
<div className="my-2 px-1">
{foregroundDisplaySets.map((displaySet, index) => (
<div
key={displaySet.displaySetInstanceUID}
className="mb-1 flex items-center"
>
<Icons.LayerForeground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value={displaySet.displaySetInstanceUID}
onValueChange={value => handleForegroundSelectionChange(displaySet, value)}
>
<SelectTrigger className="flex-1">
<SelectValue>{displaySet.label?.toUpperCase()}</SelectValue>
</SelectTrigger>
<SelectContent>
{/* Include both potential foregrounds and the current foreground */}
<SelectItem
key={displaySet.displaySetInstanceUID}
value={displaySet.displaySetInstanceUID}
className="pr-2"
>
<SelectItemWithModality displaySet={displaySet} />
</SelectItem>
{potentialForegroundDisplaySets.map(item => (
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
className="pr-2"
>
<SelectItemWithModality displaySet={item} />
</SelectItem>
))}
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-2 flex-shrink-0"
>
<Icons.More className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => handleRemoveDisplaySetLayer(displaySet.displaySetInstanceUID)}
>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
{pendingForegrounds.map(pendingId => (
<div
key={pendingId}
className="mb-2 flex items-center"
>
<Icons.LayerForeground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value=""
onValueChange={value => handlePendingForegroundSelection(pendingId, value)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="SELECT A FOREGROUND" />
</SelectTrigger>
<SelectContent>
{potentialForegroundDisplaySets.map(item => (
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
className="pr-2"
>
<SelectItemWithModality displaySet={item} />
</SelectItem>
))}
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-2 flex-shrink-0"
>
<Icons.More className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => {
// Remove this pending foreground
setPendingForegrounds(pendingForegrounds.filter(id => id !== pendingId));
}}
>
Cancel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</div>
{/* Background section */}
<div className="mt-1 mb-1 flex items-center px-1">
<Icons.LayerBackground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value={backgroundDisplaySet?.displaySetInstanceUID}
onValueChange={value => {
const selectedDisplaySet = potentialBackgroundDisplaySets.find(
ds => ds.displaySetInstanceUID === value
);
if (selectedDisplaySet) {
handleBackgroundSelection(selectedDisplaySet);
}
}}
>
<SelectTrigger className="flex-1">
<SelectValue>
{(
backgroundDisplaySet?.SeriesDescription ||
backgroundDisplaySet?.label ||
'background'
).toUpperCase()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{potentialBackgroundDisplaySets.map(displaySet => (
<SelectItem
key={displaySet.displaySetInstanceUID}
value={displaySet.displaySetInstanceUID}
className="pr-2"
>
<SelectItemWithModality displaySet={displaySet} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{foregroundDisplaySets.length > 0 && (hasAdvancedRenderingControls || hasOpacityMenu) && (
<div className="mt-1 ml-7">
<div className="flex items-center">
<Switch
id="threshold-opacity-switch"
className="mr-2"
checked={thresholdOpacityEnabled}
onCheckedChange={handleThresholdOpacityToggle}
/>
<label
htmlFor="threshold-opacity-switch"
className="text-muted-foreground cursor-pointer text-sm"
onClick={() => setThresholdOpacityEnabled(!thresholdOpacityEnabled)}
>
Control threshold & opacity
</label>
</div>
</div>
)}
</div>
);
}
export default ViewportDataOverlayMenu;
@@ -1,88 +0,0 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useIconPresentation,
} from '@ohif/ui-next';
import ViewportDataOverlayMenu from './ViewportDataOverlayMenu';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
type DataOverlayMenuProps = {
viewportId: string;
location: string;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
};
export function ViewportDataOverlayMenuWrapper(props: DataOverlayMenuProps): ReactNode {
const { viewportId, location, isOpen = false, onOpen, onClose, disabled, ...rest } = props;
const { viewportDisplaySets: displaySets } = useViewportDisplaySets(viewportId);
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const handleOpenChange = (openState: boolean) => {
if (openState) {
onOpen?.();
} else {
onClose?.();
}
};
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
const { align, side } = toolbarService.getAlignAndSide(location);
const Icon = <Icons.ViewportViews className={iconClassName} />;
return (
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="ViewportViews"
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<ViewportDataOverlayMenu
className="w-full"
viewportId={viewportId}
displaySets={displaySets}
/>
</PopoverContent>
</Popover>
);
}
@@ -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 (
<div className="bg-muted flex h-full w-[262px] flex-col rounded p-3">
<span className="text-muted-foreground mb-2 text-xs font-semibold">Current Viewport</span>
<ul className="space-y-1">
{activeSegmentations.map(segmentation => (
<li
key={segmentation.id}
className="flex items-center text-sm"
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground mr-2"
onClick={() => removeSegmentationFromViewport(segmentation.segmentationId)}
>
<Icons.Minus className="h-6 w-6" />
</Button>
<span className="text-foreground flex-grow">{segmentation.label}</span>
{segmentation.visible ? (
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={() =>
toggleSegmentationRepresentationVisibility(
segmentation.segmentationId,
segmentation.type
)
}
>
<Icons.Hide className="h-6 w-6" />
</Button>
) : (
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={() =>
toggleSegmentationRepresentationVisibility(
segmentation.segmentationId,
segmentation.type
)
}
>
<Icons.Show className="h-6 w-6" />
</Button>
)}
</li>
))}
</ul>
{availableSegmentations.length > 0 && (
<>
<Separator className="bg-input mb-3" />
<span className="text-muted-foreground mb-2 text-xs font-semibold">Available</span>
<ul className="space-y-1">
{availableSegmentations.map(({ segmentationId, label }) => (
<li
key={segmentationId}
className="flex items-center text-sm"
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground mr-2"
onClick={() => addSegmentationToViewport(segmentationId)}
>
<Icons.Plus className="h-6 w-6" />
</Button>
<span className="text-foreground/60">{label}</span>
</li>
))}
</ul>
</>
)}
</div>
);
}
export default ViewportSegmentationMenu;
@@ -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 (
<Popover>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<Button
variant="ghost"
size="icon"
>
<Icons.ViewportViews
className={classNames(
'text-highlight',
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
)}
/>
</Button>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={-15}
sideOffset={5}
>
<ViewportSegmentationMenu
className="w-full"
viewportId={viewportId}
displaySets={displaySets}
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
</PopoverContent>
</Popover>
);
}
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' };
}
};
@@ -0,0 +1,11 @@
import React, { ReactNode } from 'react';
import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper';
export function getViewportDataOverlaySettingsMenu(
props: withAppTypes<{
viewportId: string;
element: HTMLElement;
}>
): ReactNode {
return <ViewportSegmentationMenuWrapper {...props} />;
}
@@ -1,164 +0,0 @@
import { utilities as csUtils } from '@cornerstonejs/core';
export const DEFAULT_COLORMAP = 'hsv';
export const DEFAULT_OPACITY = 0.5;
export const DEFAULT_OPACITY_PERCENT = DEFAULT_OPACITY * 100;
export const DERIVED_OVERLAY_MODALITIES = ['SEG', 'RTSTRUCT'];
/**
* Get modality-specific color and opacity settings from the customization service
*/
export function getModalityOverlayColormap(customizationService, modality) {
const modalityOverlayDefaultColorMaps = customizationService?.getCustomization(
'cornerstone.modalityOverlayDefaultColorMaps'
) || { defaultSettings: {} };
return (
modalityOverlayDefaultColorMaps.defaultSettings[modality] || {
colormap: DEFAULT_COLORMAP,
opacity: DEFAULT_OPACITY,
}
);
}
/**
* Identifies display sets that can be used as overlays for a specific viewport.
*
* "Enhanced" display sets are those that:
* 1. Are not already in the viewport
* 2. Are evaluated for their ability to be overlaid onto the background display set
* 3. Have an "isOverlayable" flag indicating if they're compatible with the viewport
*
* A display set is considered overlayable when:
* - The background display set is reconstructable
* - The display set is not unsupported
* - The Frame of Reference matches the background display set
* - For non-derived modalities: background can be a volume and display set is either multiframe or valid volume
*
* @returns {Object} Object containing:
* - viewportDisplaySets: Display sets already in the viewport
* - enhancedDisplaySets: Other display sets with overlayability assessment
*/
export function getEnhancedDisplaySets({ viewportId, services }) {
const { displaySetService, viewportGridService } = services;
const displaySetsUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetsUIDs?.length) {
return {
viewportDisplaySets: [],
enhancedDisplaySets: [],
};
}
const allDisplaySets = displaySetService.getActiveDisplaySets();
const otherDisplaySets = allDisplaySets.filter(
displaySet => !displaySetsUIDs.includes(displaySet.displaySetInstanceUID)
);
const viewportDisplaySets = displaySetsUIDs.map(displaySetUID =>
displaySetService.getDisplaySetByUID(displaySetUID)
);
const backgroundCanBeVolume = csUtils.isValidVolume(viewportDisplaySets[0].imageIds || []);
const backgroundDisplaySet = viewportDisplaySets[0];
const enhancedDisplaySets = otherDisplaySets.map(displaySet => {
if (!backgroundDisplaySet.isReconstructable) {
return {
...displaySet,
isOverlayable: false,
};
}
if (displaySet.unsupported) {
return {
...displaySet,
isOverlayable: false,
};
}
// Check if Frame of Reference matches
if (
displaySet.FrameOfReferenceUID &&
displaySet.FrameOfReferenceUID !== backgroundDisplaySet.FrameOfReferenceUID
) {
return {
...displaySet,
isOverlayable: false,
};
}
// Special handling for derived modalities
if (!DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality)) {
if (!backgroundCanBeVolume) {
return {
...displaySet,
isOverlayable: false,
};
}
const imageIds = displaySet.imageIds || displaySet.images?.map(image => image.imageId);
const isMultiframe = displaySet.isMultiFrame;
if (!isMultiframe && imageIds?.length > 0 && !csUtils.isValidVolume(imageIds)) {
return {
...displaySet,
isOverlayable: false,
};
}
}
return {
...displaySet,
isOverlayable: true,
};
});
return {
viewportDisplaySets,
enhancedDisplaySets,
};
}
/**
* Sort function: puts disabled items (isOverlayable: false) at the end
*/
export const sortByOverlayable = (a, b) => {
if (a.isOverlayable === b.isOverlayable) {
return 0;
}
return a.isOverlayable ? -1 : 1;
};
/**
* Create display set options based on modality and opacity settings
*/
export function createColormapOverlayDisplaySetOptions(displaySet, opacity, customizationService) {
if (displaySet.Modality === 'SEG') {
return {};
}
const modalitySettings = getModalityOverlayColormap(customizationService, displaySet.Modality);
return {
colormap: {
name: modalitySettings.colormap || DEFAULT_COLORMAP,
opacity: opacity / 100, // Convert from percentage to 0-1 range
},
};
}
/**
* Get segmentations that can be added as overlays to the viewport
*
* Note: This function is deprecated as we now use display sets for segmentations
*/
export function getAvailableSegmentations(segmentationService) {
const segmentations = segmentationService.getSegmentations() || [];
return segmentations.map(segmentation => ({
segmentationId: segmentation.segmentationId,
label: segmentation.label || 'Segmentation',
segments: segmentation.segments,
frameOfReferenceUID: segmentation.frameOfReferenceUID,
}));
}
@@ -1,181 +0,0 @@
import React from 'react';
import { cn, Icons, ToolButton, useIconPresentation } from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
import { Enums } from '@cornerstonejs/core';
import { Popover, PopoverTrigger, PopoverContent, Button, useViewportGrid } from '@ohif/ui-next';
function ViewportOrientationMenu({
location,
viewportId,
displaySets,
isOpen = false,
onOpen,
onClose,
disabled,
...props
}: withAppTypes<{
location?: string;
viewportId: string;
displaySets: AppTypes.DisplaySet[];
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
}>) {
const [gridState] = useViewportGrid();
const viewportIdToUse = viewportId || gridState.activeViewportId;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const { servicesManager, commandsManager } = useSystem();
const { cornerstoneViewportService, toolbarService } = servicesManager.services;
const handleOrientationChange = (orientation: string) => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportIdToUse);
const currentViewportType = viewportInfo?.getViewportType();
if (!displaySets.length) {
return;
}
// Check if at least one displaySet is reconstructable
const hasReconstructableDisplaySet = displaySets.some(ds => ds.isReconstructable);
if (!hasReconstructableDisplaySet) {
console.warn('Cannot change orientation: No reconstructable display sets in viewport');
return;
}
// Set the orientation enum based on the selected orientation
let orientationEnum;
switch (orientation.toLowerCase()) {
case 'axial':
orientationEnum = Enums.OrientationAxis.AXIAL;
break;
case 'sagittal':
orientationEnum = Enums.OrientationAxis.SAGITTAL;
break;
case 'coronal':
orientationEnum = Enums.OrientationAxis.CORONAL;
break;
default:
orientationEnum = Enums.OrientationAxis.ACQUISITION;
}
const displaySetUIDs = displaySets.map(ds => ds.displaySetInstanceUID);
// If viewport is not already a volume type, we need to convert it
if (currentViewportType !== Enums.ViewportType.ORTHOGRAPHIC) {
// Configure the viewport to be a volume viewport with current display sets
const updatedViewport = {
viewportId: viewportIdToUse,
displaySetInstanceUIDs: displaySetUIDs,
viewportOptions: {
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
orientation: orientationEnum,
},
displaySetOptions: displaySetUIDs.map(() => ({})),
};
// Update the viewport to be a volume viewport
commandsManager.run('setDisplaySetsForViewports', {
viewportsToUpdate: [updatedViewport],
});
} else {
// Set the viewport orientation
commandsManager.runCommand('setViewportOrientation', {
viewportId: viewportIdToUse,
orientation: orientationEnum,
});
}
// Close the menu after selection
onClose?.();
};
const handleOpenChange = (openState: boolean) => {
if (openState) {
onOpen?.();
} else {
onClose?.();
}
};
if (!displaySets.length) {
return null;
}
// Get proper alignment and side based on the location using toolbar service
const { align, side } = toolbarService.getAlignAndSide(Number(location));
const Icon = <Icons.OrientationSwitch className={iconClassName} />;
return (
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className={cn('flex items-center justify-center')}
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="OrientationSwitch"
{...props}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
onClick={() => {}}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="w-[100px] p-1"
align={align}
side={side}
>
<div className="flex flex-col">
<Button
variant="ghost"
className="justify-start"
onClick={() => handleOrientationChange('axial')}
>
Axial
</Button>
<Button
variant="ghost"
className="justify-start"
onClick={() => handleOrientationChange('sagittal')}
>
Sagittal
</Button>
<Button
variant="ghost"
className="justify-start"
onClick={() => handleOrientationChange('coronal')}
>
Coronal
</Button>
<Button
variant="ghost"
className="justify-start"
onClick={() => handleOrientationChange('acquisition')}
>
Acquisition
</Button>
</div>
</PopoverContent>
</Popover>
);
}
export default ViewportOrientationMenu;
@@ -1,29 +0,0 @@
import React, { ReactNode } from 'react';
import ViewportOrientationMenu from './ViewportOrientationMenu';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
export function ViewportOrientationMenuWrapper(
props: withAppTypes<{
viewportId: string;
location: string;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
iconSize?: number;
disabled?: boolean;
}>
): ReactNode {
const { viewportId } = props;
const { viewportDisplaySets } = useViewportDisplaySets(viewportId);
if (!viewportDisplaySets.length) {
return null;
}
return (
<ViewportOrientationMenu
{...props}
displaySets={viewportDisplaySets}
/>
);
}
@@ -2,7 +2,7 @@ import React, { useEffect, useCallback, useState, ReactElement, useMemo } from '
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import { PanelSection, WindowLevel } from '@ohif/ui-next';
import { BaseVolumeViewport, Enums, eventTarget } from '@cornerstonejs/core';
import { Enums, eventTarget } from '@cornerstonejs/core';
import { useActiveViewportDisplaySets } from '@ohif/core';
import {
getNodeOpacity,
@@ -22,7 +22,7 @@ const ViewportWindowLevel = ({
const { cornerstoneViewportService } = servicesManager.services;
const [windowLevels, setWindowLevels] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const displaySets = useActiveViewportDisplaySets();
const displaySets = useActiveViewportDisplaySets({ servicesManager });
const getViewportsWithVolumeIds = useCallback(
(volumeIds: string[]) => {
@@ -30,7 +30,7 @@ const ViewportWindowLevel = ({
const viewports = renderingEngine.getVolumeViewports();
return viewports.filter(vp => {
const viewportVolumeIds = vp instanceof BaseVolumeViewport ? vp.getAllVolumeIds() : [];
const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId);
return (
volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
@@ -124,8 +124,7 @@ const ViewportWindowLevel = ({
return;
}
const viewportVolumeIds =
viewport instanceof BaseVolumeViewport ? viewport.getAllVolumeIds() : [];
const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId);
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => {
@@ -1,27 +1,122 @@
import React, { ReactElement, useCallback } from 'react';
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { Switch } from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import { StackViewport, VolumeViewport } from '@cornerstonejs/core';
import { ColorbarProps } from '../../types/Colorbar';
import { utilities } from '@cornerstonejs/core';
export function Colorbar({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
export function setViewportColorbar(
viewportId,
displaySets,
commandsManager,
servicesManager: AppTypes.ServicesManager,
colorbarOptions
) {
const { cornerstoneViewportService } = servicesManager.services;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const handleToggle = useCallback(() => {
toggleColorbar();
}, [toggleColorbar]);
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: '13px Inter',
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<ColorbarProps>): 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 (
<div className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded">
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
onClick={e => {
e.stopPropagation();
onSetColorbar();
}}
>
<div className="flex w-7 flex-shrink-0 items-center justify-center"></div>
<span
className="flex-grow"
onClick={handleToggle}
>
Display Color bar
</span>
<span className="flex-grow">Display Color bar</span>
<Switch
className="ml-2 flex-shrink-0"
checked={!!hasColorbar}
onCheckedChange={handleToggle}
checked={showColorbar}
onClick={e => {
e.stopPropagation();
onSetColorbar();
}}
/>
</div>
);
@@ -1,66 +1,95 @@
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
export function Colormap({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { viewportDisplaySets } = useViewportRendering(viewportId);
import { StackViewport, Types } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap';
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
viewportDisplaySets?.[0]?.displaySetInstanceUID
);
export function Colormap({
colormaps,
viewportId,
displaySets,
commandsManager,
servicesManager,
}: ColormapProps): ReactElement {
const { cornerstoneViewportService } = servicesManager.services;
// Use the hook with the active display set
const { colorbarProperties, setColormap } = useViewportRendering(viewportId, {
displaySetInstanceUID: activeDisplaySetUID,
});
const { colormaps } = colorbarProperties;
const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]);
const [showPreview, setShowPreview] = useState(false);
const [prePreviewColormap, setPrePreviewColormap] = useState(null);
const [currentColormap, setCurrentColormap] = useState(null);
const showPreviewRef = useRef(showPreview);
showPreviewRef.current = showPreview;
const prePreviewColormapRef = useRef(prePreviewColormap);
prePreviewColormapRef.current = prePreviewColormap;
const currentColormapRef = useRef(currentColormap);
currentColormapRef.current = currentColormap;
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 activeIndex = displaySets.findIndex(
ds => ds.displaySetInstanceUID === activeDisplaySetRef.current.displaySetInstanceUID
);
useEffect(() => {
setCurrentColormap(null);
setPrePreviewColormap(null);
}, [activeDisplaySetUID]);
const handleSetColorLUT = (colormap, immediate = true) => {
// Check if it's a fusion viewport
const oneOpacityColormaps = ['Grayscale', 'X Ray'];
const opacity =
viewportDisplaySets.length > 1 && !oneOpacityColormaps.includes(colormap.name) ? 0.5 : 1;
setColormap({
colormap,
opacity,
immediate,
});
};
setActiveDisplaySet(displaySets[displaySets.length - 1]);
}, [displaySets]);
return (
<>
{viewportDisplaySets && viewportDisplaySets.length > 1 && (
{displaySets.length > 1 && (
<div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
<Tabs
value={activeDisplaySetUID}
onValueChange={displaySetUID => {
setActiveDisplaySetUID(displaySetUID);
value={String(activeIndex)}
onValueChange={val => {
const index = parseInt(val, 10);
setActiveDisplaySet(displaySets[index]);
setPrePreviewColormap(null);
}}
>
<TabsList>
{viewportDisplaySets.map(ds => (
{displaySets.map((ds, i) => (
<TabsTrigger
key={ds.displaySetInstanceUID}
value={ds.displaySetInstanceUID}
key={i}
value={String(i)}
>
{ds.Modality}
</TabsTrigger>
@@ -69,7 +98,6 @@ export function Colormap({ viewportId }: { viewportId?: string } = {}): ReactEle
</Tabs>
</div>
)}
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
onClick={() => setShowPreview(!showPreview)}
@@ -80,47 +108,49 @@ export function Colormap({ viewportId }: { viewportId?: string } = {}): ReactEle
checked={showPreview}
onCheckedChange={checked => {
setShowPreview(checked);
if (!checked && currentColormapRef.current) {
handleSetColorLUT(currentColormapRef.current);
}
}}
/>
</div>
<AllInOneMenu.DividerItem />
<div className="h-[300px] flex-grow">
<ScrollArea className="h-full w-full">
<div className="p-1">
{colormaps.map((colormap, index) => (
<AllInOneMenu.Item
key={index}
label={colormap.description}
useIconSpace={false}
onClick={() => {
setCurrentColormap(colormap);
handleSetColorLUT(colormap);
setPrePreviewColormap(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
if (!prePreviewColormapRef.current) {
setPrePreviewColormap(colormap);
}
handleSetColorLUT(colormap);
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewColormapRef.current) {
handleSetColorLUT(prePreviewColormapRef.current);
}
}}
/>
))}
</div>
</ScrollArea>
</div>
<AllInOneMenu.ItemPanel
maxHeight="calc(100vh - 250px)"
className="min-h-[200px] flex-grow"
>
{colormaps.map((colormap, index) => (
<AllInOneMenu.Item
key={index}
label={colormap.description}
useIconSpace={false}
onClick={() => {
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,
});
}
}}
></AllInOneMenu.Item>
))}
</AllInOneMenu.ItemPanel>
</>
);
}
@@ -1,10 +1,13 @@
import React, { ReactElement, useState, useEffect, useCallback } from 'react';
import { VolumeLightingProps } from '../../types/ViewportPresets';
import { Numeric } from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
export function VolumeLighting({ viewportId, hasShade }: VolumeLightingProps): ReactElement {
const { servicesManager, commandsManager } = useSystem();
export function VolumeLighting({
servicesManager,
commandsManager,
viewportId,
hasShade,
}: VolumeLightingProps): ReactElement {
const { cornerstoneViewportService } = servicesManager.services;
const [lightingValues, setLightingValues] = useState({
ambient: null,
@@ -1,33 +1,47 @@
import React, { ReactElement, useState } from 'react';
import { AllInOneMenu } from '@ohif/ui-next';
import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets';
import { VolumeRenderingQuality } from './VolumeRenderingQuality';
import { VolumeShift } from './VolumeShift';
import { VolumeLighting } from './VolumeLighting';
import { VolumeShade } from './VolumeShade';
import { useViewportRendering } from '../../hooks/useViewportRendering';
export function VolumeRenderingOptions({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { volumeRenderingQualityRange } = useViewportRendering(viewportId);
export function VolumeRenderingOptions({
viewportId,
commandsManager,
volumeRenderingQualityRange,
servicesManager,
}: VolumeRenderingOptionsProps): ReactElement {
const [hasShade, setShade] = useState(false);
return (
<AllInOneMenu.ItemPanel>
<VolumeRenderingQuality
viewportId={viewportId}
commandsManager={commandsManager}
servicesManager={servicesManager}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
<VolumeShift viewportId={viewportId} />
<VolumeShift
viewportId={viewportId}
commandsManager={commandsManager}
servicesManager={servicesManager}
/>
<div className="mt-2 flex h-8 !h-[20px] w-full flex-shrink-0 items-center justify-start px-2 text-base">
<div className="text-muted-foreground text-sm">Lighting</div>
</div>
<div className="bg-background mt-1 mb-1 h-px w-full"></div>
<div className="hover:bg-accent flex h-8 w-full flex-shrink-0 items-center px-2 text-base hover:rounded">
<VolumeShade
commandsManager={commandsManager}
servicesManager={servicesManager}
viewportId={viewportId}
onClickShade={setShade}
/>
</div>
<VolumeLighting
viewportId={viewportId}
commandsManager={commandsManager}
servicesManager={servicesManager}
hasShade={hasShade}
/>
</AllInOneMenu.ItemPanel>
@@ -1,13 +1,15 @@
import { AllInOneMenu } from '@ohif/ui-next';
import { Icons } from '@ohif/ui-next';
import React, { ReactElement } from 'react';
import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets';
import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent';
import { useSystem } from '@ohif/core';
import { useViewportRendering } from '../../hooks/useViewportRendering';
export function VolumeRenderingPresets({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { volumeRenderingPresets } = useViewportRendering(viewportId);
const { servicesManager } = useSystem();
export function VolumeRenderingPresets({
viewportId,
servicesManager,
commandsManager,
volumeRenderingPresets,
}: VolumeRenderingPresetsProps): ReactElement {
const { uiDialogService } = servicesManager.services;
const onClickPresets = () => {
@@ -19,6 +21,7 @@ export function VolumeRenderingPresets({ viewportId }: { viewportId?: string } =
contentProps: {
presets: volumeRenderingPresets,
viewportId,
commandsManager,
},
});
};
@@ -1,13 +1,13 @@
import React, { ReactElement, useCallback, useState, useEffect } from 'react';
import { VolumeRenderingQualityProps } from '../../types/ViewportPresets';
import { Numeric } from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
export function VolumeRenderingQuality({
volumeRenderingQualityRange,
commandsManager,
servicesManager,
viewportId,
}: VolumeRenderingQualityProps): ReactElement {
const { servicesManager, commandsManager } = useSystem();
const { cornerstoneViewportService } = servicesManager.services;
const { min, max, step } = volumeRenderingQualityRange;
const [quality, setQuality] = useState(null);
@@ -1,13 +1,13 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { Switch } from '@ohif/ui-next';
import { VolumeShadeProps } from '../../types/ViewportPresets';
import { useSystem } from '@ohif/core';
export function VolumeShade({
commandsManager,
viewportId,
servicesManager,
onClickShade = bool => {},
}: VolumeShadeProps): ReactElement {
const { servicesManager, commandsManager } = useSystem();
const { cornerstoneViewportService } = servicesManager.services;
const [shade, setShade] = useState(true);
const [key, setKey] = useState(0);
@@ -1,10 +1,12 @@
import React, { ReactElement, useCallback, useEffect, useState, useRef } from 'react';
import { VolumeShiftProps } from '../../types/ViewportPresets';
import { Numeric } from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
export function VolumeShift({ viewportId }: VolumeShiftProps): ReactElement {
const { servicesManager, commandsManager } = useSystem();
export function VolumeShift({
viewportId,
commandsManager,
servicesManager,
}: VolumeShiftProps): ReactElement {
const { cornerstoneViewportService } = servicesManager.services;
const [minShift, setMinShift] = useState<number | null>(null);
const [maxShift, setMaxShift] = useState<number | null>(null);
@@ -1,122 +1,58 @@
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import React, { ReactElement, useCallback } from 'react';
import { AllInOneMenu } from '@ohif/ui-next';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { CommandsManager } from '@ohif/core';
import { useTranslation } from 'react-i18next';
export function WindowLevel({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { viewportDisplaySets } = useViewportRendering(viewportId);
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
viewportDisplaySets?.[0]?.displaySetInstanceUID
export type WindowLevelProps = {
viewportId: string;
presets: Array<Record<string, Array<WindowLevelPreset>>>;
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]
);
// Use the hook with the active display set
const { windowLevelPresets, setWindowLevel } = useViewportRendering(viewportId, {
displaySetInstanceUID: activeDisplaySetUID,
});
const [showPreview, setShowPreview] = useState(false);
const [prePreviewPreset, setPrePreviewPreset] = useState<WindowLevelPreset | null>(null);
const [currentPreset, setCurrentPreset] = useState<WindowLevelPreset | null>(null);
const showPreviewRef = useRef(showPreview);
showPreviewRef.current = showPreview;
const prePreviewPresetRef = useRef(prePreviewPreset);
prePreviewPresetRef.current = prePreviewPreset;
const currentPresetRef = useRef(currentPreset);
currentPresetRef.current = currentPreset;
// Reset presets when active display set changes
useEffect(() => {
setCurrentPreset(null);
setPrePreviewPreset(null);
}, [activeDisplaySetUID]);
// Handle applying window level preset
const handleSetWindowLevel = (preset: WindowLevelPreset, immediate = false) => {
setWindowLevel({
windowWidth: Number(preset.window),
windowCenter: Number(preset.level),
immediate,
});
};
return (
<>
{viewportDisplaySets && viewportDisplaySets.length > 1 && (
<div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
<Tabs
value={activeDisplaySetUID}
onValueChange={displaySetUID => {
setActiveDisplaySetUID(displaySetUID);
}}
>
<TabsList>
{viewportDisplaySets.map(ds => (
<TabsTrigger
key={ds.displaySetInstanceUID}
value={ds.displaySetInstanceUID}
>
{ds.Modality}
</TabsTrigger>
<AllInOneMenu.ItemPanel>
{presets.map((modalityPresets, modalityIndex) => (
<React.Fragment key={modalityIndex}>
{Object.entries(modalityPresets).map(([modality, presetsArray]) => (
<React.Fragment key={modality}>
<AllInOneMenu.HeaderItem>
{t('Modality Presets', { modality })}
</AllInOneMenu.HeaderItem>
{presetsArray.map((preset, index) => (
<AllInOneMenu.Item
key={`${modality}-${index}`}
label={preset.description}
secondaryLabel={`${preset.window} / ${preset.level}`}
useIconSpace={false}
onClick={() => onSetWindowLevel(preset)}
/>
))}
</TabsList>
</Tabs>
</div>
)}
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
onClick={() => setShowPreview(!showPreview)}
>
<span className="flex-shrink-0">Preview in viewport</span>
<Switch
className="ml-auto flex-shrink-0"
checked={showPreview}
onCheckedChange={checked => {
setShowPreview(checked);
// When turning off preview, restore the current preset if one exists
if (!checked && currentPresetRef.current) {
handleSetWindowLevel(currentPresetRef.current, true);
}
}}
/>
</div>
<AllInOneMenu.DividerItem />
<div className="h-[175px] flex-grow">
<ScrollArea className="h-full w-full">
<div className="p-1">
{windowLevelPresets.map((preset, index) => (
<AllInOneMenu.Item
key={index}
label={preset.description}
secondaryLabel={`${preset.window} / ${preset.level}`}
useIconSpace={false}
onClick={() => {
setCurrentPreset(preset);
handleSetWindowLevel(preset, true);
setPrePreviewPreset(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
if (!prePreviewPresetRef.current) {
setPrePreviewPreset(currentPresetRef.current || preset);
}
handleSetWindowLevel(preset, true);
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewPresetRef.current) {
handleSetWindowLevel(prePreviewPresetRef.current, true);
}
}}
/>
))}
</div>
</ScrollArea>
</div>
</>
</React.Fragment>
))}
</React.Fragment>
))}
</AllInOneMenu.ItemPanel>
);
}
@@ -1,97 +1,196 @@
import React, { ReactElement, useMemo } from 'react';
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { AllInOneMenu } from '@ohif/ui-next';
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 { useViewportRendering } from '../../hooks/useViewportRendering';
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;
align?: 'start' | 'end' | 'center';
side?: 'top' | 'bottom' | 'left' | 'right';
element: HTMLElement;
presets: Array<Record<string, Array<WindowLevelPreset>>>;
colorbarProperties: ColorbarProperties;
displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
};
export function WindowLevelActionMenu({
viewportId,
element,
align,
side,
}: WindowLevelActionMenuProps): ReactElement {
return (
<WindowLevelActionMenuContent
viewportId={viewportId}
align={align}
side={side}
/>
);
}
export function WindowLevelActionMenuContent({
viewportId,
align,
side,
}: {
viewportId: string;
align?: string;
side?: string;
}): ReactElement {
const { t } = useTranslation('WindowLevelActionMenu');
// Use a stable key for the menu to avoid infinite re-renders
const menuKey = useMemo(() => `${viewportId}`, [viewportId]);
presets,
verticalDirection,
horizontalDirection,
commandsManager,
servicesManager,
colorbarProperties,
displaySets,
volumeRenderingPresets,
volumeRenderingQualityRange,
}: withAppTypes<WindowLevelActionMenuProps>): ReactElement {
const {
is3DVolume,
colorbarProperties,
windowLevelPresets,
volumeRenderingPresets,
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,
} = useViewportRendering(viewportId);
volumeRenderingPresets,
colorbarProperties,
activeViewportId,
viewportGrid,
]);
return (
<AllInOneMenu.Menu
<AllInOneMenu.IconMenu
icon="viewport-window-level"
verticalDirection={verticalDirection}
horizontalDirection={horizontalDirection}
iconClassName={classNames(
// Visible on hover and for the active viewport
activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible',
'flex shrink-0 cursor-pointer rounded active:text-foreground text-highlight',
isLight ? ' hover:bg-primary/30' : 'hover:bg-primary/30'
)}
menuStyle={{ maxHeight: vpHeight - 32, minWidth: 218 }}
onVisibilityChange={() => {
setVpHeight(element.clientHeight);
}}
menuKey={menuKey}
key={menuKey}
// the visibility is handled by the parent component
isVisible={true}
align={align}
side={side}
>
<AllInOneMenu.ItemPanel>
{!is3DVolume && <Colorbar viewportId={viewportId} />}
{!is3DVolume && (
<Colorbar
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager}
servicesManager={servicesManager}
colorbarProperties={colorbarProperties}
/>
)}
{colorbarProperties?.colormaps && !is3DVolume && (
{colormaps && !is3DVolume && (
<AllInOneMenu.SubMenu
key="colorLUTPresets"
itemLabel="Color LUT"
itemIcon="icon-color-lut"
className="flex h-[calc(100%-32px)] flex-col"
>
<Colormap viewportId={viewportId} />
<Colormap
className="flex h-full w-full flex-col"
colormaps={colormaps}
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager}
servicesManager={servicesManager}
/>
</AllInOneMenu.SubMenu>
)}
{windowLevelPresets?.length > 0 && !is3DVolume && (
{presets && presets.length > 0 && !is3DVolume && (
<AllInOneMenu.SubMenu
key="windowLevelPresets"
itemLabel={t('Modality Window Presets')}
itemIcon="viewport-window-level"
>
<WindowLevel viewportId={viewportId} />
<WindowLevel
viewportId={viewportId}
commandsManager={commandsManager}
presets={presets}
/>
</AllInOneMenu.SubMenu>
)}
{volumeRenderingPresets && is3DVolume && <VolumeRenderingPresets viewportId={viewportId} />}
{volumeRenderingPresets && is3DVolume && (
<VolumeRenderingPresets
servicesManager={servicesManager}
viewportId={viewportId}
commandsManager={commandsManager}
volumeRenderingPresets={volumeRenderingPresets}
/>
)}
{volumeRenderingQualityRange && is3DVolume && (
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
<VolumeRenderingOptions viewportId={viewportId} />
<VolumeRenderingOptions
viewportId={viewportId}
commandsManager={commandsManager}
volumeRenderingQualityRange={volumeRenderingQualityRange}
servicesManager={servicesManager}
/>
</AllInOneMenu.SubMenu>
)}
</AllInOneMenu.ItemPanel>
</AllInOneMenu.Menu>
</AllInOneMenu.IconMenu>
);
}
@@ -1,133 +0,0 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useViewportGrid,
useIconPresentation,
} from '@ohif/ui-next';
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import { useViewportRendering } from '../../hooks';
export function WindowLevelActionMenuWrapper(
props: withAppTypes<{
viewportId: string;
element?: HTMLElement;
location?: number;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
displaySets?: Array<AppTypes.DisplaySet>;
disabled?: boolean;
isEmbedded?: boolean;
}>
): ReactNode {
const {
viewportId,
element,
location,
isOpen = false,
onOpen,
onClose,
disabled,
isEmbedded = false,
onInteraction: onInteractionProps,
hasEmbeddedVariantToUse,
...rest
} = props;
const [gridState] = useViewportGrid();
const viewportIdToUse = viewportId || gridState.activeViewportId;
const { viewportDisplaySets: displaySets } = useViewportDisplaySets(viewportIdToUse);
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
const handleOpenChange = (openState: boolean) => {
const shouldToggleColorbar = hasColorbar && !isEmbedded;
if (isOpen && shouldToggleColorbar && openState) {
toggleColorbar();
onClose?.();
return;
}
if (!isOpen && openState && shouldToggleColorbar) {
toggleColorbar();
return;
}
if (openState) {
onOpen?.();
} else {
onClose?.();
}
};
const { align, side } = toolbarService.getAlignAndSide(location);
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
if (modalities.length === 0) {
return null;
}
const Icon =
hasColorbar && !isEmbedded && hasEmbeddedVariantToUse ? (
<Icons.Close className={iconClassName} />
) : (
<Icons.ViewportWindowLevel className={iconClassName} />
);
return (
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<WindowLevelActionMenu
viewportId={viewportIdToUse}
element={element}
align={align}
side={side}
/>
</PopoverContent>
</Popover>
);
}
@@ -0,0 +1,57 @@
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(
'viewportActionMenu.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;
}
const WindowLevelActionMenuComponent = WindowLevelActionMenu?.component;
return (
<WindowLevelActionMenuComponent
viewportId={viewportId}
element={element}
presets={displaySetPresets}
verticalDirection={verticalDirection}
horizontalDirection={horizontalDirection}
commandsManager={commandsManager}
servicesManager={servicesManager}
colorbarProperties={colorbarProperties}
displaySets={displaySets}
volumeRenderingPresets={volumeRenderingPresets}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
);
}
@@ -1,11 +0,0 @@
export * from './Colorbar';
export * from './Colormap';
export * from './WindowLevel';
export * from './WindowLevelActionMenu';
export * from './WindowLevelActionMenuWrapper';
export * from './VolumeRenderingOptions';
export * from './VolumeRenderingPresets';
export * from './VolumeRenderingQuality';
export * from './VolumeLighting';
export * from './VolumeShade';
export * from './VolumeShift';
@@ -1,4 +1,3 @@
import DicomUpload from './DicomUpload/DicomUpload';
export * from './AccordionGroup';
export * from './MeasurementTableNested';
export * from './StudyMeasurements';
@@ -6,7 +5,6 @@ export * from './MeasurementsMenu';
export * from './SeriesMeasurements';
export * from './StudyMeasurementsActions';
export * from './MeasurementsOrAdditionalFindings';
export * from './WindowLevelActionMenu';
export * from './ModalityLoadBadge';
import DicomUpload from './DicomUpload/DicomUpload';
export { DicomUpload };
@@ -1,3 +0,0 @@
// Volume loader schemes
export const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
export const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume';
@@ -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-next';
import ViewportActionCornersService, {
ActionComponentInfo,
} from '../services/ViewportActionCornersService/ViewportActionCornersService';
interface StateComponentInfo extends Types.ViewportActionCornersComponentInfo {
indexPriority: number;
}
type State = Record<string, Record<ViewportActionCornersLocations, Array<StateComponentInfo>>>;
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<ActionComponentInfo>) => {
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 (
<ViewportActionCornersContext.Provider value={contextValue}>
{children}
</ViewportActionCornersContext.Provider>
);
}
ViewportActionCornersProvider.propTypes = {
children: PropTypes.node,
service: PropTypes.instanceOf(ViewportActionCornersService).isRequired,
};
export const useViewportActionCornersContext = () => useContext(ViewportActionCornersContext);
@@ -1,93 +1,13 @@
import { colormaps } from '../utils/colormaps';
import {
ColorbarPositionType,
TickPositionType,
PositionStylesMapType,
PositionTickStylesMapType,
ContainerStyleType,
TickStyleType,
ColorbarProperties,
} from '../types/Colorbar';
import { ColorMapPreset } from '../types/Colormap';
const defaultPosition: ColorbarPositionType = 'bottom';
const DefaultColormap = 'Grayscale';
const positionStyles: PositionStylesMapType = {
left: { width: '15px' },
right: { width: '15px' },
bottom: { height: '15px' },
};
// Typed position-specific tick styles
const positionTickStyles: PositionTickStylesMapType = {
bottom: {
position: 'top',
style: {
labelOffset: 5,
labelMargin: 13,
},
},
right: {
position: 'left',
style: {
labelMargin: 5,
},
},
left: {
position: 'right',
style: {
labelMargin: 5,
},
},
top: {
position: 'bottom',
style: {
labelMargin: 5,
},
},
};
// Get recommended tick position for a given colorbar position
const getTickPositionForPosition = (position: ColorbarPositionType): TickPositionType => {
if (position === 'bottom') {
return 'top';
} else if (position === 'top') {
return 'bottom';
} else if (position === 'left') {
return 'right';
} else if (position === 'right') {
return 'left';
}
return positionTickStyles[position]?.position || 'top';
};
// Container styles for colorbar
const containerStyles: ContainerStyleType = {
cursor: 'initial',
};
// Tick styling
const tickStyles: TickStyleType = {
font: '12px Arial',
color: '#fff',
maxNumTicks: 6,
tickSize: 5,
tickWidth: 1,
};
const colorbarConfig: Partial<ColorbarProperties> = {
colorbarTickPosition: getTickPositionForPosition(defaultPosition),
colormaps: colormaps as unknown as Record<string, ColorMapPreset>,
colorbarContainerPosition: defaultPosition,
colorbarInitialColormap: DefaultColormap,
positionStyles,
positionTickStyles,
containerStyles,
tickStyles,
};
export default {
'cornerstone.colorbar': colorbarConfig,
'cornerstone.colorbar': {
width: '16px',
colorbarTickPosition: 'left',
colormaps,
colorbarContainerPosition: 'right',
colorbarInitialColormap: DefaultColormap,
},
};
@@ -1,23 +0,0 @@
export default {
'cornerstone.modalityOverlayDefaultColorMaps': {
defaultSettings: {
PT: {
colormap: 'hsv',
// Note: Right now, there is a nonlinear relationship between the opacity value
// below and how it will get applied to the image. The limitation is in rendering.
// We are working on this and will remove this note when it's fixed.
// But don't expect 0.5 to be 50% opacity, but rather close to that.
opacity: 0.5,
},
RTDOSE: {
colormap: 'Isodose',
// Note: Right now, there is a nonlinear relationship between the opacity value
// below and how it will get applied to the image. The limitation is in rendering.
// We are working on this and will remove this note when it's fixed.
// But don't expect 0.5 to be 50% opacity, but rather close to that.
opacity: 0.5,
},
},
},
};
@@ -0,0 +1,14 @@
import viewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService';
import { WindowLevelActionMenu } from '../components/WindowLevelActionMenu/WindowLevelActionMenu';
export default {
'viewportActionMenu.windowLevelActionMenu': {
enabled: true,
location: viewportActionCornersService.LOCATIONS.topRight,
component: WindowLevelActionMenu,
},
'viewportActionMenu.segmentationOverlay': {
enabled: true,
location: viewportActionCornersService.LOCATIONS.topRight,
},
};
@@ -6,11 +6,11 @@ import viewportClickCommandsCustomization from './customizations/viewportClickCo
import measurementsCustomization from './customizations/measurementsCustomization';
import volumeRenderingCustomization from './customizations/volumeRenderingCustomization';
import colorbarCustomization from './customizations/colorbarCustomization';
import modalityColorMapCustomization from './customizations/modalityColorMapCustomization';
import windowLevelPresetsCustomization from './customizations/windowLevelPresetsCustomization';
import miscCustomization from './customizations/miscCustomization';
import captureViewportModalCustomization from './customizations/captureViewportModalCustomization';
import viewportDownloadWarningCustomization from './customizations/viewportDownloadWarningCustomization';
import viewportActionMenuCustomizations from './customizations/viewportActionMenuCustomizations';
function getCustomizationModule({ commandsManager, servicesManager }) {
return [
@@ -25,11 +25,11 @@ function getCustomizationModule({ commandsManager, servicesManager }) {
...measurementsCustomization,
...volumeRenderingCustomization,
...colorbarCustomization,
...modalityColorMapCustomization,
...windowLevelPresetsCustomization,
...miscCustomization,
...captureViewportModalCustomization,
...viewportDownloadWarningCustomization,
...viewportActionMenuCustomizations,
},
},
];
Loaded 100 of 1302 files, more files were not shown because too many files have changed in this diff. Show more