feat(v3p7): segmentation mode and cloud server configuration (#3720)

This commit is contained in:
Alireza 2023-10-11 12:49:12 -04:00 committed by GitHub
commit 5ddf8a1602
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1448 changed files with 53812 additions and 39218 deletions

View File

@ -13,22 +13,24 @@ version: 2.1
## ##
orbs: orbs:
codecov: codecov/codecov@1.0.5 codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@1.26.0 cypress: cypress-io/cypress@3.1.4
executors: executors:
# Custom executor to override Cypress config cypress-custom:
deploy-to-prod-executor: description: |
Single Docker container used to run Cypress Tests
docker: docker:
- image: cimg/node:16.14 - image: cimg/node:<< parameters.node-version >>-browsers
environment: parameters:
CYPRESS_BASE_URL: https://ohif-staging.netlify.com/ node-version:
chrome-and-pacs: default: '18.16.1'
docker: description: |
# Primary container image where all steps run. The version of Node to run your tests with.
- image: 'cypress/browsers:node16.14.2-slim-chrome103-ff102' type: string
defaults: &defaults defaults: &defaults
docker: docker:
- image: cimg/node:16.14-browsers - image: cimg/node:18.18
environment: environment:
TERM: xterm # Enable colors in term TERM: xterm # Enable colors in term
QUICK_BUILD: true QUICK_BUILD: true
@ -43,6 +45,7 @@ jobs:
steps: steps:
# Update yarn # Update yarn
- run: yarn -v - run: yarn -v
- run: node --version
# Checkout code and ALL Git Tags # Checkout code and ALL Git Tags
- checkout - checkout
- restore_cache: - restore_cache:
@ -90,43 +93,6 @@ jobs:
file: '/home/circleci/repo/platform/core/coverage/reports' file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core' flags: 'core'
###
# Workflow: PR_OPTIONAL_DOCKER_PUBLISH
###
DOCKER_PR_PUBLISH:
<<: *defaults
steps:
# Enable yarn workspaces
- run: yarn config set workspaces-experimental true
# Checkout code and ALL Git Tags
- checkout
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build and push Docker image
command: |
# Remove npm config
rm -f ./.npmrc
# Set our version number using vars
echo $CIRCLE_BUILD_NUM
# Build our image, auth, and push
docker build --tag ohif/app:PR_BUILD-$CIRCLE_BUILD_NUM .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/app:PR_BUILD-$CIRCLE_BUILD_NUM
### ###
# Workflow: DEPLOY # Workflow: DEPLOY
### ###
@ -177,62 +143,52 @@ jobs:
- commit.txt - commit.txt
- version.json - version.json
# DEPLOY_TO_DEV: # just to make sure later on we can publish them
# docker: BUILD_PACKAGES_QUICK:
# - image: circleci/node:16.14.0 <<: *defaults
# environment: steps:
# TERM: xterm - run: yarn -v
# NETLIFY_SITE_ID: 32708787-c9b0-4634-b50f-7ca41952da77 # Checkout code and ALL Git Tags
# working_directory: ~/repo - checkout
# steps: - attach_workspace:
# - attach_workspace: at: ~/repo
# at: ~/repo # Use increasingly general patterns to restore cache
# - run: cd .netlify && npm install - restore_cache:
# - run: cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects name: Restore Yarn and Cypress Package Cache
# - run: cd .netlify && npm run deploy keys:
- yarn-packages-{{ checksum "yarn.lock" }}
# DEPLOY_TO_STAGING: - yarn-packages-
# docker: - run:
# - image: circleci/node:16.14.0 name: Install Dependencies
# environment: command: yarn install --frozen-lockfile
# TERM: xterm - save_cache:
# NETLIFY_SITE_ID: c7502ae3-b150-493c-8422-05701e44a969 name: Save Yarn Package Cache
# working_directory: ~/repo paths:
# steps: - ~/.cache/yarn
# - attach_workspace: key: yarn-packages-{{ checksum "yarn.lock" }}
# at: ~/repo - run:
# - run: cd .netlify && npm install name: Avoid hosts unknown for github
# - run: cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects command: |
# - run: cd .netlify && npm run deploy rm -rf ~/.ssh
mkdir ~/.ssh/
# DEPLOY_TO_PRODUCTION: echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
# docker: git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
# - image: circleci/node:16.14.0 git config --global user.name "ohif-bot"
# environment: - run:
# TERM: xterm name: Authenticate with NPM registry
# NETLIFY_SITE_ID: 79c4a5da-5c95-4dc9-84f7-45fd9dfe21b0 command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
# working_directory: ~/repo - run:
# steps: name: Increase the event emitter limit
# - attach_workspace: command: |
# at: ~/repo node ./increaseEventEmitterLimit.mjs
# - run: cd .netlify && npm install - run:
# - run: cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects name: build half of the packages (to avoid out of memory in circleci)
# - run: cd .netlify && npm run deploy command: |
yarn run build:package-all
# DEPLOY_TO_RELEASE_DEV: - run:
# docker: name: build the other half of the packages
# - image: circleci/node:16.14.0 command: |
# environment: yarn run build:package-all-1
# TERM: xterm
# NETLIFY_SITE_ID: 3270878-22
# working_directory: ~/repo
# steps:
# - attach_workspace:
# at: ~/repo
# - run: cd .netlify && npm install
# - run:
# cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects
# - run: cd .netlify && npm run deploy
### ###
# Workflow: RELEASE # Workflow: RELEASE
@ -269,8 +225,7 @@ jobs:
git config --global user.name "ohif-bot" git config --global user.name "ohif-bot"
- run: - run:
name: Authenticate with NPM registry name: Authenticate with NPM registry
command: command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run: - run:
name: Increase the event emitter limit name: Increase the event emitter limit
command: | command: |
@ -283,14 +238,21 @@ jobs:
name: build the other half of the packages name: build the other half of the packages
command: | command: |
yarn run build:package-all-1 yarn run build:package-all-1
- run:
name: increase min time out
command: |
npm config set fetch-retry-mintimeout 20000
- run:
name: increase max time out
command: |
npm config set fetch-retry-maxtimeout 120000
- run: - run:
name: publish package versions name: publish package versions
command: | command: |
node ./publish-version.mjs node ./publish-version.mjs
- run: - run:
name: Again set the NPM registry (was deleted in the version script) name: Again set the NPM registry (was deleted in the version script)
command: command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run: - run:
name: publish package dist name: publish package dist
command: | command: |
@ -337,8 +299,7 @@ jobs:
- setup_remote_docker: - setup_remote_docker:
docker_layer_caching: false docker_layer_caching: false
- run: - run:
name: name: Build and push Docker image from the master branch (beta releases)
Build and push Docker image from the master branch (beta releases)
command: | command: |
echo $(ls -l) echo $(ls -l)
@ -365,71 +326,152 @@ jobs:
docker push ohif/app:$IMAGE_VERSION_FULL docker push ohif/app:$IMAGE_VERSION_FULL
fi fi
workflows: # This is copied from the Cypress orb since the default for cypress/run is node 16 and
version: 2 # we migrated to 18
CYPRESS_CUSTOM_RUN:
description: |
A single, complete job to run Cypress end-to-end tests in your application.
executor: cypress-custom
parallelism: << parameters.parallelism >>
parameters:
cypress-cache-key:
default: cypress-cache-{{ arch }}-{{ checksum "package.json" }}
description: Cache key used to cache the Cypress binary.
type: string
cypress-cache-path:
default: ~/.cache/Cypress
description: |
By default, this will cache the '~/.cache/Cypress' directory so that the Cypress binary is cached. You can override this by providing your own cache path.
type: string
cypress-command:
default: npx cypress run
description: Command used to run your Cypress tests
type: string
include-branch-in-node-cache-key:
default: false
description: |
If true, this cache will only apply to runs within the same branch. (Adds -{{ .Branch }}- to the node cache key)
type: boolean
install-browsers:
default: false
description: |
Cypress runs by default in the Electron browser. Use this flag to install additional browsers to run your tests in.
This is only needed if you are passing the `--browser` flag in your `cypress-command`.
This parameter leverages the `circleci/browser-tools` orb and includes Chrome and FireFox.
If you need additional browser support you can set this to false and use an executor with a docker image
that includes the browsers of your choosing. See https://hub.docker.com/r/cypress/browsers/tags
type: boolean
install-command:
default: ''
description: Overrides the default NPM command (npm ci)
type: string
node-cache-version:
default: v1
description:
Change the default node cache version if you need to clear the cache for any reason.
type: string
package-manager:
default: npm
description: Select the default node package manager to use. NPM v5+ Required.
enum:
- npm
- yarn
- yarn-berry
type: enum
parallelism:
default: 1
description: |
Number of Circle machines to use for load balancing, min 1
(requires `parallel` and `record` flags in your `cypress-command`)
type: integer
post-install:
default: ''
description: |
Additional commands to run after running install but before verifying Cypress and saving cache.
type: string
start-command:
default: ''
description: Command used to start your local dev server for Cypress to tests against
type: string
working-directory:
default: ''
description: Directory containing package.json
type: string
steps:
- cypress/install:
cypress-cache-key: << parameters.cypress-cache-key >>
cypress-cache-path: << parameters.cypress-cache-path >>
include-branch-in-node-cache-key: << parameters.include-branch-in-node-cache-key >>
install-browsers: << parameters.install-browsers >>
install-command: << parameters.install-command >>
node-cache-version: << parameters.node-cache-version >>
package-manager: << parameters.package-manager >>
post-install: << parameters.post-install >>
working-directory: << parameters.working-directory >>
- cypress/run-tests:
cypress-command: << parameters.cypress-command >>
start-command: << parameters.start-command >>
working-directory: << parameters.working-directory >>
workflows:
PR_CHECKS: PR_CHECKS:
jobs: jobs:
- UNIT_TESTS - BUILD_PACKAGES_QUICK:
filters:
# E2E: PWA branches:
- cypress/run: ignore: master
name: 'E2E: PWA' - UNIT_TESTS:
executor: chrome-and-pacs
browser: chrome
pre-steps:
- run: |
# Clear yarn cache; use yarn from image (update image to update yarn)
rm -rf ~/.yarn
yarn -v
yarn: true
record: true
store_artifacts: true
working_directory: platform/app
build: yarn test:data
start: yarn run test:e2e:serve
spec: 'cypress/integration/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/app/cypress/screenshots
- store_artifacts:
path: platform/app/cypress/videos
- store_test_results:
path: platform/app/cypress/results
requires: requires:
- UNIT_TESTS - BUILD_PACKAGES_QUICK
- CYPRESS_CUSTOM_RUN:
PR_OPTIONAL_VISUAL_TESTS: name: 'Cypress Tests'
jobs: context: cypress
- AWAIT_APPROVAL: matrix:
type: approval parameters:
# Update hub.docker.org start-command:
- cypress/run: - yarn run test:data && yarn run test:e2e:serve
name: 'Generate Percy Snapshots' install-browsers:
executor: cypress/browsers-chrome76 - true
browser: chrome cypress-command:
pre-steps: - 'npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run
- run: 'rm -rf ~/.yarn && yarn -v && yarn global add wait-on' --record --browser chrome --parallel'
yarn: true package-manager:
store_artifacts: false - 'yarn'
working_directory: platform/app cypress-cache-key:
build: - 'yarn-packages-{{ checksum "yarn.lock" }}'
yarn test:data && npx cross-env QUICK_BUILD=true cypress-cache-path:
APP_CONFIG=config/dicomweb-server.js yarn run build - '~/.cache/Cypress'
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/app/cypress/screenshots
- store_artifacts:
path: platform/app/cypress/videos
requires: requires:
- AWAIT_APPROVAL - BUILD_PACKAGES_QUICK
# PR_OPTIONAL_VISUAL_TESTS:
# jobs:
# - AWAIT_APPROVAL:
# type: approval
# # Update hub.docker.org
# - cypress/run:
# name: 'Generate Percy Snapshots'
# executor: cypress/browsers-chrome76
# browser: chrome
# pre-steps:
# - run: 'rm -rf ~/.yarn && yarn -v && yarn global add wait-on'
# yarn: true
# store_artifacts: false
# working_directory: platform/app
# build:
# yarn test:data && npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
# yarn run build
# # start server --> verify running --> percy + chrome + cypress
# command: yarn run test:e2e:dist
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
# no-workspace: true # Don't persist workspace
# post-steps:
# - store_artifacts:
# path: platform/app/cypress/screenshots
# - store_artifacts:
# path: platform/app/cypress/videos
# requires:
# - AWAIT_APPROVAL
# Our master branch deploys to viewer-dev.ohif.org, the viewer.ohif.org is # Our master branch deploys to viewer-dev.ohif.org, the viewer.ohif.org is
# deployed from the release branch which is more stable and less frequently updated. # deployed from the release branch which is more stable and less frequently updated.

6
.codespellrc Normal file
View File

@ -0,0 +1,6 @@
[codespell]
skip = .git,*.pdf,*.svg,yarn.lock,*.min.js,locales
# ignore words ending with … and some camelcased variables and names
ignore-regex = \b\S+…\S*|\b(doubleClick|afterAll|PostgresSQL)\b|\bWee, L\.|.*te.*Telugu.*
# some odd variables
ignore-words-list = datea,ser,childrens

View File

@ -1,48 +0,0 @@
worker_processes 1;
events { worker_connections 1024; }
http {
upstream orthanc-server {
server orthanc:8042;
}
server {
listen [::]:80 default_server;
listen 80;
# CORS Magic
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow_Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow_Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
proxy_pass http://orthanc:8042;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
# CORS Magic
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow_Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
}
}
}

View File

@ -1,89 +0,0 @@
{
"Name": "Orthanc inside Docker",
"StorageDirectory": "/var/lib/orthanc/db",
"IndexDirectory": "/var/lib/orthanc/db",
"StorageCompression": false,
"MaximumStorageSize": 0,
"MaximumPatientCount": 0,
"LuaScripts": [],
"Plugins": ["/usr/share/orthanc/plugins", "/usr/local/share/orthanc/plugins"],
"ConcurrentJobs": 2,
"HttpServerEnabled": true,
"HttpPort": 8042,
"HttpDescribeErrors": true,
"HttpCompressionEnabled": true,
"DicomServerEnabled": true,
"DicomAet": "ORTHANC",
"DicomCheckCalledAet": false,
"DicomPort": 4242,
"DefaultEncoding": "Latin1",
"DeflatedTransferSyntaxAccepted": true,
"JpegTransferSyntaxAccepted": true,
"Jpeg2000TransferSyntaxAccepted": true,
"JpegLosslessTransferSyntaxAccepted": true,
"JpipTransferSyntaxAccepted": true,
"Mpeg2TransferSyntaxAccepted": true,
"RleTransferSyntaxAccepted": true,
"UnknownSopClassAccepted": false,
"DicomScpTimeout": 30,
"RemoteAccessAllowed": true,
"SslEnabled": false,
"SslCertificate": "certificate.pem",
"AuthenticationEnabled": false,
"RegisteredUsers": {
"test": "test"
},
"DicomModalities": {},
"DicomModalitiesInDatabase": false,
"DicomAlwaysAllowEcho": true,
"DicomAlwaysAllowStore": true,
"DicomCheckModalityHost": false,
"DicomScuTimeout": 10,
"OrthancPeers": {},
"OrthancPeersInDatabase": false,
"HttpProxy": "",
"HttpVerbose": true,
"HttpTimeout": 10,
"HttpsVerifyPeers": true,
"HttpsCACertificates": "",
"UserMetadata": {},
"UserContentType": {},
"StableAge": 60,
"StrictAetComparison": false,
"StoreMD5ForAttachments": true,
"LimitFindResults": 0,
"LimitFindInstances": 0,
"LimitJobs": 10,
"LogExportedResources": false,
"KeepAlive": true,
"TcpNoDelay": true,
"HttpThreadsCount": 50,
"StoreDicom": true,
"DicomAssociationCloseDelay": 5,
"QueryRetrieveSize": 10,
"CaseSensitivePN": false,
"LoadPrivateDictionary": true,
"Dictionary": {},
"SynchronousCMove": true,
"JobsHistorySize": 10,
"SaveJobs": true,
"OverwriteInstances": false,
"MediaArchiveSize": 1,
"StorageAccessOnFind": "Always",
"MetricsEnabled": true,
"DicomWeb": {
"Enable": true,
"Root": "/dicom-web/",
"EnableWado": true,
"WadoRoot": "/wado",
"Host": "127.0.0.1",
"Ssl": false,
"StowMaxInstances": 10,
"StowMaxSize": 10,
"QidoCaseSensitive": false
}
}

View File

@ -1,23 +0,0 @@
version: '3.5'
services:
orthanc:
image: jodogne/orthanc-plugins:1.11.0
hostname: orthanc
volumes:
# Config
- ./config/orthanc.json:/etc/orthanc/orthanc.json:ro
# Persist data
- ./volumes/orthanc-db/:/var/lib/orthanc/db/
ports:
- '4242:4242' # DICOM
- '8042:8042' # Web
restart: unless-stopped
nginx:
image: nginx:latest
volumes:
- ./config/nginx.conf:/etc/nginx/nginx.conf
ports:
- '80:80' #ngnix proxy
depends_on:
- orthanc

View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -1,5 +1,6 @@
server { server {
listen ${PORT}; listen ${PORT} default_server;
listen [::]:${PORT} default_server;
location / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html index.htm; index index.html index.htm;

View File

@ -0,0 +1,22 @@
server {
listen ${SSL_PORT} ssl http2 default_server;
listen [::]:${SSL_PORT} ssl http2 default_server;
ssl_certificate /etc/ssl/certs/ssl-certificate.crt;
ssl_certificate_key /etc/ssl/private/ssl-private-key.key;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Resource-Policy same-origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -1,6 +1,16 @@
#!/bin/sh #!/bin/sh
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf if [ -n "$SSL_PORT" ]
then
envsubst '${SSL_PORT}:${PORT}' < /usr/src/default.ssl.conf.template > /etc/nginx/conf.d/default.conf
else
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
fi
if [ -n "$APP_CONFIG" ]
then
echo "$APP_CONFIG" > /usr/share/nginx/html/app-config.js
fi
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ] if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
then then

View File

@ -1,6 +1,12 @@
# Reduces size of context and hides # Reduces size of context and hides
# files from Docker (can't COPY or ADD these) # files from Docker (can't COPY or ADD these)
# Note that typically the Docker context for various OHIF containers is the
# directory of this file (i.e. the root of the source). As such, this is
# the .dockerignore file for ALL Docker containers that are built. For example,
# the Docker containers built from the recipes in ./platform/app/.recipes will
# have this file as their .dockerignore.
# Output # Output
dist/ dist/
build/ build/
@ -28,3 +34,4 @@ dockerfile
.vscode/ .vscode/
coverage/ coverage/
docs/ docs/
testdata/

View File

@ -1,10 +1,5 @@
{ {
"plugins": [ "plugins": ["@typescript-eslint", "import", "eslint-plugin-tsdoc", "prettier"],
"@typescript-eslint",
"import",
"eslint-plugin-tsdoc",
"prettier"
],
"extends": [ "extends": [
"react-app", "react-app",
"eslint:recommended", "eslint:recommended",
@ -21,6 +16,10 @@
"version": "detect" "version": "detect"
} }
}, },
"rules": {
// Enforce consistent brace style for all control statements for readability
"curly": "error"
},
"globals": { "globals": {
"cy": true, "cy": true,
"before": true, "before": true,

View File

@ -39,8 +39,7 @@ body:
attributes: attributes:
label: The current behavior label: The current behavior
description: description:
'A clear and concise description of what happens instead of the expected 'A clear and concise description of what happens instead of the expected behavior.'
behavior.'
validations: validations:
required: true required: true
@ -48,8 +47,7 @@ body:
id: expected_behavior id: expected_behavior
attributes: attributes:
label: The expected behavior label: The expected behavior
description: description: 'A clear and concise description of what you expected to happen.'
'A clear and concise description of what you expected to happen.'
validations: validations:
required: true required: true
@ -66,7 +64,7 @@ body:
attributes: attributes:
label: 'Node version' label: 'Node version'
description: 'Your Node.js version.' description: 'Your Node.js version.'
placeholder: 'e.g., 16.14.0' placeholder: 'e.g., 18.16.1'
validations: validations:
required: true required: true
- type: input - type: input
@ -81,6 +79,6 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: > value: >
> :warning: Reports we cannot reproduce are at risk of being marked > :warning: Reports we cannot reproduce are at risk of being marked stale and > closed. The
stale and > closed. The more information you can provide, the more more information you can provide, the more likely we are to look > into and address your
likely we are to look > into and address your issue. issue.

View File

@ -20,17 +20,15 @@ body:
attributes: attributes:
label: 'What feature or change would you like to see made?' label: 'What feature or change would you like to see made?'
description: description:
'Please include as much detail as possible including possibly mock up 'Please include as much detail as possible including possibly mock up screen shots, workflow
screen shots, workflow or logic flow diagrams etc.' or logic flow diagrams etc.'
placeholder: '...' placeholder: '...'
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: 'Why should we prioritize this feature?' label: 'Why should we prioritize this feature?'
description: description: 'Discuss if and how the requested feature interacts with existing features.'
'Discuss if and how the requested feature interacts with existing
features.'
placeholder: '...' placeholder: '...'
validations: validations:
required: true required: true

View File

@ -83,7 +83,7 @@ after the commits are squashed.
#### Tested Environment #### Tested Environment
- [] OS: <!--[e.g. Windows 10, macOS 10.15.4]--> - [] OS: <!--[e.g. Windows 10, macOS 10.15.4]-->
- [] Node version: <!--[e.g. 16.14.0]--> - [] Node version: <!--[e.g. 18.16.1]-->
- [] Browser: - [] Browser:
<!--[e.g. Chrome 83.0.4103.116, Firefox 77.0.1, Safari 13.1.1]--> <!--[e.g. Chrome 83.0.4103.116, Firefox 77.0.1, Safari 13.1.1]-->

5
.github/stale.yml vendored
View File

@ -19,8 +19,7 @@ exemptLabels:
staleLabel: 'Stale :baguette_bread:' staleLabel: 'Stale :baguette_bread:'
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable
markComment: > markComment: >
This issue has been automatically marked as stale because it has not had This issue has been automatically marked as stale because it has not had recent activity. It will
recent activity. It will be closed if no further activity occurs. Thank you be closed if no further activity occurs. Thank you for your contributions.
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable # Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false closeComment: false

22
.github/workflows/codespell.yml vendored Normal file
View File

@ -0,0 +1,22 @@
---
name: Codespell
on:
push:
branches: [master]
pull_request:
branches: [master]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Codespell
uses: codespell-project/actions-codespell@v2

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ platform/app/src/pluginImports.js
/Viewers.iml /Viewers.iml
platform/app/.recipes/Nginx-Dcm4Che/dcm4che/dcm4che-arc/* platform/app/.recipes/Nginx-Dcm4Che/dcm4che/dcm4che-arc/*
platform/app/.recipes/OpenResty-Orthanc/logs/* platform/app/.recipes/OpenResty-Orthanc/logs/*
.vercel
.vs

1
.gitmodules vendored
View File

@ -1,3 +1,4 @@
[submodule "testdata"] [submodule "testdata"]
path = testdata path = testdata
url = https://github.com/OHIF/viewer-testdata-dicomweb.git url = https://github.com/OHIF/viewer-testdata-dicomweb.git
branch = main

View File

@ -1,23 +1,21 @@
<html> <html>
<head>
<title>OHIF Viewer: Deploy Preview</title>
</head>
<head> <body>
<title>OHIF Viewer: Deploy Preview</title> <h1>Index of Previews</h1>
</head>
<body>
<h1>Index of Previews</h1>
<ul>
<li>
<a href="/pwa">OHIF Viewer</a>
</li>
<li>
<a href="/docs">Documentation</a>
</li>
<li>
<a href="/ui">UI: Component Library</a>
</li>
</ul>
</body>
<ul>
<li>
<a href="/pwa">OHIF Viewer</a>
</li>
<li>
<a href="/docs">Documentation</a>
</li>
<li>
<a href="/ui">UI: Component Library</a>
</li>
</ul>
</body>
</html> </html>

View File

@ -1 +1 @@
16.14.0 18.16.1

View File

@ -1,9 +1,12 @@
{ {
"plugins": ["prettier-plugin-tailwindcss"],
"trailingComma": "es5", "trailingComma": "es5",
"printWidth": 80, "printWidth": 100,
"proseWrap": "always", "proseWrap": "always",
"tabWidth": 2, "tabWidth": 2,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"arrowParens": "avoid" "arrowParens": "avoid",
"singleAttributePerLine": true,
"endOfLine": "auto"
} }

View File

@ -5,11 +5,11 @@ function excludeNodeModulesExcept(modules) {
if (pathSep == '\\') if (pathSep == '\\')
// must be quoted for use in a regexp: // must be quoted for use in a regexp:
pathSep = '\\\\'; pathSep = '\\\\';
var moduleRegExps = modules.map(function(modName) { var moduleRegExps = modules.map(function (modName) {
return new RegExp('node_modules' + pathSep + modName); return new RegExp('node_modules' + pathSep + modName);
}); });
return function(modulePath) { return function (modulePath) {
if (/node_modules/.test(modulePath)) { if (/node_modules/.test(modulePath)) {
for (var i = 0; i < moduleRegExps.length; i++) for (var i = 0; i < moduleRegExps.length; i++)
if (moduleRegExps[i].test(modulePath)) return false; if (moduleRegExps[i].test(modulePath)) return false;

View File

@ -1,9 +1,7 @@
const autoprefixer = require('autoprefixer'); const autoprefixer = require('autoprefixer');
const path = require('path'); const path = require('path');
const tailwindcss = require('tailwindcss'); const tailwindcss = require('tailwindcss');
const tailwindConfigPath = path.resolve( const tailwindConfigPath = path.resolve('../../platform/app/tailwind.config.js');
'../../platform/app/tailwind.config.js'
);
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const devMode = process.env.NODE_ENV !== 'production'; const devMode = process.env.NODE_ENV !== 'production';

View File

@ -0,0 +1,10 @@
const stylusToJavaScript = {
test: /\.styl$/,
use: [
{ loader: 'style-loader' }, // 3. Style nodes from JS Strings
{ loader: 'css-loader' }, // 2. CSS to CommonJS
{ loader: 'stylus-loader' }, // 1. Stylus to CSS
],
};
module.exports = stylusToJavaScript;

View File

@ -33,7 +33,11 @@ function transpileJavaScript(mode) {
rootMode: 'upward', rootMode: 'upward',
envName: mode, envName: mode,
cacheCompression: false, cacheCompression: false,
cacheDirectory: true, // Note: This was causing a lot of issues with yarn link of the cornerstone
// only set this to true if you don't have a yarn link to external libs
// otherwise expect the lib changes not to be reflected in the dev server
// as it will be cached
cacheDirectory: false,
}, },
}; };
} }

View File

@ -7,8 +7,7 @@ const fs = require('fs');
const webpack = require('webpack'); const webpack = require('webpack');
// ~~ PLUGINS // ~~ PLUGINS
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
.BundleAnalyzerPlugin;
const TerserJSPlugin = require('terser-webpack-plugin'); const TerserJSPlugin = require('terser-webpack-plugin');
// ~~ PackageJSON // ~~ PackageJSON
@ -19,6 +18,7 @@ const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js'); const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js'); const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
const cssToJavaScript = require('./rules/cssToJavaScript.js'); const cssToJavaScript = require('./rules/cssToJavaScript.js');
const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
// ~~ ENV VARS // ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV; const NODE_ENV = process.env.NODE_ENV;
@ -26,15 +26,34 @@ const QUICK_BUILD = process.env.QUICK_BUILD;
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0'; const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
// read from ../version.txt // read from ../version.txt
const VERSION_NUMBER = const VERSION_NUMBER = fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || '';
fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || '';
const COMMIT_HASH = const COMMIT_HASH = fs.readFileSync(path.join(__dirname, '../commit.txt'), 'utf8') || '';
fs.readFileSync(path.join(__dirname, '../commit.txt'), 'utf8') || '';
// //
dotenv.config(); dotenv.config();
const defineValues = {
/* Application */
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.NODE_DEBUG': JSON.stringify(process.env.NODE_DEBUG),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
'process.env.VERSION_NUMBER': JSON.stringify(VERSION_NUMBER),
'process.env.COMMIT_HASH': JSON.stringify(COMMIT_HASH),
/* i18n */
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(process.env.REACT_APP_I18N_DEBUG || ''),
};
// Only redefine updated values. This avoids warning messages in the logs
if (!process.env.APP_CONFIG) {
defineValues['process.env.APP_CONFIG'] = '';
}
module.exports = (env, argv, { SRC_DIR, ENTRY }) => { module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
if (!process.env.NODE_ENV) { if (!process.env.NODE_ENV) {
throw new Error('process.env.NODE_ENV not set'); throw new Error('process.env.NODE_ENV not set');
@ -73,21 +92,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
children: false, children: false,
warnings: true, warnings: true,
}, },
devServer: {
open: true,
port: 3000,
historyApiFallback: true,
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
},
cache: { cache: {
type: 'filesystem', type: 'filesystem',
}, },
module: { module: {
noParse: [/(codec)/, /(dicomicc)/], noParse: [/(codec)/, /(dicomicc)/],
rules: [ rules: [
{
test: /\.js$/,
enforce: 'pre',
use: 'source-map-loader',
},
transpileJavaScriptRule(mode), transpileJavaScriptRule(mode),
loadWebWorkersRule, loadWebWorkersRule,
// loadShadersRule, // loadShadersRule,
@ -98,6 +113,9 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
}, },
}, },
cssToJavaScript, cssToJavaScript,
// Note: Only uncomment the following if you are using the old style of stylus in v2
// Also you need to uncomment this platform/app/.webpack/rules/extractStyleChunks.js
// stylusToJavaScript,
{ {
test: /\.wasm/, test: /\.wasm/,
type: 'asset/resource', type: 'asset/resource',
@ -109,10 +127,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
alias: { alias: {
// Viewer project // Viewer project
'@': path.resolve(__dirname, '../platform/app/src'), '@': path.resolve(__dirname, '../platform/app/src'),
'@components': path.resolve( '@components': path.resolve(__dirname, '../platform/app/src/components'),
__dirname,
'../platform/app/src/components'
),
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'), '@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'), '@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'), '@state': path.resolve(__dirname, '../platform/app/src/state'),
@ -135,29 +150,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'], extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'],
// symlinked resources are resolved to their real path, not their symlinked location // symlinked resources are resolved to their real path, not their symlinked location
symlinks: true, symlinks: true,
fallback: { fs: false, path: false, zlib: false }, fallback: {
fs: false,
path: false,
zlib: false,
buffer: require.resolve('buffer'),
},
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin(defineValues),
/* Application */ new webpack.ProvidePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), Buffer: ['buffer', 'Buffer'],
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
'process.env.VERSION_NUMBER': JSON.stringify(VERSION_NUMBER),
'process.env.COMMIT_HASH': JSON.stringify(COMMIT_HASH),
/* i18n */
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
'process.env.LOCIZE_PROJECTID': JSON.stringify(
process.env.LOCIZE_PROJECTID || ''
),
'process.env.LOCIZE_API_KEY': JSON.stringify(
process.env.LOCIZE_API_KEY || ''
),
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(
process.env.REACT_APP_I18N_DEBUG || ''
),
}), }),
// Uncomment to generate bundle analyzer // Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(), // new BundleAnalyzerPlugin(),

565
CHANGELOG.md Normal file
View File

@ -0,0 +1,565 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11)
### Bug Fixes
* **display messages:** broken after timings ([#3719](https://github.com/OHIF/Viewers/issues/3719)) ([157b88c](https://github.com/OHIF/Viewers/commit/157b88c909d3289cb89ace731c1f9a19d40797ac))
# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11)
### Bug Fixes
* **export:** wrong export for the tmtv RT function ([#3715](https://github.com/OHIF/Viewers/issues/3715)) ([a3f2a1a](https://github.com/OHIF/Viewers/commit/a3f2a1a7b0d16bfcc0ecddc2ab731e54c5e377c8))
# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10)
### Bug Fixes
* **i18n:** display set(s) are two words for English messages ([#3711](https://github.com/OHIF/Viewers/issues/3711)) ([c3a5847](https://github.com/OHIF/Viewers/commit/c3a5847dcd3dce4f1c8d8b11af95f79e3f93f70d))
# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10)
### Bug Fixes
* **modules:** add stylus loader as an option to be uncommented ([#3710](https://github.com/OHIF/Viewers/issues/3710)) ([7c57f67](https://github.com/OHIF/Viewers/commit/7c57f67844b790fc6e47ac3f9708bf9d576389c8))
# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10)
### Bug Fixes
* **segmentation:** Various fixes for segmentation mode and other ([#3709](https://github.com/OHIF/Viewers/issues/3709)) ([a9a6ad5](https://github.com/OHIF/Viewers/commit/a9a6ad50eae67b43b8b34efc07182d788cacdcfe))
# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10)
### Bug Fixes
* **voi:** should publish voi change event on reset ([#3707](https://github.com/OHIF/Viewers/issues/3707)) ([52f34c6](https://github.com/OHIF/Viewers/commit/52f34c64d014f433ec1661a39b47e7fb27f15332))
# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09)
### Bug Fixes
* **modality unit:** fix the modality unit per target via upgrade of cs3d ([#3706](https://github.com/OHIF/Viewers/issues/3706)) ([0a42d57](https://github.com/OHIF/Viewers/commit/0a42d573bbca7f2551a831a46d3aa6b56674a580))
# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09)
### Bug Fixes
* **segmentation:** do not use SAB if not specified ([#3705](https://github.com/OHIF/Viewers/issues/3705)) ([4911e47](https://github.com/OHIF/Viewers/commit/4911e4796cef5e22cb7cc0ca73dc5c956bc75339))
# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06)
### Features
* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65))
# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06)
### Bug Fixes
* **bugs:** fixing lots of bugs regarding release candidate ([#3700](https://github.com/OHIF/Viewers/issues/3700)) ([8bc12a3](https://github.com/OHIF/Viewers/commit/8bc12a37d0353160ae5ea4624dc0b244b7d59c07))
# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06)
### Bug Fixes
* **segmentation scroll:** and hydration bugs ([#3701](https://github.com/OHIF/Viewers/issues/3701)) ([1fd98d9](https://github.com/OHIF/Viewers/commit/1fd98d922094d10fe0c6e9df726314ec9fce49e8))
# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04)
### Bug Fixes
* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155))
# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04)
### Features
* **locale:** add German translations - community PR ([#3697](https://github.com/OHIF/Viewers/issues/3697)) ([ebe8f71](https://github.com/OHIF/Viewers/commit/ebe8f71da22c1d24b58f889c5d803951e19817b6))
# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04)
### Features
* **locale:** Added Turkish language support (tr-TR) - Community PR ([#3695](https://github.com/OHIF/Viewers/issues/3695)) ([745050a](https://github.com/OHIF/Viewers/commit/745050a28ec7c2ef2e9a4d4e590040050b2177b2))
# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04)
### Bug Fixes
* **translation:** Side panel translate fix ([#3156](https://github.com/OHIF/Viewers/issues/3156)) ([29748d4](https://github.com/OHIF/Viewers/commit/29748d46a14d23817dbe196e0f64363fc61a8aed))
# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04)
### Bug Fixes
* **cli:** Add npm packaged mode not working ([#3689](https://github.com/OHIF/Viewers/issues/3689)) ([28cec04](https://github.com/OHIF/Viewers/commit/28cec04ff43b81e218c3e9addef4665b3833a6fe))
# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03)
### Features
* **debug:** Add timing information about time to first image/all images, and query time ([#3681](https://github.com/OHIF/Viewers/issues/3681)) ([108383b](https://github.com/OHIF/Viewers/commit/108383b9ef51e4bef82d9c932b9bc7aa5354e799))
# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03)
### Features
* **displayArea:** add display area to hanging protocol ([#3691](https://github.com/OHIF/Viewers/issues/3691)) ([5e7fe91](https://github.com/OHIF/Viewers/commit/5e7fe91617d7399f85702d82e7bfa028b8010a89))
# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03)
### Bug Fixes
* **editing:** regression bug in disable editing ([#3687](https://github.com/OHIF/Viewers/issues/3687)) ([4dc2acd](https://github.com/OHIF/Viewers/commit/4dc2acdefa872dd1d8df47f465e9e9656f95f67f))
# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03)
### Bug Fixes
* **typescript error:** Change pubSubServiceInterface file type to typescript ([#3546](https://github.com/OHIF/Viewers/issues/3546)) ([eb22328](https://github.com/OHIF/Viewers/commit/eb22328fc05d06fc4411805e7a30f826659d796a))
# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03)
### Bug Fixes
* **dicom overlay:** Handle special cases of ArrayBuffer for various DICOM overlay attributes. ([#3684](https://github.com/OHIF/Viewers/issues/3684)) ([e36a604](https://github.com/OHIF/Viewers/commit/e36a6043315e900eeb6ce183772c7f852f478e96))
* **StackSync:** Miscellaneous fixes for stack image sync ([#3663](https://github.com/OHIF/Viewers/issues/3663)) ([8a335bd](https://github.com/OHIF/Viewers/commit/8a335bd03d14ba87d65d7468d93f74040aa828d9))
# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03)
### Bug Fixes
* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e))
# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29)
### Bug Fixes
* **no sab:** should work when shared array buffer is not required ([#3686](https://github.com/OHIF/Viewers/issues/3686)) ([a67d72d](https://github.com/OHIF/Viewers/commit/a67d72de85238b369a18c010bf6d147daefc6df5))
# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29)
### Bug Fixes
* **cli:** various fixes for adding custom modes and extensions ([#3683](https://github.com/OHIF/Viewers/issues/3683)) ([dc73b18](https://github.com/OHIF/Viewers/commit/dc73b187484da029a2664bb1302f30137c973b8c))
# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26)
### Bug Fixes
* **toggleOneUp:** fixed one up for main tmtv layout ([#3677](https://github.com/OHIF/Viewers/issues/3677)) ([86f54d0](https://github.com/OHIF/Viewers/commit/86f54d0d07042750a863ae876aa8dd5fb16029a5))
# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22)
### Bug Fixes
* **react-select:** update react select package ([#3622](https://github.com/OHIF/Viewers/issues/3622)) ([04ca10d](https://github.com/OHIF/Viewers/commit/04ca10d8779dd15454920002f3d48afa8830de8a))
### Features
* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1))
* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378))
# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22)
### Performance Improvements
* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab))
# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21)
### Bug Fixes
* **mpr:** Return the original/raw hanging protocol when fetching and preserving the current active protocol. ([#3670](https://github.com/OHIF/Viewers/issues/3670)) ([221dedd](https://github.com/OHIF/Viewers/commit/221dedde5dd4df086276406a9fa2da1cc23b4eb1))
# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19)
### Bug Fixes
* **keyCloak:** fix openresty keycloak deployment recipe ([#3655](https://github.com/OHIF/Viewers/issues/3655)) ([2d7721c](https://github.com/OHIF/Viewers/commit/2d7721cb581f55dc49e3baeca2411b18dd78ad74))
# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18)
### Bug Fixes
* **DicomJson:** retrieve.series.metadata method should be async ([#3659](https://github.com/OHIF/Viewers/issues/3659)) ([2737903](https://github.com/OHIF/Viewers/commit/2737903386cf97399473e0fa64fe53ad14da155a))
# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15)
### Bug Fixes
* **measurements:** Update the calibration tool to match changes in CS3D ([#3505](https://github.com/OHIF/Viewers/issues/3505)) ([38af311](https://github.com/OHIF/Viewers/commit/38af3112ec1f94f36c0ef64ff1cf9d21c0981c81))
# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12)
### Bug Fixes
* **health imaging:** studies not loading from healthimaging if imagepositionpatient is missing ([#3646](https://github.com/OHIF/Viewers/issues/3646)) ([74e62a1](https://github.com/OHIF/Viewers/commit/74e62a176374f720080d4e777972f70e7f2d8b2b))
# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12)
### Bug Fixes
* **suv:** import calculate-suv library version that prevents SUV calculation for a zero PatientWeight ([#3638](https://github.com/OHIF/Viewers/issues/3638)) ([0d10f46](https://github.com/OHIF/Viewers/commit/0d10f46b885fe54ec3dae1848134da658eb6280a))
# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
### Bug Fixes
* **hotkeys:** preserve hotkeys if changed, and reduce re-rendering ([#3635](https://github.com/OHIF/Viewers/issues/3635)) ([94f7cfb](https://github.com/OHIF/Viewers/commit/94f7cfb08e3490488394efc42ef089ebe55e86be))
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
### Features
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
### Bug Fixes
* **nginx archive recipe:** Fixes to various configuration files. ([#3624](https://github.com/OHIF/Viewers/issues/3624)) ([3ce7225](https://github.com/OHIF/Viewers/commit/3ce72254b390f32c9aa207a0589e688805e2659d))
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
### Features
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
### Features
* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f))
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
### Bug Fixes
* **OpenIdConnectRoutes:** fix handleUnauthenticated ([#3617](https://github.com/OHIF/Viewers/issues/3617)) ([35fc30c](https://github.com/OHIF/Viewers/commit/35fc30c5359d8199cc38ffa670c08687d2672f11))
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
### Bug Fixes
* **PT Metadata:** Allow for PatientWeight to be missing from the metadata ([#3621](https://github.com/OHIF/Viewers/issues/3621)) ([44f101d](https://github.com/OHIF/Viewers/commit/44f101d3f2b3204b67e31f4e4939062e65a246ee))
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package ohif-monorepo-root
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
### Bug Fixes
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))

View File

@ -21,7 +21,7 @@
# Stage 1: Build the application # Stage 1: Build the application
# docker build -t ohif/viewer:latest . # docker build -t ohif/viewer:latest .
FROM node:16.15.0-slim as json-copier FROM node:18.16.1-slim as json-copier
RUN mkdir /usr/src/app RUN mkdir /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
@ -37,7 +37,8 @@ COPY platform /usr/src/app/platform
#RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf #RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
# Copy Files # Copy Files
FROM node:16.15.0-slim as builder FROM node:18.16.1-slim as builder
RUN apt-get update && apt-get install -y build-essential python3
RUN mkdir /usr/src/app RUN mkdir /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
@ -61,7 +62,7 @@ RUN yarn run build
# Stage 3: Bundle the built application into a Docker container # Stage 3: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux # which runs Nginx using Alpine Linux
FROM nginxinc/nginx-unprivileged:1.23.1-alpine as final FROM nginxinc/nginx-unprivileged:1.25-alpine as final
#RUN apk add --no-cache bash #RUN apk add --no-cache bash
ENV PORT=80 ENV PORT=80
RUN rm /etc/nginx/conf.d/default.conf RUN rm /etc/nginx/conf.d/default.conf
@ -69,5 +70,10 @@ USER nginx
COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src
RUN chmod 777 /usr/src/entrypoint.sh RUN chmod 777 /usr/src/entrypoint.sh
COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html
# In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable.
# The nginx user cannot chmod it, so change to root.
USER root
RUN chmod 666 /usr/share/nginx/html/app-config.js
USER nginx
ENTRYPOINT ["/usr/src/entrypoint.sh"] ENTRYPOINT ["/usr/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

View File

@ -3,7 +3,7 @@
<div align="center"> <div align="center">
<h1>OHIF Medical Imaging Viewer</h1> <h1>OHIF Medical Imaging Viewer</h1>
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer <p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer
provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/dicomweb/">DICOMweb</a>.</p> provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/using/dicomweb/">DICOMweb</a>.</p>
</div> </div>
@ -38,7 +38,16 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
<!-- [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors) --> <!-- [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors) -->
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
![Alt text](platform/docs/docs/assets/img/OHIF-Viewer.jpg)
| | | |
| :-: | :--- | :--- |
| <img src="platform/docs/docs/assets/img/demo-measurements.jpg" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
| <img src="platform/docs/docs/assets/img/demo-segmentation.png" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
| <img src="platform/docs/docs/assets/img/demo-ptct.png" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
| <img src="platform/docs/docs/assets/img/demo-microscopy.png" alt="Microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.275741864483510678566144889372061815320) |
| <img src="platform/docs/docs/assets/img/demo-volumeRendering.png" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
## About ## About
@ -85,17 +94,14 @@ forking).
### Support ### Support
We offer support through
[GitHub Issues](https://github.com/OHIF/Viewers/issues/new/choose). You can:
- [Report a Bug 🐛](https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Community%3A+Report+%3Abug%3A%2CAwaiting+Reproduction&projects=&template=bug-report.yml&title=%5BBug%5D+) - [Report a Bug 🐛](https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Community%3A+Report+%3Abug%3A%2CAwaiting+Reproduction&projects=&template=bug-report.yml&title=%5BBug%5D+)
- [Request a Feature 🚀](https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Community%3A+Request+%3Ahand%3A&projects=&template=feature-request.yml&title=%5BFeature+Request%5D+) - [Request a Feature 🚀](https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Community%3A+Request+%3Ahand%3A&projects=&template=feature-request.yml&title=%5BFeature+Request%5D+)
- [Ask a Question 🤗](community.ohif.org) - [Ask a Question 🤗](community.ohif.org)
- [Slack Channel](https://join.slack.com/t/cornerstonejs/shared_invite/zt-1r8xb2zau-dOxlD6jit3TN0Uwf928w9Q) - [Slack Channel](https://join.slack.com/t/cornerstonejs/shared_invite/zt-1r8xb2zau-dOxlD6jit3TN0Uwf928w9Q)
For commercial support, academic collaborations, and answers to common For commercial support, academic collaborations, and answers to common
questions; please read our questions; please use [Get Support](https://ohif.org/get-support/) to contact
[documented FAQ](https://docs.ohif.org/faq/index.html#does-ohif-offer-commercial-support). us.
## Developing ## Developing
@ -128,9 +134,6 @@ Here is a schematic representation of our development workflow:
### Requirements ### Requirements
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install) - [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
@ -164,7 +167,7 @@ yarn install
These commands are available from the root directory. Each project directory These commands are available from the root directory. Each project directory
also supports a number of commands that can be found in their respective also supports a number of commands that can be found in their respective
`README.md` and `project.json` files. `README.md` and `package.json` files.
| Yarn Commands | Description | | Yarn Commands | Description |
| ---------------------------- | ------------------------------------------------------------- | | ---------------------------- | ------------------------------------------------------------- |
@ -222,15 +225,6 @@ you'll see the following:
└── README.md # This file └── README.md # This file
``` ```
Want to better understand why and how we've structured this repository? Read
more about it in our [Architecture Documentation][ohif-architecture].
| Name | Description | Links |
| ---------------------------------------------------- | ----------------------------------------------------- | ---------------------- |
| [@ohif/extension-cornerstone][extension-cornerstone] | 2D image viewing, annotation, and segementation tools | [NPM][cornerstone-npm] |
## Acknowledgments ## Acknowledgments
To acknowledge the OHIF Viewer in an academic publication, please cite To acknowledge the OHIF Viewer in an academic publication, please cite
@ -259,7 +253,7 @@ or, for v1, please cite:
> [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334) > [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334)
**Note:** If you use or find this repository helpful, please take the time to **Note:** If you use or find this repository helpful, please take the time to
star this repository on Github. This is an easy way for us to assess adoption star this repository on GitHub. This is an easy way for us to assess adoption
and it can help us obtain future funding for the project. and it can help us obtain future funding for the project.
This work is supported primarily by the National Institutes of Health, National This work is supported primarily by the National Institutes of Health, National
@ -267,7 +261,11 @@ Cancer Institute, Informatics Technology for Cancer Research (ITCR) program,
under a under a
[grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104). [grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104).
This project is tested with BrowserStack. Thank you for supporting open source [NCI Imaging Data Commons (IDC) project](https://imaging.datacommons.cancer.gov/) supported the development of new features and bug fixes marked with ["IDC:priority"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Apriority),
["IDC:candidate"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acandidate) or ["IDC:collaboration"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acollaboration). NCI Imaging Data Commons is supported by contract number 19X037Q from
Leidos Biomedical Research under Task Order HHSN26100071 from NCI. [IDC Viewer](https://learn.canceridc.dev/portal/visualization) is a customized version of the OHIF Viewer.
This project is tested with BrowserStack. Thank you for supporting open-source!
## License ## License

View File

@ -3,22 +3,22 @@ const { extendDefaultPlugins } = require('svgo');
module.exports = { module.exports = {
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'], babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
presets: [ presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [ plugins: [
[ [
'inline-react-svg', 'inline-react-svg',
{ {
svgo: { svgo: {
plugins: extendDefaultPlugins([ plugins: [
{ {
name: 'removeViewBox', name: 'preset-default',
active: false, params: {
overrides: {
removeViewBox: false,
},
},
}, },
]), ],
}, },
}, },
], ],

View File

@ -1 +1 @@
1d38fe30a490010c7de487c7a0b1a5bfe3bc75a4 157b88c909d3289cb89ace731c1f9a19d40797ac

View File

@ -1,138 +0,0 @@
import ImageSet from '@ohif/core/src/classes/ImageSet';
import { IWebApiDataSource } from '@ohif/core';
/**
*
*/
export default {
id: '@ohif/extension-*',
/**
* LIFECYCLE HOOKS
*/
preRegistration() {},
beforeExtInit() {},
beforeExtDestroy() {},
/**
* MODULES
*/
getCommandsModule,
getContextModule,
getDataSourcesModule,
getLayoutTemplateModule,
getPanelModule,
getSopClassHandlerModule,
getToolbarModule() {},
getViewportModule,
};
// appConfig,
// extensionConfig,
// dataSources,
// servicesManager,
// extensionManager,
// commandsManager,
/**
*
*/
const getCommandsModule = () => ({
definitions: {
exampleActionDef: {
commandFn: ({ param1 }) => {
console.log(`param1's value is: ${param1}`);
},
// storeContexts: ['viewports'],
options: { param1: 'hello world' },
context: 'VIEWER', // optional
},
},
defaultContext: 'ACTIVE_VIEWPORT::DICOMSR',
});
const ExampleContext = React.createContext();
function ExampleContextProvider({ children }) {
return (
<ExampleContext.Provider value={{ example: 'value' }}>
{children}
</ExampleContext.Provider>
);
}
const getContextModule = () => [
{
name: 'ExampleContext',
context: ExampleContext,
provider: ExampleContextProvider,
},
];
const getDataSourcesModule = () => [
{
name: 'exampleDataSource',
type: 'webApi', // 'webApi' | 'local' | 'other'
createDataSource: dataSourceConfig => {
return IWebApiDataSource.create(/* */);
},
},
];
const getLayoutTemplateModule = (/* ... */) => [
{
id: 'exampleLayout',
name: 'exampleLayout',
component: ExampleLayoutComponent,
},
];
const getPanelModule = () => {
return [
{
name: 'exampleSidePanel',
iconName: 'info-circle-o',
iconLabel: 'Example',
label: 'Hello World',
isDisabled: studies => {}, // optional
component: ExamplePanelContentComponent,
},
];
};
const getSopClassHandlerModule = (/* ... */) => {
const BASIC_TEXT_SR = '1.2.840.10008.5.1.4.1.1.88.11';
return [
{
name: 'ExampleSopClassHandle',
sopClassUids: [BASIC_TEXT_SR],
getDisplaySetsFromSeries: instances => {
const imageSet = new ImageSet(instances);
imageSet.setAttributes(/** */);
imageSet.sortBy((a, b) => 0);
return imageSet;
},
},
];
};
const getToolbarModule = () => {};
// displaySet, viewportIndex, dataSource
const getViewportModule = () => {
const wrappedViewport = props => {
return (
<ExampleViewport
{...props}
onEvent={data => {
commandsManager.runCommand('commandName', data);
}}
/>
);
};
return [{ name: 'example', component: wrappedViewport }];
};

View File

@ -3,8 +3,7 @@ const { merge } = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js'); const webpackCommon = require('./../../../.webpack/webpack.base.js');
const pkg = require('./../package.json'); const pkg = require('./../package.json');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
.BundleAnalyzerPlugin;
const ROOT_DIR = path.join(__dirname, './..'); const ROOT_DIR = path.join(__dirname, './..');
const SRC_DIR = path.join(__dirname, '../src'); const SRC_DIR = path.join(__dirname, '../src');
@ -38,13 +37,7 @@ module.exports = (env, argv) => {
libraryTarget: 'umd', libraryTarget: 'umd',
filename: pkg.main, filename: pkg.main,
}, },
externals: [ externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
/\b(vtk.js)/,
/\b(dcmjs)/,
/\b(gl-matrix)/,
/^@ohif/,
/^@cornerstonejs/,
],
plugins: [ plugins: [
new webpack.optimize.LimitChunkCountPlugin({ new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1, maxChunks: 1,

View File

@ -0,0 +1,441 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22)
### Features
* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1))
# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
### Features
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt

View File

@ -10,7 +10,7 @@ module.exports = {
modules: 'commonjs', modules: 'commonjs',
debug: false, debug: false,
}, },
"@babel/preset-typescript", '@babel/preset-typescript',
], ],
'@babel/preset-react', '@babel/preset-react',
], ],
@ -26,7 +26,7 @@ module.exports = {
// WebPack handles ES6 --> Target Syntax // WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }], ['@babel/preset-env', { modules: false }],
'@babel/preset-react', '@babel/preset-react',
"@babel/preset-typescript", '@babel/preset-typescript',
], ],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
}, },
@ -35,7 +35,7 @@ module.exports = {
// WebPack handles ES6 --> Target Syntax // WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }], ['@babel/preset-env', { modules: false }],
'@babel/preset-react', '@babel/preset-react',
"@babel/preset-typescript", '@babel/preset-typescript',
], ],
plugins: ['react-hot-loader/babel'], plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],

View File

@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-cornerstone-dicom-rt", "name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.6.0", "version": "3.7.0-beta.110",
"description": "DICOM RT read workflow", "description": "DICOM RT read workflow",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@ -31,10 +31,10 @@
"start": "yarn run dev" "start": "yarn run dev"
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.6.0", "@ohif/core": "3.7.0-beta.110",
"@ohif/extension-cornerstone": "3.6.0", "@ohif/extension-cornerstone": "3.7.0-beta.110",
"@ohif/extension-default": "3.6.0", "@ohif/extension-default": "3.7.0-beta.110",
"@ohif/i18n": "3.6.0", "@ohif/i18n": "3.7.0-beta.110",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",

View File

@ -7,11 +7,7 @@ const sopClassUids = ['1.2.840.10008.5.1.4.1.1.481.3'];
let loadPromises = {}; let loadPromises = {};
function _getDisplaySetsFromSeries( function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
instances,
servicesManager,
extensionManager
) {
const instance = instances[0]; const instance = instances[0];
const { const {
@ -56,10 +52,7 @@ function _getDisplaySetsFromSeries(
}; };
let referencedSeriesSequence = instance.ReferencedSeriesSequence; let referencedSeriesSequence = instance.ReferencedSeriesSequence;
if ( if (instance.ReferencedFrameOfReferenceSequence && !instance.ReferencedSeriesSequence) {
instance.ReferencedFrameOfReferenceSequence &&
!instance.ReferencedSeriesSequence
) {
instance.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence( instance.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
instance.ReferencedFrameOfReferenceSequence instance.ReferencedFrameOfReferenceSequence
); );
@ -72,8 +65,7 @@ function _getDisplaySetsFromSeries(
const referencedSeries = referencedSeriesSequence[0]; const referencedSeries = referencedSeriesSequence[0];
displaySet.referencedImages = displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.getReferenceDisplaySet = () => { displaySet.getReferenceDisplaySet = () => {
@ -88,14 +80,12 @@ function _getDisplaySetsFromSeries(
const referencedDisplaySet = referencedDisplaySets[0]; const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
referencedDisplaySet.displaySetInstanceUID;
return referencedDisplaySet; return referencedDisplaySet;
}; };
displaySet.load = ({ headers }) => displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
_load(displaySet, servicesManager, extensionManager, headers);
return [displaySet]; return [displaySet];
} }
@ -194,11 +184,7 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) {
name: 'dicom-rt', name: 'dicom-rt',
sopClassUids, sopClassUids,
getDisplaySetsFromSeries: instances => { getDisplaySetsFromSeries: instances => {
return _getDisplaySetsFromSeries( return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
instances,
servicesManager,
extensionManager
);
}, },
}, },
]; ];

View File

@ -4,9 +4,7 @@ import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule'; import getSopClassHandlerModule from './getSopClassHandlerModule';
const Component = React.lazy(() => { const Component = React.lazy(() => {
return import( return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport'
);
}); });
const OHIFCornerstoneRTViewport = props => { const OHIFCornerstoneRTViewport = props => {
@ -36,12 +34,14 @@ const extension: Types.Extensions.Extension = {
getViewportModule({ getViewportModule({
servicesManager, servicesManager,
extensionManager, extensionManager,
commandsManager,
}: Types.Extensions.ExtensionParams) { }: Types.Extensions.ExtensionParams) {
const ExtendedOHIFCornerstoneRTViewport = props => { const ExtendedOHIFCornerstoneRTViewport = props => {
return ( return (
<OHIFCornerstoneRTViewport <OHIFCornerstoneRTViewport
servicesManager={servicesManager} servicesManager={servicesManager}
extensionManager={extensionManager} extensionManager={extensionManager}
commandsManager={commandsManager}
{...props} {...props}
/> />
); );

View File

@ -25,23 +25,13 @@ async function checkAndLoadContourData(instance, datasource) {
if (Array.isArray(contourData)) { if (Array.isArray(contourData)) {
promisesMap.has(referencedROINumber) promisesMap.has(referencedROINumber)
? promisesMap ? promisesMap.get(referencedROINumber).push(Promise.resolve(contourData))
.get(referencedROINumber) : promisesMap.set(referencedROINumber, [Promise.resolve(contourData)]);
.push(Promise.resolve(contourData))
: promisesMap.set(referencedROINumber, [
Promise.resolve(contourData),
]);
} else if (contourData && contourData.BulkDataURI) { } else if (contourData && contourData.BulkDataURI) {
const bulkDataURI = contourData.BulkDataURI; const bulkDataURI = contourData.BulkDataURI;
if ( if (!datasource || !datasource.retrieve || !datasource.retrieve.bulkDataURI) {
!datasource || return Promise.reject('Invalid datasource object or retrieve function');
!datasource.retrieve ||
!datasource.retrieve.bulkDataURI
) {
return Promise.reject(
'Invalid datasource object or retrieve function'
);
} }
const bulkDataPromise = datasource.retrieve.bulkDataURI({ const bulkDataPromise = datasource.retrieve.bulkDataURI({
@ -74,10 +64,7 @@ async function checkAndLoadContourData(instance, datasource) {
ROIContour.ContourSequence.forEach((Contour, index) => { ROIContour.ContourSequence.forEach((Contour, index) => {
const promise = resolvedPromises[index]; const promise = resolvedPromises[index];
if (promise.status === 'fulfilled') { if (promise.status === 'fulfilled') {
if ( if (Array.isArray(promise.value) && promise.value.every(Number.isFinite)) {
Array.isArray(promise.value) &&
promise.value.every(Number.isFinite)
) {
// If promise.value is already an array of numbers, use it directly // If promise.value is already an array of numbers, use it directly
Contour.ContourData = promise.value; Contour.ContourData = promise.value;
} else { } else {
@ -85,13 +72,8 @@ async function checkAndLoadContourData(instance, datasource) {
const uint8Array = new Uint8Array(promise.value); const uint8Array = new Uint8Array(promise.value);
const textDecoder = new TextDecoder(); const textDecoder = new TextDecoder();
const dataUint8Array = textDecoder.decode(uint8Array); const dataUint8Array = textDecoder.decode(uint8Array);
if ( if (typeof dataUint8Array === 'string' && dataUint8Array.includes('\\')) {
typeof dataUint8Array === 'string' && Contour.ContourData = dataUint8Array.split('\\').map(parseFloat);
dataUint8Array.includes('\\')
) {
Contour.ContourData = dataUint8Array
.split('\\')
.map(parseFloat);
} else { } else {
Contour.ContourData = []; Contour.ContourData = [];
} }
@ -120,9 +102,8 @@ export default async function loadRTStruct(
const { bulkDataURI } = dataSource.getConfig?.() || {}; const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports; const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet( const imageIdSopInstanceUidPairs =
referencedDisplaySet _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
);
// Set here is loading is asynchronous. // Set here is loading is asynchronous.
// If this function throws its set back to false. // If this function throws its set back to false.
@ -137,20 +118,14 @@ export default async function loadRTStruct(
); );
const dicomData = DicomMessage.readFile(segArrayBuffer); const dicomData = DicomMessage.readFile(segArrayBuffer);
const rtStructDataset = DicomMetaDictionary.naturalizeDataset( const rtStructDataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
dicomData.dict
);
rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
instance = rtStructDataset; instance = rtStructDataset;
} else { } else {
await checkAndLoadContourData(instance, dataSource); await checkAndLoadContourData(instance, dataSource);
} }
const { const { StructureSetROISequence, ROIContourSequence, RTROIObservationsSequence } = instance;
StructureSetROISequence,
ROIContourSequence,
RTROIObservationsSequence,
} = instance;
// Define our structure set entry and add it to the rtstruct module state. // Define our structure set entry and add it to the rtstruct module state.
const structureSet = { const structureSet = {
@ -174,19 +149,9 @@ export default async function loadRTStruct(
const contourPoints = []; const contourPoints = [];
for (let c = 0; c < ContourSequenceArray.length; c++) { for (let c = 0; c < ContourSequenceArray.length; c++) {
const { const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } =
ContourImageSequence, ContourSequenceArray[c];
ContourData,
NumberOfContourPoints,
ContourGeometricType,
} = ContourSequenceArray[c];
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
if (!imageId) {
continue;
}
let isSupported = false; let isSupported = false;
const points = []; const points = [];
@ -235,9 +200,7 @@ const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
); );
return imageIdSopInstanceUidPairsEntry return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
? imageIdSopInstanceUidPairsEntry.imageId
: null;
}; };
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) { function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
@ -258,8 +221,7 @@ function _setROIContourMetadata(
isSupported isSupported
) { ) {
const StructureSetROI = StructureSetROISequence.find( const StructureSetROI = StructureSetROISequence.find(
structureSetROI => structureSetROI => structureSetROI.ROINumber === ROIContour.ReferencedROINumber
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
); );
const ROIContourData = { const ROIContourData = {
@ -299,23 +261,15 @@ function _setROIContourDataColor(ROIContour, ROIContourData) {
} }
} }
function _setROIContourRTROIObservations( function _setROIContourRTROIObservations(ROIContourData, RTROIObservationsSequence, ROINumber) {
ROIContourData,
RTROIObservationsSequence,
ROINumber
) {
const RTROIObservations = RTROIObservationsSequence.find( const RTROIObservations = RTROIObservationsSequence.find(
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
); );
if (RTROIObservations) { if (RTROIObservations) {
// Deep copy so we don't keep the reference to the dcmjs dataset entry. // Deep copy so we don't keep the reference to the dcmjs dataset entry.
const { const { ObservationNumber, ROIObservationDescription, RTROIInterpretedType, ROIInterpreter } =
ObservationNumber, RTROIObservations;
ROIObservationDescription,
RTROIInterpretedType,
ROIInterpreter,
} = RTROIObservations;
ROIContourData.RTROIObservations = { ROIContourData.RTROIObservations = {
ObservationNumber, ObservationNumber,

View File

@ -1,70 +0,0 @@
async function _hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
servicesManager,
}) {
const {
segmentationService,
hangingProtocolService,
viewportGridService,
} = servicesManager.services;
const displaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
let segmentationId = null;
// We need the hydration to notify panels about the new segmentation added
const suppressEvents = false;
segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
rtDisplaySet,
segmentationId,
suppressEvents
);
segmentationService.hydrateSegmentation(rtDisplaySet.displaySetInstanceUID);
const { viewports } = viewportGridService.getState();
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportIndex,
displaySetInstanceUID
);
viewportGridService.setDisplaySetsForViewports(updatedViewports);
// Todo: fix this after we have a better way for stack viewport segmentations
// check every viewport in the viewports to see if the displaySetInstanceUID
// is being displayed, if so we need to update the viewport to use volume viewport
// (if already is not using it) since Cornerstone3D currently only supports
// volume viewport for segmentation
viewports.forEach((viewport, index) => {
if (index === viewportIndex) {
return;
}
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewport.displaySetInstanceUIDs,
rtDisplaySet.displaySetInstanceUID
);
if (shouldDisplaySeg) {
updatedViewports.push({
viewportIndex: index,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
initialImageOptions: {
preset: 'middle',
},
},
});
}
});
// Do the entire update at once
viewportGridService.setDisplaySetsForViewports(updatedViewports);
return true;
}
export default _hydrateRTDisplaySet;

View File

@ -1,12 +1,7 @@
function createRTToolGroupAndAddTools( function createRTToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
ToolGroupService, const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
customizationService,
toolGroupId
) {
const { tools } =
customizationService.get('cornerstone.overlayViewportTools') ?? {};
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {}); return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
} }
export default createRTToolGroupAndAddTools; export default createRTToolGroupAndAddTools;

View File

@ -1,4 +1,4 @@
import hydrateRTDisplaySet from './_hydrateRT'; import { ButtonEnums } from '@ohif/ui';
const RESPONSE = { const RESPONSE = {
NO_NEVER: -1, NO_NEVER: -1,
@ -9,21 +9,24 @@ const RESPONSE = {
function promptHydrateRT({ function promptHydrateRT({
servicesManager, servicesManager,
rtDisplaySet, rtDisplaySet,
viewportIndex, viewportId,
toolGroupId = 'default', toolGroupId = 'default',
preHydrateCallbacks,
hydrateRTDisplaySet,
}) { }) {
const { uiViewportDialogService } = servicesManager.services; const { uiViewportDialogService } = servicesManager.services;
return new Promise(async function(resolve, reject) { return new Promise(async function (resolve, reject) {
const promptResult = await _askHydrate( const promptResult = await _askHydrate(uiViewportDialogService, viewportId);
uiViewportDialogService,
viewportIndex
);
if (promptResult === RESPONSE.HYDRATE_SEG) { if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateRTDisplaySet({ const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet, rtDisplaySet,
viewportIndex, viewportId,
toolGroupId, toolGroupId,
servicesManager, servicesManager,
}); });
@ -33,17 +36,17 @@ function promptHydrateRT({
}); });
} }
function _askHydrate(uiViewportDialogService, viewportIndex) { function _askHydrate(uiViewportDialogService, viewportId) {
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
const message = 'Do you want to open this Segmentation?'; const message = 'Do you want to open this Segmentation?';
const actions = [ const actions = [
{ {
type: 'secondary', type: ButtonEnums.type.secondary,
text: 'No', text: 'No',
value: RESPONSE.CANCEL, value: RESPONSE.CANCEL,
}, },
{ {
type: 'primary', type: ButtonEnums.type.primary,
text: 'Yes', text: 'Yes',
value: RESPONSE.HYDRATE_SEG, value: RESPONSE.HYDRATE_SEG,
}, },
@ -54,7 +57,7 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
}; };
uiViewportDialogService.show({ uiViewportDialogService.show({
viewportIndex, viewportId,
type: 'info', type: 'info',
message, message,
actions, actions,

View File

@ -1,17 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import OHIF, { utils } from '@ohif/core'; import OHIF, { utils } from '@ohif/core';
import { import { ViewportActionBar, useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
ViewportActionBar,
useViewportGrid,
LoadingIndicatorTotalPercent,
} from '@ohif/ui';
import _hydrateRTdisplaySet from '../utils/_hydrateRT';
import promptHydrateRT from '../utils/promptHydrateRT'; import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent'; import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup'; import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import _hydrateRTDisplaySet from '../utils/_hydrateRT';
const { formatDate } = utils; const { formatDate } = utils;
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup'; const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
@ -21,10 +15,10 @@ function OHIFCornerstoneRTViewport(props) {
children, children,
displaySets, displaySets,
viewportOptions, viewportOptions,
viewportIndex,
viewportLabel, viewportLabel,
servicesManager, servicesManager,
extensionManager, extensionManager,
commandsManager,
} = props; } = props;
const { const {
@ -35,7 +29,9 @@ function OHIFCornerstoneRTViewport(props) {
customizationService, customizationService,
} = servicesManager.services; } = servicesManager.services;
const toolGroupId = `${RT_TOOLGROUP_BASE_NAME}-${viewportIndex}`; const viewportId = viewportOptions.viewportId;
const toolGroupId = `${RT_TOOLGROUP_BASE_NAME}-${viewportId}`;
// RT viewport will always have a single display set // RT viewport will always have a single display set
if (displaySets.length > 1) { if (displaySets.length > 1) {
@ -66,12 +62,10 @@ function OHIFCornerstoneRTViewport(props) {
// refs // refs
const referencedDisplaySetRef = useRef(null); const referencedDisplaySetRef = useRef(null);
const { viewports, activeViewportIndex } = viewportGrid; const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet(); const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata( const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
referencedDisplaySet
);
referencedDisplaySetRef.current = { referencedDisplaySetRef.current = {
displaySet: referencedDisplaySet, displaySet: referencedDisplaySet,
@ -91,14 +85,27 @@ function OHIFCornerstoneRTViewport(props) {
setElement(null); setElement(null);
}; };
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportId }) => {
commandsManager.runCommand('storePresentation', {
viewportId,
});
});
}, [viewportGrid]);
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [rtDisplaySet],
viewportId,
});
};
const getCornerstoneViewport = useCallback(() => { const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry( const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone' '@ohif/extension-cornerstone.viewportModule.cornerstone'
); );
const { const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
displaySet: referencedDisplaySet,
} = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment // Todo: jump to the center of the first segment
return ( return (
@ -115,7 +122,7 @@ function OHIFCornerstoneRTViewport(props) {
onElementDisabled={onElementDisabled} onElementDisabled={onElementDisabled}
></Component> ></Component>
); );
}, [viewportIndex, rtDisplaySet, toolGroupId]); }, [viewportId, rtDisplaySet, toolGroupId]);
const onSegmentChange = useCallback( const onSegmentChange = useCallback(
direction => { direction => {
@ -136,11 +143,7 @@ function OHIFCornerstoneRTViewport(props) {
newSelectedSegmentIndex = numberOfSegments - 1; newSelectedSegmentIndex = numberOfSegments - 1;
} }
segmentationService.jumpToSegmentCenter( segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
segmentationId,
newSelectedSegmentIndex,
toolGroupId
);
setSelectedSegment(newSelectedSegmentIndex); setSelectedSegment(newSelectedSegmentIndex);
}, },
[selectedSegment] [selectedSegment]
@ -153,31 +156,29 @@ function OHIFCornerstoneRTViewport(props) {
promptHydrateRT({ promptHydrateRT({
servicesManager, servicesManager,
viewportIndex, viewportId,
rtDisplaySet, rtDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateRTDisplaySet,
}).then(isHydrated => { }).then(isHydrated => {
if (isHydrated) { if (isHydrated) {
setIsHydrated(true); setIsHydrated(true);
} }
}); });
}, [servicesManager, viewportIndex, rtDisplaySet, rtIsLoading]); }, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
useEffect(() => { useEffect(() => {
const { unsubscribe } = segmentationService.subscribe( const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => { evt => {
if ( if (evt.rtDisplaySet.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) {
evt.rtDisplaySet.displaySetInstanceUID ===
rtDisplaySet.displaySetInstanceUID
) {
setRtIsLoading(false); setRtIsLoading(false);
} }
if (evt.overlappingSegments) { if (evt.overlappingSegments) {
uiNotificationService.show({ uiNotificationService.show({
title: 'Overlapping Segments', title: 'Overlapping Segments',
message: message: 'Overlapping segments detected which is not currently supported',
'Overlapping segments detected which is not currently supported',
type: 'warning', type: 'warning',
}); });
} }
@ -212,12 +213,10 @@ function OHIFCornerstoneRTViewport(props) {
const onDisplaySetsRemovedSubscription = displaySetService.subscribe( const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_REMOVED, displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => { ({ displaySetInstanceUIDs }) => {
const activeViewport = viewports[activeViewportIndex]; const activeViewport = viewports.get(activeViewportId);
if ( if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
) {
viewportGridService.setDisplaySetsForViewport({ viewportGridService.setDisplaySetsForViewport({
viewportIndex: activeViewportIndex, viewportId: activeViewportId,
displaySetInstanceUIDs: [], displaySetInstanceUIDs: [],
}); });
} }
@ -236,19 +235,13 @@ function OHIFCornerstoneRTViewport(props) {
return; return;
} }
toolGroup = createRTToolGroupAndAddTools( toolGroup = createRTToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
toolGroupService,
customizationService,
toolGroupId
);
setToolGroupCreated(true); setToolGroupCreated(true);
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup( segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
toolGroupId
);
toolGroupService.destroyToolGroup(toolGroupId); toolGroupService.destroyToolGroup(toolGroupId);
}; };
@ -259,9 +252,7 @@ function OHIFCornerstoneRTViewport(props) {
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup( segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
toolGroupId
);
referencedDisplaySetRef.current = null; referencedDisplaySetRef.current = null;
}; };
}, [rtDisplaySet]); }, [rtDisplaySet]);
@ -282,7 +273,7 @@ function OHIFCornerstoneRTViewport(props) {
return ( return (
child && child &&
React.cloneElement(child, { React.cloneElement(child, {
viewportIndex, viewportId,
key: index, key: index,
}) })
); );
@ -303,10 +294,16 @@ function OHIFCornerstoneRTViewport(props) {
} = referencedDisplaySetRef.current.metadata; } = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => { const onStatusClick = async () => {
const isHydrated = await _hydrateRTDisplaySet({ // 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, rtDisplaySet,
viewportIndex, viewportId,
servicesManager,
}); });
setIsHydrated(isHydrated); setIsHydrated(isHydrated);
@ -333,26 +330,22 @@ function OHIFCornerstoneRTViewport(props) {
currentSeries: SeriesNumber, currentSeries: SeriesNumber,
seriesDescription: `RT Viewport ${SeriesDescription}`, seriesDescription: `RT Viewport ${SeriesDescription}`,
patientInformation: { patientInformation: {
patientName: PatientName patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientSex: PatientSex || '', patientSex: PatientSex || '',
patientAge: PatientAge || '', patientAge: PatientAge || '',
MRN: PatientID || '', MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '', thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
spacing: spacing:
SpacingBetweenSlices !== undefined SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
? `${SpacingBetweenSlices.toFixed(2)}mm`
: '',
scanner: ManufacturerModelName || '', scanner: ManufacturerModelName || '',
}, },
}} }}
/> />
<div className="relative flex flex-row w-full h-full overflow-hidden"> <div className="relative flex h-full w-full flex-row overflow-hidden">
{rtIsLoading && ( {rtIsLoading && (
<LoadingIndicatorTotalPercent <LoadingIndicatorTotalPercent
className="w-full h-full" className="h-full w-full"
totalNumbers={processingProgress.totalSegments} totalNumbers={processingProgress.totalSegments}
percentComplete={processingProgress.percentComplete} percentComplete={processingProgress.percentComplete}
loadingText="Loading RTSTRUCT..." loadingText="Loading RTSTRUCT..."
@ -367,7 +360,7 @@ function OHIFCornerstoneRTViewport(props) {
OHIFCornerstoneRTViewport.propTypes = { OHIFCornerstoneRTViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object), displaySets: PropTypes.arrayOf(PropTypes.object),
viewportIndex: PropTypes.number.isRequired, viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object, dataSource: PropTypes.object,
children: PropTypes.node, children: PropTypes.node,
customProps: PropTypes.object, customProps: PropTypes.object,

View File

@ -13,25 +13,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
case true: case true:
StatusIcon = () => <Icon name="status-alert" />; StatusIcon = () => <Icon name="status-alert" />;
ToolTipMessage = () => ( ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
<div>This Segmentation is loaded in the segmentation panel</div>
);
break; break;
case false: case false:
StatusIcon = () => <Icon name="status-untracked" />; StatusIcon = () => (
<Icon
className="text-aqua-pale"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>; ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
} }
const StatusArea = () => ( const StatusArea = () => (
<div className="flex h-6 leading-6 cursor-default text-sm text-white"> <div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100"> <div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon /> <StatusIcon />
<span className="ml-1">RTSTRUCT</span> <span className="ml-1">RTSTRUCT</span>
</div> </div>
{!isHydrated && ( {!isHydrated && (
<div <div
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light" className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none // Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick} onMouseUp={onStatusClick}
> >
@ -44,7 +47,10 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
return ( return (
<> <>
{ToolTipMessage && ( {ToolTipMessage && (
<Tooltip content={<ToolTipMessage />} position="bottom-left"> <Tooltip
content={<ToolTipMessage />}
position="bottom-left"
>
<StatusArea /> <StatusArea />
</Tooltip> </Tooltip>
)} )}

View File

@ -40,21 +40,15 @@ module.exports = (env, argv) => {
libraryTarget: 'umd', libraryTarget: 'umd',
filename: pkg.main, filename: pkg.main,
}, },
externals: [ externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
/\b(vtk.js)/,
/\b(dcmjs)/,
/\b(gl-matrix)/,
/^@ohif/,
/^@cornerstonejs/,
],
plugins: [ plugins: [
new webpack.optimize.LimitChunkCountPlugin({ new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1, maxChunks: 1,
}), }),
// new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
// filename: `./dist/${outputName}.css`, filename: `./dist/${outputName}.css`,
// chunkFilename: `./dist/${outputName}.css`, chunkFilename: `./dist/${outputName}.css`,
// }), }),
], ],
}); });
}; };

View File

@ -0,0 +1,460 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10)
### Bug Fixes
* **voi:** should publish voi change event on reset ([#3707](https://github.com/OHIF/Viewers/issues/3707)) ([52f34c6](https://github.com/OHIF/Viewers/commit/52f34c64d014f433ec1661a39b47e7fb27f15332))
# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09)
### Bug Fixes
* **modality unit:** fix the modality unit per target via upgrade of cs3d ([#3706](https://github.com/OHIF/Viewers/issues/3706)) ([0a42d57](https://github.com/OHIF/Viewers/commit/0a42d573bbca7f2551a831a46d3aa6b56674a580))
# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06)
### Features
* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65))
# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06)
### Bug Fixes
* **segmentation scroll:** and hydration bugs ([#3701](https://github.com/OHIF/Viewers/issues/3701)) ([1fd98d9](https://github.com/OHIF/Viewers/commit/1fd98d922094d10fe0c6e9df726314ec9fce49e8))
# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04)
### Bug Fixes
* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155))
# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03)
### Bug Fixes
* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e))
# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22)
### Features
* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1))
* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378))
# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
### Features
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg

View File

@ -10,7 +10,7 @@ module.exports = {
modules: 'commonjs', modules: 'commonjs',
debug: false, debug: false,
}, },
"@babel/preset-typescript", '@babel/preset-typescript',
], ],
'@babel/preset-react', '@babel/preset-react',
], ],
@ -26,7 +26,7 @@ module.exports = {
// WebPack handles ES6 --> Target Syntax // WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }], ['@babel/preset-env', { modules: false }],
'@babel/preset-react', '@babel/preset-react',
"@babel/preset-typescript", '@babel/preset-typescript',
], ],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
}, },
@ -35,7 +35,7 @@ module.exports = {
// WebPack handles ES6 --> Target Syntax // WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }], ['@babel/preset-env', { modules: false }],
'@babel/preset-react', '@babel/preset-react',
"@babel/preset-typescript", '@babel/preset-typescript',
], ],
plugins: ['react-hot-loader/babel'], plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],

View File

@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-cornerstone-dicom-seg", "name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.6.0", "version": "3.7.0-beta.110",
"description": "DICOM SEG read workflow", "description": "DICOM SEG read workflow",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@ -31,10 +31,10 @@
"start": "yarn run dev" "start": "yarn run dev"
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.6.0", "@ohif/core": "3.7.0-beta.110",
"@ohif/extension-cornerstone": "3.6.0", "@ohif/extension-cornerstone": "3.7.0-beta.110",
"@ohif/extension-default": "3.6.0", "@ohif/extension-default": "3.7.0-beta.110",
"@ohif/i18n": "3.6.0", "@ohif/i18n": "3.7.0-beta.110",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
@ -44,6 +44,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.20.3",
"@cornerstonejs/tools": "^1.20.3",
"@kitware/vtk.js": "27.3.1",
"react-color": "^2.19.3" "react-color": "^2.19.3"
} }
} }

View File

@ -0,0 +1,435 @@
import dcmjs from 'dcmjs';
import { createReportDialogPrompt } from '@ohif/extension-default';
import { ServicesManager, Types } from '@ohif/core';
import { cache, metaData } from '@cornerstonejs/core';
import {
segmentation as cornerstoneToolsSegmentation,
Enums as cornerstoneToolsEnums,
} from '@cornerstonejs/tools';
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
import { classes, DicomMetadataStore } from '@ohif/core';
import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchingSquares';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
} from './utils/hydrationUtils';
const { datasetToBlob } = dcmjs.data;
const {
Cornerstone3D: {
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation },
},
} = adaptersSEG;
const {
Cornerstone3D: {
RTSS: { generateRTSSFromSegmentations },
},
} = adaptersRT;
const { downloadDICOMData } = helpers;
const commandsModule = ({
servicesManager,
extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const {
uiNotificationService,
segmentationService,
uiDialogService,
displaySetService,
viewportGridService,
} = (servicesManager as ServicesManager).services;
const actions = {
/**
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
* This function evaluates viewports based on their compatibility with the provided segmentation's
* frame of reference UID and appends them to the updated list if they should render the segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the ID of the viewport to be updated.
* @param params.servicesManager - The services manager
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
*
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
*/
getUpdatedViewportsForSegmentation,
/**
* Creates an empty segmentation for a specified viewport.
* It first checks if the display set associated with the viewport is reconstructable.
* If not, it raises a notification error. Otherwise, it creates a new segmentation
* for the display set after handling the necessary steps for making the viewport
* a volume viewport first
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the target viewport ID.
*
*/
createEmptySegmentationForViewport: async ({ viewportId }) => {
const viewport = getTargetViewport({ viewportId, viewportGridService });
// Todo: add support for multiple display sets
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!displaySet.isReconstructable) {
uiNotificationService.show({
title: 'Segmentation',
message: 'Segmentation is not supported for non-reconstructible displaysets yet',
type: 'error',
});
return;
}
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
loadFn: async () => {
const currentSegmentations = segmentationService.getSegmentations();
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
displaySetInstanceUID,
{ label: `Segmentation ${currentSegmentations.length + 1}` }
);
const toolGroupId = viewport.viewportOptions.toolGroupId;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId
);
// Add only one segment for now
segmentationService.addSegment(segmentationId, {
toolGroupId,
segmentIndex: 1,
properties: {
label: 'Segment 1',
},
});
return segmentationId;
},
});
},
/**
* Loads segmentations for a specified viewport.
* The function prepares the viewport for rendering, then loads the segmentation details.
* Additionally, if the segmentation has scalar data, it is set for the corresponding label map volume.
*
* @param {Object} params - Parameters for the function.
* @param params.segmentations - Array of segmentations to be loaded.
* @param params.viewportId - the target viewport ID.
*
*/
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
loadFn: async () => {
// Todo: handle adding more than one segmentation
const viewport = getTargetViewport({ viewportId, viewportGridService });
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const segmentation = segmentations[0];
const segmentationId = segmentation.id;
const label = segmentation.label;
const segments = segmentation.segments;
delete segmentation.segments;
await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, {
segmentationId,
label,
});
if (segmentation.scalarData) {
const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId);
labelmapVolume.scalarData.set(segmentation.scalarData);
}
segmentationService.addOrUpdateSegmentation(segmentation);
const toolGroupId = viewport.viewportOptions.toolGroupId;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId
);
segments.forEach(segment => {
if (segment === null) {
return;
}
segmentationService.addSegment(segmentationId, {
segmentIndex: segment.segmentIndex,
toolGroupId,
properties: {
color: segment.color,
label: segment.label,
opacity: segment.opacity,
isLocked: segment.isLocked,
visibility: segment.isVisible,
active: segmentation.activeSegmentIndex === segment.segmentIndex,
},
});
});
if (segmentation.centroidsIJK) {
segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK);
}
return segmentationId;
},
});
},
/**
* Loads segmentation display sets for a specified viewport.
* Depending on the modality of the display set (SEG or RTSTRUCT),
* it chooses the appropriate service function to create
* the segmentation for the display set.
* The function then prepares the viewport for rendering segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded.
* @param params.displaySets - Array of display sets to be loaded for segmentation.
*
*/
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
// Todo: handle adding more than one segmentation
const displaySet = displaySets[0];
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
referencedDisplaySetInstanceUID: displaySet.referencedDisplaySetInstanceUID,
loadFn: async () => {
const segDisplaySet = displaySet;
const suppressEvents = false;
const serviceFunction =
segDisplaySet.Modality === 'SEG'
? 'createSegmentationForSEGDisplaySet'
: 'createSegmentationForRTDisplaySet';
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
return segmentationId;
},
});
},
/**
* Generates a segmentation from a given segmentation ID.
* This function retrieves the associated segmentation and
* its referenced volume, extracts label maps from the
* segmentation volume, and produces segmentation data
* alongside associated metadata.
*
* @param {Object} params - Parameters for the function.
* @param params.segmentationId - ID of the segmentation to be generated.
* @param params.options - Optional configuration for the generation process.
*
* @returns Returns the generated segmentation data.
*/
generateSegmentation: ({ segmentationId, options = {} }) => {
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
const { referencedVolumeId } = segmentation.representationData.LABELMAP;
const segmentationVolume = cache.getVolume(segmentationId);
const referencedVolume = cache.getVolume(referencedVolumeId);
const referencedImages = referencedVolume.getCornerstoneImages();
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume);
// Generate fake metadata as an example
labelmapObj.metadata = [];
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
labelmapObj.segmentsOnLabelmap.forEach(segmentIndex => {
// segmentation service already has a color for each segment
const segment = segmentationInOHIF?.segments[segmentIndex];
const { label, color } = segment;
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
color.slice(0, 3).map(value => value / 255)
).map(value => Math.round(value));
const segmentMetadata = {
SegmentNumber: segmentIndex.toString(),
SegmentLabel: label,
SegmentAlgorithmType: 'MANUAL',
SegmentAlgorithmName: 'OHIF Brush',
RecommendedDisplayCIELabValue,
SegmentedPropertyCategoryCodeSequence: {
CodeValue: 'T-D0050',
CodingSchemeDesignator: 'SRT',
CodeMeaning: 'Tissue',
},
SegmentedPropertyTypeCodeSequence: {
CodeValue: 'T-D0050',
CodingSchemeDesignator: 'SRT',
CodeMeaning: 'Tissue',
},
};
labelmapObj.metadata[segmentIndex] = segmentMetadata;
});
const generatedSegmentation = generateSegmentation(
referencedImages,
labelmapObj,
metaData,
options
);
return generatedSegmentation;
},
/**
* Downloads a segmentation based on the provided segmentation ID.
* This function retrieves the associated segmentation and
* uses it to generate the corresponding DICOM dataset, which
* is then downloaded with an appropriate filename.
*
* @param {Object} params - Parameters for the function.
* @param params.segmentationId - ID of the segmentation to be downloaded.
*
*/
downloadSegmentation: ({ segmentationId }) => {
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
const generatedSegmentation = actions.generateSegmentation({
segmentationId,
});
downloadDICOMData(generatedSegmentation.dataset, `${segmentationInOHIF.label}`);
},
/**
* Stores a segmentation based on the provided segmentationId into a specified data source.
* The SeriesDescription is derived from user input or defaults to the segmentation label,
* and in its absence, defaults to 'Research Derived Series'.
*
* @param {Object} params - Parameters for the function.
* @param params.segmentationId - ID of the segmentation to be stored.
* @param params.dataSource - Data source where the generated segmentation will be stored.
*
* @returns {Object|void} Returns the naturalized report if successfully stored,
* otherwise throws an error.
*/
storeSegmentation: async ({ segmentationId, dataSource }) => {
const promptResult = await createReportDialogPrompt(uiDialogService, {
extensionManager,
});
if (promptResult.action !== 1 && promptResult.value) {
return;
}
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation) {
throw new Error('No segmentation found');
}
const { label } = segmentation;
const SeriesDescription = promptResult.value || label || 'Research Derived Series';
const generatedData = actions.generateSegmentation({
segmentationId,
options: {
SeriesDescription,
},
});
if (!generatedData || !generatedData.dataset) {
throw new Error('Error during segmentation generation');
}
const { dataset: naturalizedReport } = generatedData;
await dataSource.store.dicom(naturalizedReport);
// The "Mode" route listens for DicomMetadataStore changes
// When a new instance is added, it listens and
// automatically calls makeDisplaySets
// add the information for where we stored it to the instance as well
naturalizedReport.wadoRoot = dataSource.getConfig().wadoRoot;
DicomMetadataStore.addInstances([naturalizedReport], true);
return naturalizedReport;
},
/**
* Converts segmentations into RTSS for download.
* This sample function retrieves all segentations and passes to
* cornerstone tool adapter to convert to DICOM RTSS format. It then
* converts dataset to downloadable blob.
*
*/
downloadRTSS: ({ segmentationId }) => {
const segmentations = segmentationService.getSegmentation(segmentationId);
const vtkUtils = {
vtkImageMarchingSquares,
vtkDataArray,
vtkImageData,
};
const RTSS = generateRTSSFromSegmentations(
segmentations,
classes.MetadataProvider,
DicomMetadataStore,
cache,
cornerstoneToolsEnums,
vtkUtils
);
try {
const reportBlob = datasetToBlob(RTSS);
//Create a URL for the binary.
const objectUrl = URL.createObjectURL(reportBlob);
window.location.assign(objectUrl);
} catch (e) {
console.warn(e);
}
},
};
const definitions = {
getUpdatedViewportsForSegmentation: {
commandFn: actions.getUpdatedViewportsForSegmentation,
},
loadSegmentationDisplaySetsForViewport: {
commandFn: actions.loadSegmentationDisplaySetsForViewport,
},
loadSegmentationsForViewport: {
commandFn: actions.loadSegmentationsForViewport,
},
createEmptySegmentationForViewport: {
commandFn: actions.createEmptySegmentationForViewport,
},
generateSegmentation: {
commandFn: actions.generateSegmentation,
},
downloadSegmentation: {
commandFn: actions.downloadSegmentation,
},
storeSegmentation: {
commandFn: actions.storeSegmentation,
},
downloadRTSS: {
commandFn: actions.downloadRTSS,
},
};
return {
actions,
definitions,
};
};
export default commandsModule;

View File

@ -5,7 +5,6 @@ const segProtocol: Types.HangingProtocol.Protocol = {
// Don't store this hanging protocol as it applies to the currently active // Don't store this hanging protocol as it applies to the currently active
// display set by default // display set by default
// cacheId: null, // cacheId: null,
hasUpdatedPriorsInformation: false,
name: 'Segmentations', name: 'Segmentations',
// Just apply this one when specifically listed // Just apply this one when specifically listed
protocolMatchingRules: [], protocolMatchingRules: [],

View File

@ -0,0 +1,70 @@
import React from 'react';
import { useAppConfig } from '@state';
import PanelSegmentation from './panels/PanelSegmentation';
import SegmentationToolbox from './panels/SegmentationToolbox';
const getPanelModule = ({ commandsManager, servicesManager, extensionManager, configuration }) => {
const { customizationService } = servicesManager.services;
const wrappedPanelSegmentation = configuration => {
const [appConfig] = useAppConfig();
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing || disableEditingForMode?.value,
}}
/>
);
};
const wrappedPanelSegmentationWithTools = configuration => {
const [appConfig] = useAppConfig();
return (
<>
<SegmentationToolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
}}
/>
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
}}
/>
</>
);
};
return [
{
name: 'panelSegmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentation,
},
{
name: 'panelSegmentationWithTools',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationWithTools,
},
];
};
export default getPanelModule;

View File

@ -1,21 +1,15 @@
import vtkMath from '@kitware/vtk.js/Common/Core/Math';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id'; import { SOPClassHandlerId } from './id';
import dcmjs from 'dcmjs'; import { dicomlabToRGB } from './utils/dicomlabToRGB';
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4']; const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
let loadPromises = {}; let loadPromises = {};
function _getDisplaySetsFromSeries( function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
instances,
servicesManager,
extensionManager
) {
const instance = instances[0]; const instance = instances[0];
const { const {
@ -63,13 +57,13 @@ function _getDisplaySetsFromSeries(
const referencedSeriesSequence = instance.ReferencedSeriesSequence; const referencedSeriesSequence = instance.ReferencedSeriesSequence;
if (!referencedSeriesSequence) { if (!referencedSeriesSequence) {
throw new Error('ReferencedSeriesSequence is missing for the SEG'); console.error('ReferencedSeriesSequence is missing for the SEG');
return;
} }
const referencedSeries = referencedSeriesSequence[0]; const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence;
displaySet.referencedImages = displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.getReferenceDisplaySet = () => { displaySet.getReferenceDisplaySet = () => {
@ -84,8 +78,7 @@ function _getDisplaySetsFromSeries(
const referencedDisplaySet = referencedDisplaySets[0]; const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
referencedDisplaySet.displaySetInstanceUID;
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc. // Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID; displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
@ -118,17 +111,13 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
// We don't want to fire multiple loads, so we'll wait for the first to finish // We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers. // and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => { loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if ( if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) {
!segDisplaySet.segments || await _loadSegments({
Object.keys(segDisplaySet.segments).length === 0
) {
const segments = await _loadSegments(
extensionManager, extensionManager,
servicesManager,
segDisplaySet, segDisplaySet,
headers headers,
); });
segDisplaySet.segments = segments;
} }
const suppressEvents = true; const suppressEvents = true;
@ -147,152 +136,61 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
return loadPromises[SOPInstanceUID]; return loadPromises[SOPInstanceUID];
} }
async function _loadSegments(extensionManager, segDisplaySet, headers) { async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, headers }) {
const utilityModule = extensionManager.getModuleEntry( const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common' '@ohif/extension-cornerstone.utilityModule.common'
); );
const { segmentationService } = servicesManager.services;
const { dicomLoaderService } = utilityModule.exports; const { dicomLoaderService } = utilityModule.exports;
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise( const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
segDisplaySet,
null,
headers
);
const dicomData = DicomMessage.readFile(segArrayBuffer); const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
if (!Array.isArray(dataset.SegmentSequence)) { if (!cachedReferencedVolume) {
dataset.SegmentSequence = [dataset.SegmentSequence]; throw new Error(
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
);
} }
const segments = _getSegments(dataset); const { imageIds } = cachedReferencedVolume;
return segments;
// Todo: what should be defaults here
const tolerance = 0.001;
const skipOverlapping = true;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
percentComplete,
});
});
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
imageIds,
arrayBuffer,
metaData,
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
);
results.segMetadata.data.forEach((data, i) => {
if (i > 0) {
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
}
});
Object.assign(segDisplaySet, results);
} }
function _segmentationExists(segDisplaySet, segmentationService) { function _segmentationExists(segDisplaySet, segmentationService) {
// This should be abstracted with the CornerstoneCacheService // This should be abstracted with the CornerstoneCacheService
return segmentationService.getSegmentation( return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
segDisplaySet.displaySetInstanceUID
);
}
function _getPixelData(dataset, segments) {
let frameSize = Math.ceil((dataset.Rows * dataset.Columns) / 8);
let nextOffset = 0;
Object.keys(segments).forEach(segmentKey => {
const segment = segments[segmentKey];
segment.numberOfFrames = segment.functionalGroups.length;
segment.size = segment.numberOfFrames * frameSize;
segment.offset = nextOffset;
nextOffset = segment.offset + segment.size;
const packedSegment = dataset.PixelData[0].slice(
segment.offset,
nextOffset
);
segment.pixelData = dcmjs.data.BitArray.unpack(packedSegment);
segment.geometry = geometryFromFunctionalGroups(
dataset,
segment.functionalGroups
);
});
return segments;
}
function geometryFromFunctionalGroups(dataset, perFrame) {
let pixelMeasures =
dataset.SharedFunctionalGroupsSequence.PixelMeasuresSequence;
let planeOrientation =
dataset.SharedFunctionalGroupsSequence.PlaneOrientationSequence;
let planePosition = perFrame[0].PlanePositionSequence; // TODO: assume sorted frames!
const geometry = {};
// NB: DICOM PixelSpacing is defined as Row then Column,
// unlike ImageOrientationPatient
let spacingBetweenSlices = pixelMeasures.SpacingBetweenSlices;
if (!spacingBetweenSlices) {
if (pixelMeasures.SliceThickness) {
console.log('Using SliceThickness as SpacingBetweenSlices');
spacingBetweenSlices = pixelMeasures.SliceThickness;
}
}
geometry.spacing = [
pixelMeasures.PixelSpacing[1],
pixelMeasures.PixelSpacing[0],
spacingBetweenSlices,
].map(Number);
geometry.dimensions = [dataset.Columns, dataset.Rows, perFrame.length].map(
Number
);
let orientation = planeOrientation.ImageOrientationPatient.map(Number);
const columnStepToPatient = orientation.slice(0, 3);
const rowStepToPatient = orientation.slice(3, 6);
geometry.planeNormal = [];
vtkMath.cross(columnStepToPatient, rowStepToPatient, geometry.planeNormal);
let firstPosition = perFrame[0].PlanePositionSequence.ImagePositionPatient.map(
Number
);
let lastPosition = perFrame[
perFrame.length - 1
].PlanePositionSequence.ImagePositionPatient.map(Number);
geometry.sliceStep = [];
vtkMath.subtract(lastPosition, firstPosition, geometry.sliceStep);
vtkMath.normalize(geometry.sliceStep);
geometry.direction = columnStepToPatient
.concat(rowStepToPatient)
.concat(geometry.sliceStep);
geometry.origin = planePosition.ImagePositionPatient.map(Number);
return geometry;
}
function _getSegments(dataset) {
const segments = {};
dataset.SegmentSequence.forEach(segment => {
const cielab = segment.RecommendedDisplayCIELabValue;
const rgba = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
Math.round(x * 255)
);
rgba.push(255);
const segmentNumber = segment.SegmentNumber;
segments[segmentNumber] = {
color: rgba,
functionalGroups: [],
offset: null,
size: null,
pixelData: null,
label: segment.SegmentLabel,
};
});
// make a list of functional groups per segment
dataset.PerFrameFunctionalGroupsSequence.forEach(functionalGroup => {
const segmentNumber =
functionalGroup.SegmentIdentificationSequence.ReferencedSegmentNumber;
segments[segmentNumber].functionalGroups.push(functionalGroup);
});
return _getPixelData(dataset, segments);
} }
function getSopClassHandlerModule({ servicesManager, extensionManager }) { function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => { const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries( return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
instances,
servicesManager,
extensionManager
);
}; };
return [ return [

View File

@ -1,16 +1,14 @@
import { id } from './id'; import { id } from './id';
import React from 'react'; import React from 'react';
import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule'; import getSopClassHandlerModule from './getSopClassHandlerModule';
import PanelSegmentation from './panels/PanelSegmentation';
import getHangingProtocolModule from './getHangingProtocolModule'; import getHangingProtocolModule from './getHangingProtocolModule';
import getPanelModule from './getPanelModule';
import getCommandsModule from './commandsModule';
import preRegistration from './init';
const Component = React.lazy(() => { const Component = React.lazy(() => {
return import( return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport'
);
}); });
const OHIFCornerstoneSEGViewport = props => { const OHIFCornerstoneSEGViewport = props => {
@ -30,6 +28,7 @@ const extension = {
* You ID can be anything you want, but it should be unique. * You ID can be anything you want, but it should be unique.
*/ */
id, id,
preRegistration,
/** /**
* PanelModule should provide a list of panels that will be available in OHIF * PanelModule should provide a list of panels that will be available in OHIF
@ -37,31 +36,8 @@ const extension = {
* iconName, iconLabel, label, component} object. Example of a panel module * iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF. * is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/ */
getPanelModule: ({ getPanelModule,
servicesManager, getCommandsModule,
commandsManager,
extensionManager,
}): Types.Panel[] => {
const wrappedPanelSegmentation = () => {
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
/>
);
};
return [
{
name: 'panelSegmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentation,
},
];
},
getViewportModule({ servicesManager, extensionManager }) { getViewportModule({ servicesManager, extensionManager }) {
const ExtendedOHIFCornerstoneSEGViewport = props => { const ExtendedOHIFCornerstoneSEGViewport = props => {
@ -69,14 +45,13 @@ const extension = {
<OHIFCornerstoneSEGViewport <OHIFCornerstoneSEGViewport
servicesManager={servicesManager} servicesManager={servicesManager}
extensionManager={extensionManager} extensionManager={extensionManager}
commandsManager={commandsManager}
{...props} {...props}
/> />
); );
}; };
return [ return [{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport }];
{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport },
];
}, },
/** /**
* SopClassHandlerModule should provide a list of sop class handlers that will be * SopClassHandlerModule should provide a list of sop class handlers that will be
@ -88,4 +63,4 @@ const extension = {
getHangingProtocolModule, getHangingProtocolModule,
}; };
export default extension; export default extension;

View File

@ -0,0 +1,5 @@
import { addTool, BrushTool } from '@cornerstonejs/tools';
export default function init({ configuration = {} }): void {
addTool(BrushTool);
}

View File

@ -1,48 +1,28 @@
import { createReportAsync } from '@ohif/extension-default';
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SegmentationGroupTable } from '@ohif/ui'; import { SegmentationGroupTable, LegacyButtonGroup, LegacyButton } from '@ohif/ui';
import callInputDialog from './callInputDialog';
import callInputDialog from './callInputDialog';
import callColorPickerDialog from './colorPickerDialog';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export default function PanelSegmentation({ export default function PanelSegmentation({
servicesManager, servicesManager,
commandsManager, commandsManager,
extensionManager,
configuration,
}) { }) {
const { segmentationService, uiDialogService } = servicesManager.services; const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
const { t } = useTranslation('PanelSegmentation'); const { t } = useTranslation('PanelSegmentation');
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null); const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
const [segmentationConfiguration, setSegmentationConfiguration] = useState( const [segmentationConfiguration, setSegmentationConfiguration] = useState(
segmentationService.getConfiguration() segmentationService.getConfiguration()
); );
const [segmentations, setSegmentations] = useState(() => const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
segmentationService.getSegmentations()
);
const [isMinimized, setIsMinimized] = useState({});
const onToggleMinimizeSegmentation = useCallback(
id => {
setIsMinimized(prevState => ({
...prevState,
[id]: !prevState[id],
}));
},
[setIsMinimized]
);
// Only expand the last segmentation added to the list and collapse the rest
useEffect(() => {
const lastSegmentationId = segmentations[segmentations.length - 1]?.id;
if (lastSegmentationId) {
setIsMinimized(prevState => ({
...prevState,
[lastSegmentationId]: false,
}));
}
}, [segmentations, setIsMinimized]);
useEffect(() => { useEffect(() => {
// ~~ Subscription // ~~ Subscription
@ -67,6 +47,16 @@ export default function PanelSegmentation({
}; };
}, []); }, []);
const getToolGroupIds = segmentationId => {
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
return toolGroupIds;
};
const onSegmentationAdd = async () => {
commandsManager.runCommand('createEmptySegmentationForViewport');
};
const onSegmentationClick = (segmentationId: string) => { const onSegmentationClick = (segmentationId: string) => {
segmentationService.setActiveSegmentationForToolGroup(segmentationId); segmentationService.setActiveSegmentationForToolGroup(segmentationId);
}; };
@ -75,33 +65,19 @@ export default function PanelSegmentation({
segmentationService.remove(segmentationId); segmentationService.remove(segmentationId);
}; };
const getToolGroupIds = segmentationId => { const onSegmentAdd = segmentationId => {
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation( segmentationService.addSegment(segmentationId);
segmentationId
);
return toolGroupIds;
}; };
const onSegmentClick = (segmentationId, segmentIndex) => { const onSegmentClick = (segmentationId, segmentIndex) => {
segmentationService.setActiveSegmentForSegmentation( segmentationService.setActiveSegment(segmentationId, segmentIndex);
segmentationId,
segmentIndex
);
const toolGroupIds = getToolGroupIds(segmentationId); const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => { toolGroupIds.forEach(toolGroupId => {
// const toolGroupId = // const toolGroupId =
segmentationService.setActiveSegmentationForToolGroup( segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
segmentationId, segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
toolGroupId
);
segmentationService.jumpToSegmentCenter(
segmentationId,
segmentIndex,
toolGroupId
);
}); });
}; };
@ -116,11 +92,7 @@ export default function PanelSegmentation({
return; return;
} }
segmentationService.setSegmentLabelForSegmentation( segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
segmentationId,
segmentIndex,
label
);
}); });
}; };
@ -145,16 +117,34 @@ export default function PanelSegmentation({
}; };
const onSegmentColorClick = (segmentationId, segmentIndex) => { const onSegmentColorClick = (segmentationId, segmentIndex) => {
// Todo: Implement color picker later const segmentation = segmentationService.getSegmentation(segmentationId);
return;
const segment = segmentation.segments[segmentIndex];
const { color, opacity } = segment;
const rgbaColor = {
r: color[0],
g: color[1],
b: color[2],
a: opacity / 255.0,
};
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
if (actionId === 'cancel') {
return;
}
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
newRgbaColor.r,
newRgbaColor.g,
newRgbaColor.b,
newRgbaColor.a * 255.0,
]);
});
}; };
const onSegmentDelete = (segmentationId, segmentIndex) => { const onSegmentDelete = (segmentationId, segmentIndex) => {
// segmentationService.removeSegmentFromSegmentation( segmentationService.removeSegment(segmentationId, segmentIndex);
// segmentationId,
// segmentIndex
// );
console.warn('not implemented yet');
}; };
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => { const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
@ -174,6 +164,10 @@ export default function PanelSegmentation({
}); });
}; };
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
};
const onToggleSegmentationVisibility = segmentationId => { const onToggleSegmentationVisibility = segmentationId => {
segmentationService.toggleSegmentationVisibility(segmentationId); segmentationService.toggleSegmentationVisibility(segmentationId);
}; };
@ -188,47 +182,78 @@ export default function PanelSegmentation({
[segmentationService] [segmentationService]
); );
const onSegmentationDownload = segmentationId => {
commandsManager.runCommand('downloadSegmentation', {
segmentationId,
});
};
const storeSegmentation = async segmentationId => {
const datasources = extensionManager.getActiveDataSource();
const displaySetInstanceUIDs = await createReportAsync({
servicesManager,
getReport: () =>
commandsManager.runCommand('storeSegmentation', {
segmentationId,
dataSource: datasources[0],
}),
reportType: 'Segmentation',
});
// Show the exported report in the active viewport as read only (similar to SR)
if (displaySetInstanceUIDs) {
// clear the segmentation that we exported, similar to the storeMeasurement
// where we remove the measurements and prompt again the user if they would like
// to re-read the measurements in a SR read only viewport
segmentationService.remove(segmentationId);
viewportGridService.setDisplaySetsForViewport({
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs,
});
}
};
const onSegmentationDownloadRTSS = segmentationId => {
commandsManager.runCommand('downloadRTSS', {
segmentationId,
});
};
return ( return (
<div className="flex flex-col flex-auto min-h-0 justify-between mt-1"> <>
{/* show segmentation table */} <div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
{segmentations?.length ? (
<SegmentationGroupTable <SegmentationGroupTable
title={t('Segmentations')} title={t('Segmentations')}
showAddSegmentation={false}
segmentations={segmentations} segmentations={segmentations}
isMinimized={isMinimized} disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''} activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAdd}
onSegmentationClick={onSegmentationClick} onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete} onSegmentationDelete={onSegmentationDelete}
onSegmentationDownload={onSegmentationDownload}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
storeSegmentation={storeSegmentation}
onSegmentationEdit={onSegmentationEdit} onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick} onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit} onSegmentEdit={onSegmentEdit}
onSegmentAdd={onSegmentAdd}
onSegmentColorClick={onSegmentColorClick} onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete} onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility} onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentLock={onToggleSegmentLock}
onToggleSegmentationVisibility={onToggleSegmentationVisibility} onToggleSegmentationVisibility={onToggleSegmentationVisibility}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation} showDeleteSegment={true}
segmentationConfig={{ initialConfig: segmentationConfiguration }} segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value => setRenderOutline={value =>
_setSegmentationConfiguration( _setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
selectedSegmentationId,
'renderOutline',
value
)
} }
setOutlineOpacityActive={value => setOutlineOpacityActive={value =>
_setSegmentationConfiguration( _setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
selectedSegmentationId,
'outlineOpacity',
value
)
} }
setRenderFill={value => setRenderFill={value =>
_setSegmentationConfiguration( _setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
selectedSegmentationId,
'renderFill',
value
)
} }
setRenderInactiveSegmentations={value => setRenderInactiveSegmentations={value =>
_setSegmentationConfiguration( _setSegmentationConfiguration(
@ -238,29 +263,17 @@ export default function PanelSegmentation({
) )
} }
setOutlineWidthActive={value => setOutlineWidthActive={value =>
_setSegmentationConfiguration( _setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
selectedSegmentationId,
'outlineWidthActive',
value
)
} }
setFillAlpha={value => setFillAlpha={value =>
_setSegmentationConfiguration( _setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
selectedSegmentationId,
'fillAlpha',
value
)
} }
setFillAlphaInactive={value => setFillAlphaInactive={value =>
_setSegmentationConfiguration( _setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
selectedSegmentationId,
'fillAlphaInactive',
value
)
} }
/> />
) : null} </div>
</div> </>
); );
} }

View File

@ -0,0 +1,405 @@
import React, { useCallback, useEffect, useState, useReducer } from 'react';
import { AdvancedToolbox, InputDoubleRange, useViewportGrid } from '@ohif/ui';
import { Types } from '@ohif/extension-cornerstone';
import { utilities } from '@cornerstonejs/tools';
const { segmentation: segmentationUtils } = utilities;
const TOOL_TYPES = {
CIRCULAR_BRUSH: 'CircularBrush',
SPHERE_BRUSH: 'SphereBrush',
CIRCULAR_ERASER: 'CircularEraser',
SPHERE_ERASER: 'SphereEraser',
CIRCLE_SHAPE: 'CircleScissor',
RECTANGLE_SHAPE: 'RectangleScissor',
SPHERE_SHAPE: 'SphereScissor',
THRESHOLD_CIRCULAR_BRUSH: 'ThresholdCircularBrush',
THRESHOLD_SPHERE_BRUSH: 'ThresholdSphereBrush',
};
const ACTIONS = {
SET_TOOL_CONFIG: 'SET_TOOL_CONFIG',
SET_ACTIVE_TOOL: 'SET_ACTIVE_TOOL',
};
const initialState = {
Brush: {
brushSize: 15,
mode: 'CircularBrush', // Can be 'CircularBrush' or 'SphereBrush'
},
Eraser: {
brushSize: 15,
mode: 'CircularEraser', // Can be 'CircularEraser' or 'SphereEraser'
},
Shapes: {
brushSize: 15,
mode: 'CircleScissor', // E.g., 'CircleScissor', 'RectangleScissor', or 'SphereScissor'
},
ThresholdBrush: {
brushSize: 15,
thresholdRange: [-500, 500],
},
activeTool: null,
};
function toolboxReducer(state, action) {
switch (action.type) {
case ACTIONS.SET_TOOL_CONFIG:
const { tool, config } = action.payload;
return {
...state,
[tool]: {
...state[tool],
...config,
},
};
case ACTIONS.SET_ACTIVE_TOOL:
return { ...state, activeTool: action.payload };
default:
return state;
}
}
function SegmentationToolbox({ servicesManager, extensionManager }) {
const { toolbarService, segmentationService, toolGroupService } =
servicesManager.services as Types.CornerstoneServices;
const [viewportGrid] = useViewportGrid();
const { viewports, activeViewportId } = viewportGrid;
const [toolsEnabled, setToolsEnabled] = useState(false);
const [state, dispatch] = useReducer(toolboxReducer, initialState);
const updateActiveTool = useCallback(() => {
if (!viewports?.size || activeViewportId === undefined) {
return;
}
const viewport = viewports.get(activeViewportId);
if (!viewport) {
return;
}
dispatch({
type: ACTIONS.SET_ACTIVE_TOOL,
payload: toolGroupService.getActiveToolForViewport(viewport.viewportId),
});
}, [activeViewportId, viewports, toolGroupService, dispatch]);
const setToolActive = useCallback(
toolName => {
toolbarService.recordInteraction({
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName,
},
},
],
});
dispatch({ type: ACTIONS.SET_ACTIVE_TOOL, payload: toolName });
},
[toolbarService, dispatch]
);
/**
* sets the tools enabled IF there are segmentations
*/
useEffect(() => {
const events = [
segmentationService.EVENTS.SEGMENTATION_ADDED,
segmentationService.EVENTS.SEGMENTATION_UPDATED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
];
const unsubscriptions = [];
events.forEach(event => {
const { unsubscribe } = segmentationService.subscribe(event, () => {
const segmentations = segmentationService.getSegmentations();
const activeSegmentation = segmentations?.find(seg => seg.isActive);
setToolsEnabled(activeSegmentation?.segmentCount > 0);
});
unsubscriptions.push(unsubscribe);
});
updateActiveTool();
return () => {
unsubscriptions.forEach(unsubscribe => unsubscribe());
};
}, [activeViewportId, viewports, segmentationService, updateActiveTool]);
/**
* Update the active tool when the toolbar state changes
*/
useEffect(() => {
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
updateActiveTool();
}
);
return () => {
unsubscribe();
};
}, [toolbarService, updateActiveTool]);
useEffect(() => {
// if the active tool is not a brush tool then do nothing
if (!Object.values(TOOL_TYPES).includes(state.activeTool)) {
return;
}
// if the tool is Segmentation and it is enabled then do nothing
if (toolsEnabled) {
return;
}
// if the tool is Segmentation and it is disabled, then switch
// back to the window level tool to not confuse the user when no
// segmentation is active or when there is no segment in the segmentation
setToolActive('WindowLevel');
}, [toolsEnabled, state.activeTool, setToolActive]);
const updateBrushSize = useCallback(
(toolName, brushSize) => {
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
});
},
[toolGroupService]
);
const onBrushSizeChange = useCallback(
(valueAsStringOrNumber, toolCategory) => {
const value = Number(valueAsStringOrNumber);
_getToolNamesFromCategory(toolCategory).forEach(toolName => {
updateBrushSize(toolName, value);
});
dispatch({
type: ACTIONS.SET_TOOL_CONFIG,
payload: {
tool: toolCategory,
config: { brushSize: value },
},
});
},
[toolGroupService, dispatch]
);
const handleRangeChange = useCallback(
newRange => {
if (
newRange[0] === state.ThresholdBrush.thresholdRange[0] &&
newRange[1] === state.ThresholdBrush.thresholdRange[1]
) {
return;
}
const toolNames = _getToolNamesFromCategory('ThresholdBrush');
toolNames.forEach(toolName => {
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
toolGroup.setToolConfiguration(toolName, {
strategySpecificConfiguration: {
THRESHOLD_INSIDE_CIRCLE: {
threshold: newRange,
},
},
});
});
});
dispatch({
type: ACTIONS.SET_TOOL_CONFIG,
payload: {
tool: 'ThresholdBrush',
config: { thresholdRange: newRange },
},
});
},
[toolGroupService, dispatch, state.ThresholdBrush.thresholdRange]
);
return (
<AdvancedToolbox
title="Segmentation Tools"
items={[
{
name: 'Brush',
icon: 'icon-tool-brush',
disabled: !toolsEnabled,
active:
state.activeTool === TOOL_TYPES.CIRCULAR_BRUSH ||
state.activeTool === TOOL_TYPES.SPHERE_BRUSH,
onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_BRUSH),
options: [
{
name: 'Radius (mm)',
id: 'brush-radius',
type: 'range',
min: 0.5,
max: 99.5,
value: state.Brush.brushSize,
step: 0.5,
onChange: value => onBrushSizeChange(value, 'Brush'),
},
{
name: 'Mode',
type: 'radio',
id: 'brush-mode',
value: state.Brush.mode,
values: [
{ value: TOOL_TYPES.CIRCULAR_BRUSH, label: 'Circle' },
{ value: TOOL_TYPES.SPHERE_BRUSH, label: 'Sphere' },
],
onChange: value => setToolActive(value),
},
],
},
{
name: 'Eraser',
icon: 'icon-tool-eraser',
disabled: !toolsEnabled,
active:
state.activeTool === TOOL_TYPES.CIRCULAR_ERASER ||
state.activeTool === TOOL_TYPES.SPHERE_ERASER,
onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_ERASER),
options: [
{
name: 'Radius (mm)',
type: 'range',
id: 'eraser-radius',
min: 0.5,
max: 99.5,
value: state.Eraser.brushSize,
step: 0.5,
onChange: value => onBrushSizeChange(value, 'Eraser'),
},
{
name: 'Mode',
type: 'radio',
id: 'eraser-mode',
value: state.Eraser.mode,
values: [
{ value: TOOL_TYPES.CIRCULAR_ERASER, label: 'Circle' },
{ value: TOOL_TYPES.SPHERE_ERASER, label: 'Sphere' },
],
onChange: value => setToolActive(value),
},
],
},
{
name: 'Shapes',
icon: 'icon-tool-shape',
disabled: !toolsEnabled,
active:
state.activeTool === TOOL_TYPES.CIRCLE_SHAPE ||
state.activeTool === TOOL_TYPES.RECTANGLE_SHAPE ||
state.activeTool === TOOL_TYPES.SPHERE_SHAPE,
onClick: () => setToolActive(TOOL_TYPES.CIRCLE_SHAPE),
options: [
{
name: 'Mode',
type: 'radio',
value: state.Shapes.mode,
id: 'shape-mode',
values: [
{ value: TOOL_TYPES.CIRCLE_SHAPE, label: 'Circle' },
{ value: TOOL_TYPES.RECTANGLE_SHAPE, label: 'Rectangle' },
{ value: TOOL_TYPES.SPHERE_SHAPE, label: 'Sphere' },
],
onChange: value => setToolActive(value),
},
],
},
{
name: 'Threshold Tool',
icon: 'icon-tool-threshold',
disabled: !toolsEnabled,
active:
state.activeTool === TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH ||
state.activeTool === TOOL_TYPES.THRESHOLD_SPHERE_BRUSH,
onClick: () => setToolActive(TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH),
options: [
{
name: 'Radius (mm)',
id: 'threshold-radius',
type: 'range',
min: 0.5,
max: 99.5,
value: state.ThresholdBrush.brushSize,
step: 0.5,
onChange: value => onBrushSizeChange(value, 'ThresholdBrush'),
},
{
name: 'Mode',
type: 'radio',
id: 'threshold-mode',
value: state.activeTool,
values: [
{ value: TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH, label: 'Circle' },
{ value: TOOL_TYPES.THRESHOLD_SPHERE_BRUSH, label: 'Sphere' },
],
onChange: value => setToolActive(value),
},
{
type: 'custom',
id: 'segmentation-threshold-range',
children: () => {
return (
<div>
<div className="bg-secondary-light h-[1px]"></div>
<div className="mt-1 text-[13px] text-white">Threshold</div>
<InputDoubleRange
values={state.ThresholdBrush.thresholdRange}
onChange={handleRangeChange}
minValue={-1000}
maxValue={1000}
step={1}
showLabel={true}
allowNumberEdit={true}
showAdjustmentArrows={false}
/>
</div>
);
},
},
],
},
]}
/>
);
}
function _getToolNamesFromCategory(category) {
let toolNames = [];
switch (category) {
case 'Brush':
toolNames = ['CircularBrush', 'SphereBrush'];
break;
case 'Eraser':
toolNames = ['CircularEraser', 'SphereEraser'];
break;
case 'ThresholdBrush':
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'];
break;
default:
break;
}
return toolNames;
}
export default SegmentationToolbox;

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Input, Dialog } from '@ohif/ui'; import { Input, Dialog, ButtonEnums } from '@ohif/ui';
function callInputDialog(uiDialogService, label, callback) { function callInputDialog(uiDialogService, label, callback) {
const dialogId = 'enter-segment-label'; const dialogId = 'enter-segment-label';
@ -29,8 +29,8 @@ function callInputDialog(uiDialogService, label, callback) {
noCloseButton: true, noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }), onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [ actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' }, { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Confirm', type: 'secondary' }, { id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
], ],
onSubmit: onSubmitHandler, onSubmit: onSubmitHandler,
body: ({ value, setValue }) => { body: ({ value, setValue }) => {
@ -39,7 +39,7 @@ function callInputDialog(uiDialogService, label, callback) {
label="Enter the segment label" label="Enter the segment label"
labelClassName="text-white text-[14px] leading-[1.2]" labelClassName="text-white text-[14px] leading-[1.2]"
autoFocus autoFocus
className="bg-black border-primary-main" className="border-primary-main bg-black"
type="text" type="text"
value={value.label} value={value.label}
onChange={event => { onChange={event => {

View File

@ -0,0 +1,3 @@
.chrome-picker {
background: #090c29 !important;
}

View File

@ -0,0 +1,58 @@
import React from 'react';
import { Dialog } from '@ohif/ui';
import { ChromePicker } from 'react-color';
import './colorPickerDialog.css';
function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
const dialogId = 'pick-color';
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
callback(value.rgbaColor, action.id);
break;
case 'cancel':
callback('', action.id);
break;
}
uiDialogService.dismiss({ id: dialogId });
};
if (uiDialogService) {
uiDialogService.create({
id: dialogId,
centralize: true,
isDraggable: false,
showOverlay: true,
content: Dialog,
contentProps: {
title: 'Segment Color',
value: { rgbaColor },
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
const handleChange = color => {
setValue({ rgbaColor: color.rgb });
};
return (
<ChromePicker
color={value.rgbaColor}
onChange={handleChange}
presetColors={[]}
width={300}
/>
);
},
},
});
}
}
export default callColorPickerDialog;

View File

@ -1,69 +0,0 @@
async function _hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,
servicesManager,
}) {
const {
segmentationService,
hangingProtocolService,
viewportGridService,
} = servicesManager.services;
const displaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
let segmentationId = null;
// We need the hydration to notify panels about the new segmentation added
const suppressEvents = false;
segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
segDisplaySet,
segmentationId,
suppressEvents
);
segmentationService.hydrateSegmentation(segDisplaySet.displaySetInstanceUID);
const { viewports } = viewportGridService.getState();
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportIndex,
displaySetInstanceUID
);
// Todo: fix this after we have a better way for stack viewport segmentations
// check every viewport in the viewports to see if the displaySetInstanceUID
// is being displayed, if so we need to update the viewport to use volume viewport
// (if already is not using it) since Cornerstone3D currently only supports
// volume viewport for segmentation
viewports.forEach((viewport, index) => {
if (index === viewportIndex) {
return;
}
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewport.displaySetInstanceUIDs,
segDisplaySet.displaySetInstanceUID
);
if (shouldDisplaySeg) {
updatedViewports.push({
viewportIndex: index,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
initialImageOptions: {
preset: 'middle',
},
},
});
}
});
// Do the entire update at once
viewportGridService.setDisplaySetsForViewports(updatedViewports);
return true;
}
export default _hydrateSEGDisplaySet;

View File

@ -0,0 +1,14 @@
import dcmjs from 'dcmjs';
/**
* Converts a CIELAB color to an RGB color using the dcmjs library.
* @param cielab - The CIELAB color to convert.
* @returns The RGB color as an array of three integers between 0 and 255.
*/
function dicomlabToRGB(cielab: number[]): number[] {
const rgb = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x => Math.round(x * 255));
return rgb;
}
export { dicomlabToRGB };

View File

@ -0,0 +1,190 @@
import { Enums, cache } from '@cornerstonejs/core';
/**
* Updates the viewports in preparation for rendering segmentations.
* Evaluates each viewport to determine which need modifications,
* then for those viewports, changes them to a volume type and ensures
* they are ready for segmentation rendering.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - ID of the viewport to be updated.
* @param params.loadFn - Function to load the segmentation data.
* @param params.servicesManager - The services manager.
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
*
* @returns Returns true upon successful update of viewports for segmentation rendering.
*/
async function updateViewportsForSegmentationRendering({
viewportId,
loadFn,
servicesManager,
referencedDisplaySetInstanceUID,
}: {
viewportId: string;
loadFn: () => Promise<string>;
servicesManager: any;
referencedDisplaySetInstanceUID?: string;
}) {
const { cornerstoneViewportService, segmentationService, viewportGridService } =
servicesManager.services;
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
referencedDisplaySetInstanceUID =
referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
const updatedViewports = getUpdatedViewportsForSegmentation({
servicesManager,
viewportId,
referencedDisplaySetInstanceUID,
});
// create Segmentation callback which needs to be waited until
// the volume is created (if coming from stack)
const createSegmentationForVolume = async () => {
const segmentationId = await loadFn();
segmentationService.hydrateSegmentation(segmentationId);
};
// the reference volume that is used to draw the segmentation. so check if the
// volume exists in the cache (the target Viewport is already a volume viewport)
const volumeExists = Array.from(cache._volumeCache.keys()).some(volumeId =>
volumeId.includes(referencedDisplaySetInstanceUID)
);
updatedViewports.forEach(async viewport => {
viewport.viewportOptions = {
...viewport.viewportOptions,
viewportType: 'volume',
needsRerendering: true,
};
const viewportId = viewport.viewportId;
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const prevCamera = csViewport.getCamera();
// only run the createSegmentationForVolume for the targetViewportId
// since the rest will get handled by cornerstoneViewportService
if (volumeExists && viewportId === targetViewportId) {
await createSegmentationForVolume();
return;
}
const createNewSegmentationWhenVolumeMounts = async evt => {
const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac =>
ac.uid.includes(referencedDisplaySetInstanceUID)
);
// Note: make sure to re-grab the viewport since it might have changed
// during the time it took for the volume to be mounted, for instance
// the stack viewport has been changed to a volume viewport
const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
volumeViewport.setCamera(prevCamera);
volumeViewport.element.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
createNewSegmentationWhenVolumeMounts
);
if (!isTheActiveViewportVolumeMounted) {
// it means it is one of those other updated viewports so just update the camera
return;
}
if (viewportId === targetViewportId) {
await createSegmentationForVolume();
}
};
csViewport.element.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
createNewSegmentationWhenVolumeMounts
);
});
// Set the displaySets for the viewports that require to be updated
viewportGridService.setDisplaySetsForViewports(updatedViewports);
return true;
}
const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
const targetViewportId = viewportId || activeViewportId;
const viewport = viewports.get(targetViewportId);
return viewport;
};
/**
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
* This function evaluates viewports based on their compatibility with the provided segmentation's
* frame of reference UID and appends them to the updated list if they should render the segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the ID of the viewport to be updated.
* @param params.servicesManager - The services manager
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
*
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
*/
function getUpdatedViewportsForSegmentation({
viewportId,
servicesManager,
referencedDisplaySetInstanceUID,
}) {
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
servicesManager.services;
const { viewports } = viewportGridService.getState();
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs;
const referenceDisplaySetInstanceUID =
referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID);
const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID;
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
targetViewportId,
referenceDisplaySetInstanceUID
);
viewports.forEach((viewport, viewportId) => {
if (
targetViewportId === viewportId ||
updatedViewports.find(v => v.viewportId === viewportId)
) {
return;
}
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewport.displaySetInstanceUIDs,
segmentationFrameOfReferenceUID
);
if (shouldDisplaySeg) {
updatedViewports.push({
viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
viewportType: 'volume',
needsRerendering: true,
},
});
}
});
return updatedViewports;
}
export {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
};

View File

@ -1,12 +1,7 @@
function createSEGToolGroupAndAddTools( function createSEGToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
ToolGroupService, const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
customizationService,
toolGroupId
) {
const { tools } =
customizationService.get('cornerstone.overlayViewportTools') ?? {};
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {}); return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
} }
export default createSEGToolGroupAndAddTools; export default createSEGToolGroupAndAddTools;

View File

@ -1,4 +1,4 @@
import hydrateSEGDisplaySet from './_hydrateSEG'; import { ButtonEnums } from '@ohif/ui';
const RESPONSE = { const RESPONSE = {
NO_NEVER: -1, NO_NEVER: -1,
@ -9,21 +9,23 @@ const RESPONSE = {
function promptHydrateSEG({ function promptHydrateSEG({
servicesManager, servicesManager,
segDisplaySet, segDisplaySet,
viewportIndex, viewportId,
preHydrateCallbacks,
hydrateSEGDisplaySet,
}) { }) {
const { uiViewportDialogService } = servicesManager.services; const { uiViewportDialogService } = servicesManager.services;
return new Promise(async function(resolve, reject) { return new Promise(async function (resolve, reject) {
const promptResult = await _askHydrate( const promptResult = await _askHydrate(uiViewportDialogService, viewportId);
uiViewportDialogService,
viewportIndex
);
if (promptResult === RESPONSE.HYDRATE_SEG) { if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateSEGDisplaySet({ const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet, segDisplaySet,
viewportIndex, viewportId,
servicesManager,
}); });
resolve(isHydrated); resolve(isHydrated);
@ -31,17 +33,17 @@ function promptHydrateSEG({
}); });
} }
function _askHydrate(uiViewportDialogService, viewportIndex) { function _askHydrate(uiViewportDialogService, viewportId) {
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
const message = 'Do you want to open this Segmentation?'; const message = 'Do you want to open this Segmentation?';
const actions = [ const actions = [
{ {
type: 'secondary', type: ButtonEnums.type.secondary,
text: 'No', text: 'No',
value: RESPONSE.CANCEL, value: RESPONSE.CANCEL,
}, },
{ {
type: 'primary', type: ButtonEnums.type.primary,
text: 'Yes', text: 'Yes',
value: RESPONSE.HYDRATE_SEG, value: RESPONSE.HYDRATE_SEG,
}, },
@ -52,7 +54,7 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
}; };
uiViewportDialogService.show({ uiViewportDialogService.show({
viewportIndex, viewportId,
type: 'info', type: 'info',
message, message,
actions, actions,

View File

@ -2,14 +2,9 @@ import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import OHIF, { utils } from '@ohif/core'; import OHIF, { utils } from '@ohif/core';
import { import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionBar } from '@ohif/ui';
LoadingIndicatorTotalPercent,
useViewportGrid,
ViewportActionBar,
} from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG'; import promptHydrateSEG from '../utils/promptHydrateSEG';
import hydrateSEGDisplaySet from '../utils/_hydrateSEG';
import _getStatusComponent from './_getStatusComponent'; import _getStatusComponent from './_getStatusComponent';
const { formatDate } = utils; const { formatDate } = utils;
@ -20,13 +15,14 @@ function OHIFCornerstoneSEGViewport(props) {
children, children,
displaySets, displaySets,
viewportOptions, viewportOptions,
viewportIndex,
viewportLabel, viewportLabel,
servicesManager, servicesManager,
extensionManager, extensionManager,
commandsManager,
} = props; } = props;
const { t } = useTranslation('SEGViewport'); const { t } = useTranslation('SEGViewport');
const viewportId = viewportOptions.viewportId;
const { const {
displaySetService, displaySetService,
@ -36,7 +32,7 @@ function OHIFCornerstoneSEGViewport(props) {
customizationService, customizationService,
} = servicesManager.services; } = servicesManager.services;
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`; const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
// SEG viewport will always have a single display set // SEG viewport will always have a single display set
if (displaySets.length > 1) { if (displaySets.length > 1) {
@ -48,7 +44,6 @@ function OHIFCornerstoneSEGViewport(props) {
const [viewportGrid, viewportGridService] = useViewportGrid(); const [viewportGrid, viewportGridService] = useViewportGrid();
// States // States
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
const [selectedSegment, setSelectedSegment] = useState(1); const [selectedSegment, setSelectedSegment] = useState(1);
// Hydration means that the SEG is opened and segments are loaded into the // Hydration means that the SEG is opened and segments are loaded into the
@ -67,11 +62,12 @@ function OHIFCornerstoneSEGViewport(props) {
// refs // refs
const referencedDisplaySetRef = useRef(null); const referencedDisplaySetRef = useRef(null);
const { viewports, activeViewportIndex } = viewportGrid; const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet(); const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata( const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet referencedDisplaySet,
segDisplaySet
); );
referencedDisplaySetRef.current = { referencedDisplaySetRef.current = {
@ -92,14 +88,20 @@ function OHIFCornerstoneSEGViewport(props) {
setElement(null); setElement(null);
}; };
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportId }) => {
commandsManager.runCommand('storePresentation', {
viewportId,
});
});
}, [viewportGrid]);
const getCornerstoneViewport = useCallback(() => { const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry( const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone' '@ohif/extension-cornerstone.viewportModule.cornerstone'
); );
const { const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
displaySet: referencedDisplaySet,
} = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment // Todo: jump to the center of the first segment
return ( return (
@ -117,7 +119,7 @@ function OHIFCornerstoneSEGViewport(props) {
// initialImageIndex={initialImageIndex} // initialImageIndex={initialImageIndex}
></Component> ></Component>
); );
}, [viewportIndex, segDisplaySet, toolGroupId]); }, [viewportId, segDisplaySet, toolGroupId]);
const onSegmentChange = useCallback( const onSegmentChange = useCallback(
direction => { direction => {
@ -139,11 +141,7 @@ function OHIFCornerstoneSEGViewport(props) {
newSelectedSegmentIndex = numberOfSegments - 1; newSelectedSegmentIndex = numberOfSegments - 1;
} }
segmentationService.jumpToSegmentCenter( segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
segmentationId,
newSelectedSegmentIndex,
toolGroupId
);
setSelectedSegment(newSelectedSegmentIndex); setSelectedSegment(newSelectedSegmentIndex);
}, },
[selectedSegment] [selectedSegment]
@ -156,31 +154,29 @@ function OHIFCornerstoneSEGViewport(props) {
promptHydrateSEG({ promptHydrateSEG({
servicesManager, servicesManager,
viewportIndex, viewportId,
segDisplaySet, segDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateSEGDisplaySet,
}).then(isHydrated => { }).then(isHydrated => {
if (isHydrated) { if (isHydrated) {
setIsHydrated(true); setIsHydrated(true);
} }
}); });
}, [servicesManager, viewportIndex, segDisplaySet, segIsLoading]); }, [servicesManager, viewportId, segDisplaySet, segIsLoading]);
useEffect(() => { useEffect(() => {
const { unsubscribe } = segmentationService.subscribe( const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => { evt => {
if ( if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
evt.segDisplaySet.displaySetInstanceUID ===
segDisplaySet.displaySetInstanceUID
) {
setSegIsLoading(false); setSegIsLoading(false);
} }
if (evt.overlappingSegments) { if (evt.overlappingSegments) {
uiNotificationService.show({ uiNotificationService.show({
title: 'Overlapping Segments', title: 'Overlapping Segments',
message: message: 'Overlapping segments detected which is not currently supported',
'Overlapping segments detected which is not currently supported',
type: 'warning', type: 'warning',
}); });
} }
@ -215,12 +211,10 @@ function OHIFCornerstoneSEGViewport(props) {
const onDisplaySetsRemovedSubscription = displaySetService.subscribe( const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_REMOVED, displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => { ({ displaySetInstanceUIDs }) => {
const activeViewport = viewports[activeViewportIndex]; const activeViewport = viewports.get(activeViewportId);
if ( if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
) {
viewportGridService.setDisplaySetsForViewport({ viewportGridService.setDisplaySetsForViewport({
viewportIndex: activeViewportIndex, viewportId: activeViewportId,
displaySetInstanceUIDs: [], displaySetInstanceUIDs: [],
}); });
} }
@ -241,19 +235,11 @@ function OHIFCornerstoneSEGViewport(props) {
// This creates a custom tool group which has the lifetime of this view // This creates a custom tool group which has the lifetime of this view
// only, and does NOT interfere with currently displayed segmentations. // only, and does NOT interfere with currently displayed segmentations.
toolGroup = createSEGToolGroupAndAddTools( toolGroup = createSEGToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
toolGroupService,
customizationService,
toolGroupId
);
setToolGroupCreated(true);
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup( segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
toolGroupId
);
// Only destroy the viewport specific implementation // Only destroy the viewport specific implementation
toolGroupService.destroyToolGroup(toolGroupId); toolGroupService.destroyToolGroup(toolGroupId);
@ -265,9 +251,7 @@ function OHIFCornerstoneSEGViewport(props) {
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup( segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
toolGroupId
);
referencedDisplaySetRef.current = null; referencedDisplaySetRef.current = null;
}; };
}, [segDisplaySet]); }, [segDisplaySet]);
@ -288,7 +272,7 @@ function OHIFCornerstoneSEGViewport(props) {
return ( return (
child && child &&
React.cloneElement(child, { React.cloneElement(child, {
viewportIndex, viewportId,
key: index, key: index,
}) })
); );
@ -307,16 +291,28 @@ function OHIFCornerstoneSEGViewport(props) {
SpacingBetweenSlices, SpacingBetweenSlices,
} = referencedDisplaySetRef.current.metadata; } = referencedDisplaySetRef.current.metadata;
const hydrateSEGDisplaySet = ({ segDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [segDisplaySet],
viewportId,
});
};
const onStatusClick = async () => { const onStatusClick = 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();
const isHydrated = await hydrateSEGDisplaySet({ const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet, segDisplaySet,
viewportIndex, viewportId,
servicesManager,
}); });
setIsHydrated(isHydrated); setIsHydrated(isHydrated);
}; };
return ( return (
<> <>
<ViewportActionBar <ViewportActionBar
@ -337,26 +333,23 @@ function OHIFCornerstoneSEGViewport(props) {
studyDate: formatDate(StudyDate), studyDate: formatDate(StudyDate),
seriesDescription: `SEG Viewport ${SeriesDescription}`, seriesDescription: `SEG Viewport ${SeriesDescription}`,
patientInformation: { patientInformation: {
patientName: PatientName patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientSex: PatientSex || '', patientSex: PatientSex || '',
patientAge: PatientAge || '', patientAge: PatientAge || '',
MRN: PatientID || '', MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '', thickness: SliceThickness ? utils.roundNumber(SliceThickness, 2) : '',
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
spacing: spacing:
SpacingBetweenSlices !== undefined SpacingBetweenSlices !== undefined ? utils.roundNumber(SpacingBetweenSlices, 2) : '',
? `${SpacingBetweenSlices.toFixed(2)}mm`
: '',
scanner: ManufacturerModelName || '', scanner: ManufacturerModelName || '',
}, },
}} }}
/> />
<div className="relative flex flex-row w-full h-full overflow-hidden"> <div className="relative flex h-full w-full flex-row overflow-hidden">
{segIsLoading && ( {segIsLoading && (
<LoadingIndicatorTotalPercent <LoadingIndicatorTotalPercent
className="w-full h-full" className="h-full w-full"
totalNumbers={processingProgress.totalSegments} totalNumbers={processingProgress.totalSegments}
percentComplete={processingProgress.percentComplete} percentComplete={processingProgress.percentComplete}
loadingText="Loading SEG..." loadingText="Loading SEG..."
@ -371,7 +364,7 @@ function OHIFCornerstoneSEGViewport(props) {
OHIFCornerstoneSEGViewport.propTypes = { OHIFCornerstoneSEGViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object), displaySets: PropTypes.arrayOf(PropTypes.object),
viewportIndex: PropTypes.number.isRequired, viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object, dataSource: PropTypes.object,
children: PropTypes.node, children: PropTypes.node,
customProps: PropTypes.object, customProps: PropTypes.object,
@ -381,20 +374,34 @@ OHIFCornerstoneSEGViewport.defaultProps = {
customProps: {}, customProps: {},
}; };
function _getReferencedDisplaySetMetadata(referencedDisplaySet) { function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence)
? SharedFunctionalGroupsSequence[0]
: SharedFunctionalGroupsSequence;
const { PixelMeasuresSequence } = SharedFunctionalGroup;
const PixelMeasures = Array.isArray(PixelMeasuresSequence)
? PixelMeasuresSequence[0]
: PixelMeasuresSequence;
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
const image0 = referencedDisplaySet.images[0]; const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = { const referencedDisplaySetMetadata = {
PatientID: image0.PatientID, PatientID: image0.PatientID,
PatientName: image0.PatientName, PatientName: image0.PatientName,
PatientSex: image0.PatientSex, PatientSex: image0.PatientSex,
PatientAge: image0.PatientAge, PatientAge: image0.PatientAge,
SliceThickness: image0.SliceThickness, SliceThickness: image0.SliceThickness || SliceThickness,
StudyDate: image0.StudyDate, StudyDate: image0.StudyDate,
SeriesDescription: image0.SeriesDescription, SeriesDescription: image0.SeriesDescription,
SeriesInstanceUID: image0.SeriesInstanceUID, SeriesInstanceUID: image0.SeriesInstanceUID,
SeriesNumber: image0.SeriesNumber, SeriesNumber: image0.SeriesNumber,
ManufacturerModelName: image0.ManufacturerModelName, ManufacturerModelName: image0.ManufacturerModelName,
SpacingBetweenSlices: image0.SpacingBetweenSlices, SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices,
}; };
return referencedDisplaySetMetadata; return referencedDisplaySetMetadata;

View File

@ -2,37 +2,39 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon, Tooltip } from '@ohif/ui'; import { Icon, Tooltip } from '@ohif/ui';
export default function _getStatusComponent({ isHydrated, onStatusClick }) { export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null; let ToolTipMessage = null;
let StatusIcon = null; let StatusIcon = null;
const {t} = useTranslation("Common"); const { t } = useTranslation('Common');
const loadStr = t("LOAD"); const loadStr = t('LOAD');
switch (isHydrated) { switch (isHydrated) {
case true: case true:
StatusIcon = () => <Icon name="status-alert" />; StatusIcon = () => <Icon name="status-alert" />;
ToolTipMessage = () => ( ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
<div>This Segmentation is loaded in the segmentation panel</div>
);
break; break;
case false: case false:
StatusIcon = () => <Icon name="status-untracked" />; StatusIcon = () => (
<Icon
className="text-aqua-pale"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>; ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
} }
const StatusArea = () => ( const StatusArea = () => (
<div className="flex h-6 leading-6 cursor-default text-sm text-white"> <div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100"> <div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon /> <StatusIcon />
<span className="ml-1">SEG</span> <span className="ml-1">SEG</span>
</div> </div>
{!isHydrated && ( {!isHydrated && (
<div <div
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light" className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none // Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick} onMouseUp={onStatusClick}
> >
@ -42,11 +44,13 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
</div> </div>
); );
return ( return (
<> <>
{ToolTipMessage && ( {ToolTipMessage && (
<Tooltip content={<ToolTipMessage />} position="bottom-left"> <Tooltip
content={<ToolTipMessage />}
position="bottom-left"
>
<StatusArea /> <StatusArea />
</Tooltip> </Tooltip>
)} )}

View File

@ -41,13 +41,7 @@ module.exports = (env, argv) => {
libraryTarget: 'umd', libraryTarget: 'umd',
filename: pkg.main, filename: pkg.main,
}, },
externals: [ externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
/\b(vtk.js)/,
/\b(dcmjs)/,
/\b(gl-matrix)/,
/^@ohif/,
/^@cornerstonejs/,
],
plugins: [ plugins: [
new webpack.optimize.LimitChunkCountPlugin({ new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1, maxChunks: 1,

View File

@ -0,0 +1,468 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10)
### Bug Fixes
* **voi:** should publish voi change event on reset ([#3707](https://github.com/OHIF/Viewers/issues/3707)) ([52f34c6](https://github.com/OHIF/Viewers/commit/52f34c64d014f433ec1661a39b47e7fb27f15332))
# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09)
### Bug Fixes
* **modality unit:** fix the modality unit per target via upgrade of cs3d ([#3706](https://github.com/OHIF/Viewers/issues/3706)) ([0a42d57](https://github.com/OHIF/Viewers/commit/0a42d573bbca7f2551a831a46d3aa6b56674a580))
# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06)
### Features
* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65))
# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04)
### Bug Fixes
* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155))
# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03)
### Bug Fixes
* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e))
# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22)
### Features
* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1))
# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22)
### Performance Improvements
* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab))
# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
### Features
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
### Features
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
### Features
* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f))
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
### Features
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
### Bug Fixes
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))

View File

@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-cornerstone-dicom-sr", "name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.6.0", "version": "3.7.0-beta.110",
"description": "OHIF extension for an SR Cornerstone Viewport", "description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@ -32,10 +32,10 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.6.0", "@ohif/core": "3.7.0-beta.110",
"@ohif/extension-cornerstone": "3.6.0", "@ohif/extension-cornerstone": "3.7.0-beta.110",
"@ohif/extension-measurement-tracking": "3.6.0", "@ohif/extension-measurement-tracking": "3.7.0-beta.110",
"@ohif/ui": "3.6.0", "@ohif/ui": "3.7.0-beta.110",
"dcmjs": "^0.29.5", "dcmjs": "^0.29.5",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -44,9 +44,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.1.0", "@cornerstonejs/adapters": "^1.20.3",
"@cornerstonejs/core": "^1.1.0", "@cornerstonejs/core": "^1.20.3",
"@cornerstonejs/tools": "^1.1.0", "@cornerstonejs/tools": "^1.20.3",
"classnames": "^2.3.2" "classnames": "^2.3.2"
} }
} }

View File

@ -17,11 +17,7 @@ const { log } = OHIF;
* @param options Naturalized DICOM JSON headers to merge into the displaySet. * @param options Naturalized DICOM JSON headers to merge into the displaySet.
* *
*/ */
const _generateReport = ( const _generateReport = (measurementData, additionalFindingTypes, options = {}) => {
measurementData,
additionalFindingTypes,
options = {}
) => {
const filteredToolState = getFilteredCornerstoneToolState( const filteredToolState = getFilteredCornerstoneToolState(
measurementData, measurementData,
additionalFindingTypes additionalFindingTypes
@ -54,16 +50,8 @@ const commandsModule = ({}) => {
* as opposed to Finding Sites. * as opposed to Finding Sites.
* that you wish to serialize. * that you wish to serialize.
*/ */
downloadReport: ({ downloadReport: ({ measurementData, additionalFindingTypes, options = {} }) => {
measurementData, const srDataset = actions.generateReport(measurementData, additionalFindingTypes, options);
additionalFindingTypes,
options = {},
}) => {
const srDataset = actions.generateReport(
measurementData,
additionalFindingTypes,
options
);
const reportBlob = dcmjs.data.datasetToBlob(srDataset); const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary. //Create a URL for the binary.
@ -91,28 +79,19 @@ const commandsModule = ({}) => {
log.info('[DICOMSR] storeMeasurements'); log.info('[DICOMSR] storeMeasurements');
if (!dataSource || !dataSource.store || !dataSource.store.dicom) { if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
log.error( log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!');
'[DICOMSR] datasource has no dataSource.store.dicom endpoint!'
);
return Promise.reject({}); return Promise.reject({});
} }
try { try {
const naturalizedReport = _generateReport( const naturalizedReport = _generateReport(measurementData, additionalFindingTypes, options);
measurementData,
additionalFindingTypes,
options
);
const { StudyInstanceUID, ContentSequence } = naturalizedReport; const { StudyInstanceUID, ContentSequence } = naturalizedReport;
// The content sequence has 5 or more elements, of which // The content sequence has 5 or more elements, of which
// the `[4]` element contains the annotation data, so this is // the `[4]` element contains the annotation data, so this is
// checking that there is some annotation data present. // checking that there is some annotation data present.
if (!ContentSequence?.[4].ContentSequence?.length) { if (!ContentSequence?.[4].ContentSequence?.length) {
console.log( console.log('naturalizedReport missing imaging content', naturalizedReport);
'naturalizedReport missing imaging content',
naturalizedReport
);
throw new Error('Invalid report, no content'); throw new Error('Invalid report, no content');
} }
@ -130,12 +109,8 @@ const commandsModule = ({}) => {
return naturalizedReport; return naturalizedReport;
} catch (error) { } catch (error) {
console.warn(error); console.warn(error);
log.error( log.error(`[DICOMSR] Error while saving the measurements: ${error.message}`);
`[DICOMSR] Error while saving the measurements: ${error.message}` throw new Error(error.message || 'Error while saving the measurements.');
);
throw new Error(
error.message || 'Error while saving the measurements.'
);
} }
}, },
}; };

View File

@ -5,7 +5,6 @@ const srProtocol: Types.HangingProtocol.Protocol = {
// Don't store this hanging protocol as it applies to the currently active // Don't store this hanging protocol as it applies to the currently active
// display set by default // display set by default
// cacheId: null, // cacheId: null,
hasUpdatedPriorsInformation: false,
name: 'SR Key Images', name: 'SR Key Images',
// Just apply this one when specifically listed // Just apply this one when specifically listed
protocolMatchingRules: [], protocolMatchingRules: [],

View File

@ -29,9 +29,7 @@ const validateSameStudyUID = (uid: string, instances): void => {
instances.forEach(it => { instances.forEach(it => {
if (it.StudyInstanceUID !== uid) { if (it.StudyInstanceUID !== uid) {
console.warn('Not all instances have the same UID', uid, it); console.warn('Not all instances have the same UID', uid, it);
throw new Error( throw new Error(`Instances ${it.SOPInstanceUID} does not belong to ${uid}`);
`Instances ${it.SOPInstanceUID} does not belong to ${uid}`
);
} }
}); });
}; };
@ -51,10 +49,7 @@ const CodeNameCodeSequenceValues = {
const CodingSchemeDesignators = { const CodingSchemeDesignators = {
SRT: 'SRT', SRT: 'SRT',
CornerstoneCodeSchemes: [ CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
Cornerstone3DCodeScheme.CodingSchemeDesignator,
'CST4',
],
}; };
const RELATIONSHIP_TYPE = { const RELATIONSHIP_TYPE = {
@ -71,10 +66,7 @@ const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
* @param instances is a list of instances from THIS series that are not * @param instances is a list of instances from THIS series that are not
* in this DICOM SR Display Set already. * in this DICOM SR Display Set already.
*/ */
function addInstances( function addInstances(instances: InstanceMetadata[], displaySetService: DisplaySetService) {
instances: InstanceMetadata[],
displaySetService: DisplaySetService
) {
this.instances.push(...instances); this.instances.push(...instances);
utils.sortStudyInstances(this.instances); utils.sortStudyInstances(this.instances);
// The last instance is the newest one, so is the one most interesting. // The last instance is the newest one, so is the one most interesting.
@ -93,11 +85,7 @@ function addInstances(
* @param servicesManager is the services that can be used for creating * @param servicesManager is the services that can be used for creating
* @returns The list of display sets created for the given instances object * @returns The list of display sets created for the given instances object
*/ */
function _getDisplaySetsFromSeries( function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
instances,
servicesManager,
extensionManager
) {
// If the series has no instances, stop here // If the series has no instances, stop here
if (!instances || !instances.length) { if (!instances || !instances.length) {
throw new Error('No instances were provided'); throw new Error('No instances were provided');
@ -123,8 +111,7 @@ function _getDisplaySetsFromSeries(
if ( if (
!ConceptNameCodeSequence || !ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !== ConceptNameCodeSequence.CodeValue !== CodeNameCodeSequenceValues.ImagingMeasurementReport
CodeNameCodeSequenceValues.ImagingMeasurementReport
) { ) {
servicesManager.services.uiNotificationService.show({ servicesManager.services.uiNotificationService.show({
title: 'DICOM SR', title: 'DICOM SR',
@ -184,36 +171,21 @@ function _load(displaySet, servicesManager, extensionManager) {
// Check currently added displaySets and add measurements if the sources exist. // Check currently added displaySets and add measurements if the sources exist.
displaySetService.activeDisplaySets.forEach(activeDisplaySet => { displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet( _checkIfCanAddMeasurementsToDisplaySet(displaySet, activeDisplaySet, dataSource);
displaySet,
activeDisplaySet,
dataSource
);
}); });
// Subscribe to new displaySets as the source may come in after. // Subscribe to new displaySets as the source may come in after.
displaySetService.subscribe( displaySetService.subscribe(displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => {
displaySetService.EVENTS.DISPLAY_SETS_ADDED, const { displaySetsAdded } = data;
data => { // If there are still some measurements that have not yet been loaded into cornerstone,
const { displaySetsAdded } = data; // See if we can load them onto any of the new displaySets.
// If there are still some measurements that have not yet been loaded into cornerstone, displaySetsAdded.forEach(newDisplaySet => {
// See if we can load them onto any of the new displaySets. _checkIfCanAddMeasurementsToDisplaySet(displaySet, newDisplaySet, dataSource);
displaySetsAdded.forEach(newDisplaySet => { });
_checkIfCanAddMeasurementsToDisplaySet( });
displaySet,
newDisplaySet,
dataSource
);
});
}
);
} }
function _checkIfCanAddMeasurementsToDisplaySet( function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dataSource) {
srDisplaySet,
newDisplaySet,
dataSource
) {
let unloadedMeasurements = srDisplaySet.measurements.filter( let unloadedMeasurements = srDisplaySet.measurements.filter(
measurement => measurement.loaded === false measurement => measurement.loaded === false
); );
@ -248,8 +220,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
const { coords } = measurement; const { coords } = measurement;
coords.forEach(coord => { coords.forEach(coord => {
const SOPInstanceUID = const SOPInstanceUID = coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) { if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
SOPInstanceUIDs.push(SOPInstanceUID); SOPInstanceUIDs.push(SOPInstanceUID);
@ -257,9 +228,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
}); });
}); });
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet( const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet);
newDisplaySet
);
for (const imageId of imageIdsForDisplaySet) { for (const imageId of imageIdsForDisplaySet) {
if (!unloadedMeasurements.length) { if (!unloadedMeasurements.length) {
@ -267,25 +236,13 @@ function _checkIfCanAddMeasurementsToDisplaySet(
return; return;
} }
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID( const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
imageId
);
if (SOPInstanceUIDs.includes(SOPInstanceUID)) { if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) { for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
const measurement = unloadedMeasurements[j]; const measurement = unloadedMeasurements[j];
if ( if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
_measurementReferencesSOPInstanceUID( addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
measurement,
SOPInstanceUID,
frameNumber
)
) {
addMeasurement(
measurement,
imageId,
newDisplaySet.displaySetInstanceUID
);
unloadedMeasurements.splice(j, 1); unloadedMeasurements.splice(j, 1);
} }
@ -294,11 +251,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
} }
} }
function _measurementReferencesSOPInstanceUID( function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
measurement,
SOPInstanceUID,
frameNumber
) {
const { coords } = measurement; const { coords } = measurement;
// NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM // NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
@ -308,8 +261,9 @@ function _measurementReferencesSOPInstanceUID(
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) || measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
1; 1;
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
return false; return false;
}
for (let j = 0; j < coords.length; j++) { for (let j = 0; j < coords.length; j++) {
const coord = coords[j]; const coord = coords[j];
@ -323,11 +277,7 @@ function _measurementReferencesSOPInstanceUID(
function getSopClassHandlerModule({ servicesManager, extensionManager }) { function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => { const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries( return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
instances,
servicesManager,
extensionManager
);
}; };
return [ return [
@ -342,30 +292,22 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) {
function _getMeasurements(ImagingMeasurementReportContentSequence) { function _getMeasurements(ImagingMeasurementReportContentSequence) {
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find( const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
item => item =>
item.ConceptNameCodeSequence.CodeValue === item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements
CodeNameCodeSequenceValues.ImagingMeasurements
); );
const MeasurementGroups = _getSequenceAsArray( const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter(
ImagingMeasurements.ContentSequence item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup
).filter(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.MeasurementGroup
); );
const mergedContentSequencesByTrackingUniqueIdentifiers = _getMergedContentSequencesByTrackingUniqueIdentifiers( const mergedContentSequencesByTrackingUniqueIdentifiers =
MeasurementGroups _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups);
);
const measurements = []; const measurements = [];
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach( Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
trackingUniqueIdentifier => { trackingUniqueIdentifier => {
const mergedContentSequence = const mergedContentSequence =
mergedContentSequencesByTrackingUniqueIdentifiers[ mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier];
trackingUniqueIdentifier
];
const measurement = _processMeasurement(mergedContentSequence); const measurement = _processMeasurement(mergedContentSequence);
@ -378,15 +320,11 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
return measurements; return measurements;
} }
function _getMergedContentSequencesByTrackingUniqueIdentifiers( function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
MeasurementGroups
) {
const mergedContentSequencesByTrackingUniqueIdentifiers = {}; const mergedContentSequencesByTrackingUniqueIdentifiers = {};
MeasurementGroups.forEach(MeasurementGroup => { MeasurementGroups.forEach(MeasurementGroup => {
const ContentSequence = _getSequenceAsArray( const ContentSequence = _getSequenceAsArray(MeasurementGroup.ContentSequence);
MeasurementGroup.ContentSequence
);
const TrackingUniqueIdentifierItem = ContentSequence.find( const TrackingUniqueIdentifierItem = ContentSequence.find(
item => item =>
@ -395,22 +333,16 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
); );
if (!TrackingUniqueIdentifierItem) { if (!TrackingUniqueIdentifierItem) {
console.warn( console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.');
'No Tracking Unique Identifier, skipping ambiguous measurement.'
);
} }
const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID; const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID;
if ( if (mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] === undefined) {
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
] === undefined
) {
// Add the full ContentSequence // Add the full ContentSequence
mergedContentSequencesByTrackingUniqueIdentifiers[ mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] = [
trackingUniqueIdentifier ...ContentSequence,
] = [...ContentSequence]; ];
} else { } else {
// Add the ContentSequence minus the tracking identifier, as we have this // Add the ContentSequence minus the tracking identifier, as we have this
// Information in the merged ContentSequence anyway. // Information in the merged ContentSequence anyway.
@ -419,9 +351,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
item.ConceptNameCodeSequence.CodeValue !== item.ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.TrackingUniqueIdentifier CodeNameCodeSequenceValues.TrackingUniqueIdentifier
) { ) {
mergedContentSequencesByTrackingUniqueIdentifiers[ mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier].push(item);
trackingUniqueIdentifier
].push(item);
} }
}); });
} }
@ -446,18 +376,12 @@ function _processTID1410Measurement(mergedContentSequence) {
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level, // Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D // And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
const graphicItem = mergedContentSequence.find( const graphicItem = mergedContentSequence.find(group => group.ValueType === 'SCOORD');
group => group.ValueType === 'SCOORD'
);
const UIDREFContentItem = mergedContentSequence.find( const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
group => group.ValueType === 'UIDREF'
);
const TrackingIdentifierContentItem = mergedContentSequence.find( const TrackingIdentifierContentItem = mergedContentSequence.find(
item => item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingIdentifier
); );
if (!graphicItem) { if (!graphicItem) {
@ -467,9 +391,7 @@ function _processTID1410Measurement(mergedContentSequence) {
return; return;
} }
const NUMContentItems = mergedContentSequence.filter( const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
group => group.ValueType === 'NUM'
);
const measurement = { const measurement = {
loaded: false, loaded: false,
@ -484,10 +406,7 @@ function _processTID1410Measurement(mergedContentSequence) {
if (MeasuredValueSequence) { if (MeasuredValueSequence) {
measurement.labels.push( measurement.labels.push(
_getLabelFromMeasuredValueSequence( _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
ConceptNameCodeSequence,
MeasuredValueSequence
)
); );
} }
}); });
@ -496,32 +415,22 @@ function _processTID1410Measurement(mergedContentSequence) {
} }
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter( const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
group => group.ValueType === 'NUM'
);
const UIDREFContentItem = mergedContentSequence.find( const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
group => group.ValueType === 'UIDREF'
);
const TrackingIdentifierContentItem = mergedContentSequence.find( const TrackingIdentifierContentItem = mergedContentSequence.find(
item => item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingIdentifier
); );
const finding = mergedContentSequence.find( const finding = mergedContentSequence.find(
item => item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.Finding
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.Finding
); );
const findingSites = mergedContentSequence.filter( const findingSites = mergedContentSequence.filter(
item => item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator === item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SRT &&
CodingSchemeDesignators.SRT && item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.FindingSite
); );
const measurement = { const measurement = {
@ -537,8 +446,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes( CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
finding.ConceptCodeSequence.CodingSchemeDesignator finding.ConceptCodeSequence.CodingSchemeDesignator
) && ) &&
finding.ConceptCodeSequence.CodeValue === finding.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
CodeNameCodeSequenceValues.CornerstoneFreeText
) { ) {
measurement.labels.push({ measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE, label: CORNERSTONE_FREETEXT_CODE_VALUE,
@ -553,8 +461,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes( CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
FindingSite.ConceptCodeSequence.CodingSchemeDesignator FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) && ) &&
FindingSite.ConceptCodeSequence.CodeValue === FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
CodeNameCodeSequenceValues.CornerstoneFreeText
); );
if (cornerstoneFreeTextFindingSite) { if (cornerstoneFreeTextFindingSite) {
@ -566,18 +473,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
} }
NUMContentItems.forEach(item => { NUMContentItems.forEach(item => {
const { const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
ConceptNameCodeSequence,
ContentSequence,
MeasuredValueSequence,
} = item;
const { ValueType } = ContentSequence; const { ValueType } = ContentSequence;
if (!ValueType === 'SCOORD') { if (!ValueType === 'SCOORD') {
console.warn( console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
`Graphic ${ValueType} not currently supported, skipping annotation.`
);
return; return;
} }
@ -590,10 +491,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
if (MeasuredValueSequence) { if (MeasuredValueSequence) {
measurement.labels.push( measurement.labels.push(
_getLabelFromMeasuredValueSequence( _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
ConceptNameCodeSequence,
MeasuredValueSequence
)
); );
} }
}); });
@ -633,17 +531,12 @@ function _getCoordsFromSCOORDOrSCOORD3D(item) {
return coords; return coords;
} }
function _getLabelFromMeasuredValueSequence( function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) {
ConceptNameCodeSequence,
MeasuredValueSequence
) {
const { CodeMeaning } = ConceptNameCodeSequence; const { CodeMeaning } = ConceptNameCodeSequence;
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence; const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
const { CodeValue } = MeasurementUnitsCodeSequence; const { CodeValue } = MeasurementUnitsCodeSequence;
const formatedNumericValue = NumericValue const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : '';
? Number(NumericValue).toFixed(2)
: '';
return { return {
label: CodeMeaning, label: CodeMeaning,
@ -653,24 +546,20 @@ function _getLabelFromMeasuredValueSequence(
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
const ImageLibrary = ImagingMeasurementReportContentSequence.find( const ImageLibrary = ImagingMeasurementReportContentSequence.find(
item => item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.ImageLibrary
); );
const ImageLibraryGroup = _getSequenceAsArray( const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
ImageLibrary.ContentSequence item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
).find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.ImageLibraryGroup
); );
const referencedImages = []; const referencedImages = [];
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => { _getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
const { ReferencedSOPSequence } = item; const { ReferencedSOPSequence } = item;
if (!ReferencedSOPSequence) return; if (!ReferencedSOPSequence) {
return;
}
for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) { for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) {
if (ref.ReferencedSOPClassUID) { if (ref.ReferencedSOPClassUID) {
const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref; const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref;
@ -687,7 +576,9 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
} }
function _getSequenceAsArray(sequence) { function _getSequenceAsArray(sequence) {
if (!sequence) return []; if (!sequence) {
return [];
}
return Array.isArray(sequence) ? sequence : [sequence]; return Array.isArray(sequence) ? sequence : [sequence];
} }

View File

@ -1,8 +1,6 @@
import React from 'react'; import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule'; import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule, { import getHangingProtocolModule, { srProtocol } from './getHangingProtocolModule';
srProtocol,
} from './getHangingProtocolModule';
import onModeEnter from './onModeEnter'; import onModeEnter from './onModeEnter';
import getCommandsModule from './commandsModule'; import getCommandsModule from './commandsModule';
import preRegistration from './init'; import preRegistration from './init';
@ -12,9 +10,7 @@ import hydrateStructuredReport from './utils/hydrateStructuredReport';
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet'; import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
const Component = React.lazy(() => { const Component = React.lazy(() => {
return import( return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport');
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport'
);
}); });
const OHIFCornerstoneSRViewport = props => { const OHIFCornerstoneSRViewport = props => {
@ -58,7 +54,7 @@ const dicomSRExtension = {
}, },
getCommandsModule, getCommandsModule,
getSopClassHandlerModule, getSopClassHandlerModule,
// Include dynmically computed values such as toolNames not known till instantiation // Include dynamically computed values such as toolNames not known till instantiation
getUtilityModule({ servicesManager }) { getUtilityModule({ servicesManager }) {
return [ return [
{ {

View File

@ -18,9 +18,7 @@ import toolNames from './tools/toolNames';
/** /**
* @param {object} configuration * @param {object} configuration
*/ */
export default function init({ export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
configuration = {},
}: Types.Extensions.ExtensionParams): void {
addTool(DICOMSRDisplayTool); addTool(DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool, {}); addToolInstance(toolNames.SRLength, LengthTool, {});
addToolInstance(toolNames.SRBidirectional, BidirectionalTool); addToolInstance(toolNames.SRBidirectional, BidirectionalTool);

View File

@ -40,49 +40,32 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
isPointNearTool = () => null; isPointNearTool = () => null;
getHandleNearImagePoint = () => null; getHandleNearImagePoint = () => null;
renderAnnotation = ( renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => {
enabledElement: Types.IEnabledElement,
svgDrawingHelper: any
): void => {
const { viewport } = enabledElement; const { viewport } = enabledElement;
const { element } = viewport; const { element } = viewport;
let annotations = annotation.state.getAnnotations( let annotations = annotation.state.getAnnotations(this.getToolName(), element);
this.getToolName(),
element
);
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender // Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
if (!annotations?.length) { if (!annotations?.length) {
return; return;
} }
annotations = this.filterInteractableAnnotationsForElement( annotations = this.filterInteractableAnnotationsForElement(element, annotations);
element,
annotations
);
if (!annotations?.length) { if (!annotations?.length) {
return; return;
} }
const trackingUniqueIdentifiersForElement = getTrackingUniqueIdentifiersForElement( const trackingUniqueIdentifiersForElement = getTrackingUniqueIdentifiersForElement(element);
element
);
const { const { activeIndex, trackingUniqueIdentifiers } = trackingUniqueIdentifiersForElement;
activeIndex,
trackingUniqueIdentifiers,
} = trackingUniqueIdentifiersForElement;
const activeTrackingUniqueIdentifier = const activeTrackingUniqueIdentifier = trackingUniqueIdentifiers[activeIndex];
trackingUniqueIdentifiers[activeIndex];
// Filter toolData to only render the data for the active SR. // Filter toolData to only render the data for the active SR.
const filteredAnnotations = annotations.filter(annotation => const filteredAnnotations = annotations.filter(annotation =>
trackingUniqueIdentifiers.includes( trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier)
annotation.data?.cachedStats?.TrackingUniqueIdentifier
)
); );
if (!viewport._actors?.size) { if (!viewport._actors?.size) {
@ -138,8 +121,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
break; break;
case SCOORD_TYPES.ELLIPSE: case SCOORD_TYPES.ELLIPSE:
renderMethod = this.renderEllipse; renderMethod = this.renderEllipse;
canvasCoordinatesAdapter = canvasCoordinatesAdapter = utilities.math.ellipse.getCanvasEllipseCorners;
utilities.math.ellipse.getCanvasEllipseCorners;
break; break;
default: default:
throw new Error(`Unsupported GraphicType: ${GraphicType}`); throw new Error(`Unsupported GraphicType: ${GraphicType}`);
@ -221,15 +203,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
renderableData.map((data, index) => { renderableData.map((data, index) => {
canvasCoordinates = data.map(p => viewport.worldToCanvas(p)); canvasCoordinates = data.map(p => viewport.worldToCanvas(p));
const handleGroupUID = '0'; const handleGroupUID = '0';
drawing.drawHandles( drawing.drawHandles(svgDrawingHelper, annotationUID, handleGroupUID, canvasCoordinates, {
svgDrawingHelper, color: options.color,
annotationUID, });
handleGroupUID,
canvasCoordinates,
{
color: options.color,
}
);
}); });
} }
@ -248,10 +224,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
canvasCoordinates.push(viewport.worldToCanvas(point)); canvasCoordinates.push(viewport.worldToCanvas(point));
// We get the other point for the arrow by using the image size // We get the other point for the arrow by using the image size
const imagePixelModule = metaData.get( const imagePixelModule = metaData.get('imagePixelModule', referencedImageId);
'imagePixelModule',
referencedImageId
);
let xOffset = 10; let xOffset = 10;
let yOffset = 10; let yOffset = 10;
@ -309,23 +282,19 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
const rotation = viewport.getRotation(); const rotation = viewport.getRotation();
canvasCoordinates = ellipsePointsWorld.map(p => canvasCoordinates = ellipsePointsWorld.map(p => viewport.worldToCanvas(p));
viewport.worldToCanvas(p)
);
let canvasCorners; let canvasCorners;
if (rotation == 90 || rotation == 270) { if (rotation == 90 || rotation == 270) {
canvasCorners = <Array<Types.Point2>>( canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners([
utilities.math.ellipse.getCanvasEllipseCorners([ canvasCoordinates[2],
canvasCoordinates[2], canvasCoordinates[3],
canvasCoordinates[3], canvasCoordinates[0],
canvasCoordinates[0], canvasCoordinates[1],
canvasCoordinates[1], ]) as Array<Types.Point2>;
])
);
} else { } else {
canvasCorners = <Array<Types.Point2>>( canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners(
utilities.math.ellipse.getCanvasEllipseCorners(canvasCoordinates) canvasCoordinates
); ) as Array<Types.Point2>;
} }
const lineUID = `${index}`; const lineUID = `${index}`;
@ -368,23 +337,14 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates); adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates);
} }
const textLines = this._getTextBoxLinesFromLabels(label); const textLines = this._getTextBoxLinesFromLabels(label);
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas( const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
adaptedCanvasCoordinates
);
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld( annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
canvasTextBoxCoords
);
const textBoxPosition = viewport.worldToCanvas( const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
annotation.data.handles.textBox.worldPosition
);
const textBoxUID = '1'; const textBoxUID = '1';
const textBoxOptions = this.getLinkedTextBoxStyle( const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
styleSpecifier,
annotation
);
const boundingBox = drawing.drawLinkedTextBox( const boundingBox = drawing.drawLinkedTextBox(
svgDrawingHelper, svgDrawingHelper,

View File

@ -27,15 +27,11 @@ function setTrackingUniqueIdentifiersForElement(
}; };
} }
function setActiveTrackingUniqueIdentifierForElement( function setActiveTrackingUniqueIdentifierForElement(element, TrackingUniqueIdentifier) {
element,
TrackingUniqueIdentifier
) {
const enabledElement = getEnabledElement(element); const enabledElement = getEnabledElement(element);
const { viewport } = enabledElement; const { viewport } = enabledElement;
const trackingIdentifiersForElement = const trackingIdentifiersForElement = state.trackingIdentifiersByViewportId[viewport.id];
state.trackingIdentifiersByViewportId[viewport.id];
if (trackingIdentifiersForElement) { if (trackingIdentifiersForElement) {
const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex( const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex(

View File

@ -8,11 +8,7 @@ const EPSILON = 1e-4;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
export default function addMeasurement( export default function addMeasurement(measurement, imageId, displaySetInstanceUID) {
measurement,
imageId,
displaySetInstanceUID
) {
// TODO -> Render rotated ellipse . // TODO -> Render rotated ellipse .
const toolName = toolNames.DICOMSRDisplay; const toolName = toolNames.DICOMSRDisplay;
@ -31,12 +27,7 @@ export default function addMeasurement(
} }
measurementData.renderableData[GraphicType].push( measurementData.renderableData[GraphicType].push(
_getRenderableData( _getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier)
GraphicType,
GraphicData,
imageId,
measurement.TrackingIdentifier
)
); );
}); });
@ -86,12 +77,7 @@ export default function addMeasurement(
delete measurement.coords; delete measurement.coords;
} }
function _getRenderableData( function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifier) {
GraphicType,
GraphicData,
imageId,
TrackingIdentifier
) {
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':'); const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
let renderableData: csTypes.Point3[]; let renderableData: csTypes.Point3[];
@ -207,38 +193,22 @@ function _getRenderableData(
throw new Error('imageId does not have imagePlaneModule metadata'); throw new Error('imageId does not have imagePlaneModule metadata');
} }
const { const { columnCosines }: { columnCosines: csTypes.Point3 } = imagePlaneModule;
columnCosines,
}: { columnCosines: csTypes.Point3 } = imagePlaneModule;
// find which axis is parallel to the columnCosines // find which axis is parallel to the columnCosines
const columnCosinesVec = vec3.fromValues(...columnCosines); const columnCosinesVec = vec3.fromValues(...columnCosines);
const projectedMajorAxisOnColVec = Math.abs( const projectedMajorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, majorAxisVec));
vec3.dot(columnCosinesVec, majorAxisVec) const projectedMinorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, minorAxisVec));
);
const projectedMinorAxisOnColVec = Math.abs(
vec3.dot(columnCosinesVec, minorAxisVec)
);
const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec); const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec);
const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec); const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec);
renderableData = []; renderableData = [];
if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) { if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) {
renderableData = [ renderableData = [pointsWorld[0], pointsWorld[1], pointsWorld[2], pointsWorld[3]];
pointsWorld[0],
pointsWorld[1],
pointsWorld[2],
pointsWorld[3],
];
} else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) { } else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) {
renderableData = [ renderableData = [pointsWorld[2], pointsWorld[3], pointsWorld[0], pointsWorld[1]];
pointsWorld[2],
pointsWorld[3],
pointsWorld[0],
pointsWorld[1],
];
} else { } else {
console.warn('OBLIQUE ELLIPSE NOT YET SUPPORTED'); console.warn('OBLIQUE ELLIPSE NOT YET SUPPORTED');
} }

View File

@ -1,10 +1,6 @@
import { addTool } from '@cornerstonejs/tools'; import { addTool } from '@cornerstonejs/tools';
export default function addToolInstance( export default function addToolInstance(name: string, toolClass, configuration?): void {
name: string,
toolClass,
configuration?
): void {
class InstanceClass extends toolClass { class InstanceClass extends toolClass {
static toolName = name; static toolName = name;
} }

View File

@ -3,12 +3,11 @@ import { DisplaySetService, classes } from '@ohif/core';
const ImageSet = classes.ImageSet; const ImageSet = classes.ImageSet;
const findInstance = (measurement, displaySetService: DisplaySetService) => { const findInstance = (measurement, displaySetService: DisplaySetService) => {
const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } = const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } = measurement;
measurement; const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const referencedDisplaySet = displaySetService.getDisplaySetByUID( if (!referencedDisplaySet.images) {
displaySetInstanceUID return;
); }
if (!referencedDisplaySet.images) return;
return referencedDisplaySet.images.find(it => it.SOPInstanceUID === sopUid); return referencedDisplaySet.images.find(it => it.SOPInstanceUID === sopUid);
}; };
@ -16,16 +15,17 @@ const findInstance = (measurement, displaySetService: DisplaySetService) => {
* contained within the provided display set. * contained within the provided display set.
* @return an array of instances referenced. * @return an array of instances referenced.
*/ */
const findReferencedInstances = ( const findReferencedInstances = (displaySetService: DisplaySetService, displaySet) => {
displaySetService: DisplaySetService,
displaySet
) => {
const instances = []; const instances = [];
const instanceById = {}; const instanceById = {};
for (const measurement of displaySet.measurements) { for (const measurement of displaySet.measurements) {
const { imageId } = measurement; const { imageId } = measurement;
if (!imageId) continue; if (!imageId) {
if (instanceById[imageId]) continue; continue;
}
if (instanceById[imageId]) {
continue;
}
const instance = findInstance(measurement, displaySetService); const instance = findInstance(measurement, displaySetService);
if (!instance) { if (!instance) {
@ -50,7 +50,7 @@ const findReferencedInstances = (
const createReferencedImageDisplaySet = (displaySetService, displaySet) => { const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
const instances = findReferencedInstances(displaySetService, displaySet); const instances = findReferencedInstances(displaySetService, displaySet);
// This will be a member function of the created image set // This will be a member function of the created image set
const updateInstances = function() { const updateInstances = function () {
this.images.splice( this.images.splice(
0, 0,
this.images.length, this.images.length,

View File

@ -9,11 +9,12 @@ const findInstanceMetadataBySopInstanceUID = (displaySets, SOPInstanceUID) => {
let instanceFound; let instanceFound;
displaySets.find(displaySet => { displaySets.find(displaySet => {
if (!displaySet.images) return false; if (!displaySet.images) {
return false;
}
instanceFound = displaySet.images.find( instanceFound = displaySet.images.find(
instanceMetadata => instanceMetadata => instanceMetadata.getSOPInstanceUID() === SOPInstanceUID
instanceMetadata.getSOPInstanceUID() === SOPInstanceUID
); );
return !!instanceFound; return !!instanceFound;

View File

@ -18,10 +18,7 @@ const findMostRecentStructuredReport = studies => {
} }
if (isStructuredReportSeries(series)) { if (isStructuredReportSeries(series)) {
if ( if (!mostRecentStructuredReport || compareSeriesDate(series, mostRecentStructuredReport)) {
!mostRecentStructuredReport ||
compareSeriesDate(series, mostRecentStructuredReport)
) {
mostRecentStructuredReport = series; mostRecentStructuredReport = series;
} }
} }
@ -38,10 +35,7 @@ const findMostRecentStructuredReport = studies => {
* @returns {boolean} * @returns {boolean}
*/ */
const isStructuredReportSeries = series => { const isStructuredReportSeries = series => {
const supportedSopClassUIDs = [ const supportedSopClassUIDs = ['1.2.840.10008.5.1.4.1.1.88.22', '1.2.840.10008.5.1.4.1.1.11.1'];
'1.2.840.10008.5.1.4.1.1.88.22',
'1.2.840.10008.5.1.4.1.1.11.1',
];
const firstInstance = series.getFirstInstance(); const firstInstance = series.getFirstInstance();
const SOPClassUID = firstInstance.getData().metadata.SOPClassUID; const SOPClassUID = firstInstance.getData().metadata.SOPClassUID;
@ -50,7 +44,7 @@ const isStructuredReportSeries = series => {
}; };
/** /**
* Checkes if series1 is newer than series2 * Checks if series1 is newer than series2
* *
* @param {Object} series1 - Series Metadata 1 * @param {Object} series1 - Series Metadata 1
* @param {Object} series2 - Series Metadata 2 * @param {Object} series2 - Series Metadata 2

View File

@ -2,17 +2,12 @@ import OHIF from '@ohif/core';
import { annotation } from '@cornerstonejs/tools'; import { annotation } from '@cornerstonejs/tools';
const { log } = OHIF; const { log } = OHIF;
function getFilteredCornerstoneToolState( function getFilteredCornerstoneToolState(measurementData, additionalFindingTypes) {
measurementData,
additionalFindingTypes
) {
const filteredToolState = {}; const filteredToolState = {};
function addToFilteredToolState(annotation, toolType) { function addToFilteredToolState(annotation, toolType) {
if (!annotation.metadata?.referencedImageId) { if (!annotation.metadata?.referencedImageId) {
log.warn( log.warn(`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`);
`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`
);
return; return;
} }
@ -30,9 +25,7 @@ function getFilteredCornerstoneToolState(
}; };
} }
const measurementDataI = measurementData.find( const measurementDataI = measurementData.find(md => md.uid === annotation.annotationUID);
md => md.uid === annotation.annotationUID
);
const toolData = imageIdSpecificToolState[toolType].data; const toolData = imageIdSpecificToolState[toolType].data;
let { finding } = measurementDataI; let { finding } = measurementDataI;
@ -77,9 +70,7 @@ function getFilteredCornerstoneToolState(
for (let i = 0; i < framesOfReference.length; i++) { for (let i = 0; i < framesOfReference.length; i++) {
const frameOfReference = framesOfReference[i]; const frameOfReference = framesOfReference[i];
const frameOfReferenceAnnotations = annotationManager.getAnnotations( const frameOfReferenceAnnotations = annotationManager.getAnnotations(frameOfReference);
frameOfReference
);
const toolTypes = Object.keys(frameOfReferenceAnnotations); const toolTypes = Object.keys(frameOfReferenceAnnotations);
@ -91,9 +82,7 @@ function getFilteredCornerstoneToolState(
if (annotations) { if (annotations) {
for (let k = 0; k < annotations.length; k++) { for (let k = 0; k < annotations.length; k++) {
const annotation = annotations[k]; const annotation = annotations[k];
const uidIndex = uids.findIndex( const uidIndex = uids.findIndex(uid => uid === annotation.annotationUID);
uid => uid === annotation.annotationUID
);
if (uidIndex !== -1) { if (uidIndex !== -1) {
addToFilteredToolState(annotation, toolType); addToFilteredToolState(annotation, toolType);

View File

@ -9,9 +9,7 @@
export default function getLabelFromDCMJSImportedToolData(toolData) { export default function getLabelFromDCMJSImportedToolData(toolData) {
const { findingSites = [], finding } = toolData; const { findingSites = [], finding } = toolData;
let freeTextLabel = findingSites.find( let freeTextLabel = findingSites.find(fs => fs.CodeValue === 'CORNERSTONEFREETEXT');
fs => fs.CodeValue === 'CORNERSTONEFREETEXT'
);
if (freeTextLabel) { if (freeTextLabel) {
return freeTextLabel.CodeMeaning; return freeTextLabel.CodeMeaning;

View File

@ -2,6 +2,8 @@ import { utilities, metaData } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore } from '@ohif/core'; import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData'; import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adaptersSR } from '@cornerstonejs/adapters'; import { adaptersSR } from '@cornerstonejs/adapters';
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
const { locking } = CsAnnotation;
const { guid } = OHIF.utils; const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D; const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
@ -12,20 +14,26 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const convertCode = (codingValues, code) => { const convertCode = (codingValues, code) => {
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') return; if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') {
return;
}
const ref = `${code.CodingSchemeDesignator}:${code.CodeValue}`; const ref = `${code.CodingSchemeDesignator}:${code.CodeValue}`;
const ret = { ...codingValues[ref], ref, ...code, text: code.CodeMeaning }; const ret = { ...codingValues[ref], ref, ...code, text: code.CodeMeaning };
return ret; return ret;
}; };
const convertSites = (codingValues, sites) => { const convertSites = (codingValues, sites) => {
if (!sites || !sites.length) return; if (!sites || !sites.length) {
return;
}
const ret = []; const ret = [];
// Do as a loop to convert away from Proxy instances // Do as a loop to convert away from Proxy instances
for (let i = 0; i < sites.length; i++) { for (let i = 0; i < sites.length; i++) {
// Deal with irregular conversion from dcmjs // Deal with irregular conversion from dcmjs
const site = convertCode(codingValues, sites[i][0] || sites[i]); const site = convertCode(codingValues, sites[i][0] || sites[i]);
if (site) ret.push(site); if (site) {
ret.push(site);
}
} }
return (ret.length && ret) || undefined; return (ret.length && ret) || undefined;
}; };
@ -35,23 +43,16 @@ const convertSites = (codingValues, sites) => {
* *
*/ */
export default function hydrateStructuredReport( export default function hydrateStructuredReport(
{ servicesManager, extensionManager }, { servicesManager, extensionManager, appConfig },
displaySetInstanceUID displaySetInstanceUID
) { ) {
const annotationManager = CsAnnotation.state.getAnnotationManager();
const disableEditing = appConfig?.disableEditing;
const dataSource = extensionManager.getActiveDataSource()[0]; const dataSource = extensionManager.getActiveDataSource()[0];
const { const { measurementService, displaySetService, customizationService } = servicesManager.services;
measurementService,
displaySetService,
customizationService,
} = servicesManager.services;
const codingValues = customizationService.getCustomization( const codingValues = customizationService.getCustomization('codingValues', {});
'codingValues', const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
{}
);
const displaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
// TODO -> We should define a strict versioning somewhere. // TODO -> We should define a strict versioning somewhere.
const mappings = measurementService.getSourceMappings( const mappings = measurementService.getSourceMappings(
@ -115,16 +116,14 @@ export default function hydrateStructuredReport(
// TODO: notification if no hydratable? // TODO: notification if no hydratable?
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => { Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType = const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => { toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState // Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame // dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results // files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value. // in the wrong value.
const frameNumber = const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
(toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId = const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] || imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid]; sopInstanceUIDToImageId[toolData.sopInstanceUid];
@ -140,10 +139,7 @@ export default function hydrateStructuredReport(
for (let i = 0; i < imageIds.length; i++) { for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i]; const imageId = imageIds[i];
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get( const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
'instance',
imageId
);
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) { if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
SeriesInstanceUIDs.push(SeriesInstanceUID); SeriesInstanceUIDs.push(SeriesInstanceUID);
@ -152,23 +148,19 @@ export default function hydrateStructuredReport(
if (!targetStudyInstanceUID) { if (!targetStudyInstanceUID) {
targetStudyInstanceUID = StudyInstanceUID; targetStudyInstanceUID = StudyInstanceUID;
} else if (targetStudyInstanceUID !== StudyInstanceUID) { } else if (targetStudyInstanceUID !== StudyInstanceUID) {
console.warn( console.warn('NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.');
'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.'
);
} }
} }
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => { Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType = const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => { toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState // Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame // dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results // files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value. // in the wrong value.
const frameNumber = const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
(toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId = const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] || imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid]; sopInstanceUIDToImageId[toolData.sopInstanceUid];
@ -198,21 +190,13 @@ export default function hydrateStructuredReport(
CORNERSTONE_3D_TOOLS_SOURCE_VERSION CORNERSTONE_3D_TOOLS_SOURCE_VERSION
); );
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData); annotation.data.label = getLabelFromDCMJSImportedToolData(toolData);
annotation.data.finding = convertCode( annotation.data.finding = convertCode(codingValues, toolData.finding?.[0]);
codingValues, annotation.data.findingSites = convertSites(codingValues, toolData.findingSites);
toolData.finding?.[0]
);
annotation.data.findingSites = convertSites(
codingValues,
toolData.findingSites
);
annotation.data.site = annotation.data.findingSites?.[0]; annotation.data.site = annotation.data.findingSites?.[0];
const matchingMapping = mappings.find( const matchingMapping = mappings.find(m => m.annotationType === annotationType);
m => m.annotationType === annotationType
);
measurementService.addRawMeasurement( const newAnnotationUID = measurementService.addRawMeasurement(
source, source,
annotationType, annotationType,
{ annotation }, { annotation },
@ -220,6 +204,11 @@ export default function hydrateStructuredReport(
dataSource dataSource
); );
if (disableEditing) {
const addedAnnotation = annotationManager.getAnnotation(newAnnotationUID);
locking.setAnnotationLocked(addedAnnotation, true);
}
if (!imageIds.includes(imageId)) { if (!imageIds.includes(imageId)) {
imageIds.push(imageId); imageIds.push(imageId);
} }
@ -245,15 +234,14 @@ function _mapLegacyDataSet(dataset) {
); );
// Retrieve the Measurements themselves // Retrieve the Measurements themselves
const measurementGroups = toArray( const measurementGroups = toArray(imagingMeasurementContent.ContentSequence).filter(
imagingMeasurementContent.ContentSequence codeMeaningEquals(GROUP)
).filter(codeMeaningEquals(GROUP)); );
// For each of the supported measurement types, compute the measurement data // For each of the supported measurement types, compute the measurement data
const measurementData = {}; const measurementData = {};
const cornerstoneToolClasses = const cornerstoneToolClasses = MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const registeredToolClasses = []; const registeredToolClasses = [];
@ -263,13 +251,10 @@ function _mapLegacyDataSet(dataset) {
}); });
measurementGroups.forEach((measurementGroup, index) => { measurementGroups.forEach((measurementGroup, index) => {
const measurementGroupContentSequence = toArray( const measurementGroupContentSequence = toArray(measurementGroup.ContentSequence);
measurementGroup.ContentSequence
);
const TrackingIdentifierGroup = measurementGroupContentSequence.find( const TrackingIdentifierGroup = measurementGroupContentSequence.find(
contentItem => contentItem => contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER
contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER
); );
const TrackingIdentifier = TrackingIdentifierGroup.TextValue; const TrackingIdentifier = TrackingIdentifierGroup.TextValue;

View File

@ -1,8 +1,7 @@
import { adaptersSR } from '@cornerstonejs/adapters'; import { adaptersSR } from '@cornerstonejs/adapters';
const cornerstoneAdapters = const cornerstoneAdapters =
adaptersSR.Cornerstone3D.MeasurementReport adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG; const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
@ -24,8 +23,7 @@ export default function isRehydratable(displaySet, mappings) {
const adapterKeys = Object.keys(cornerstoneAdapters).filter( const adapterKeys = Object.keys(cornerstoneAdapters).filter(
adapterKey => adapterKey =>
typeof cornerstoneAdapters[adapterKey] typeof cornerstoneAdapters[adapterKey].isValidCornerstoneTrackingIdentifier === 'function'
.isValidCornerstoneTrackingIdentifier === 'function'
); );
const adapters = []; const adapters = [];
@ -48,19 +46,13 @@ export default function isRehydratable(displaySet, mappings) {
const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`; const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`;
return adapter.isValidCornerstoneTrackingIdentifier( return adapter.isValidCornerstoneTrackingIdentifier(mappedTrackingIdentifier);
mappedTrackingIdentifier
);
}); });
if (hydratable) { if (hydratable) {
return true; return true;
} }
console.log( console.log('Measurement is not rehydratable', TrackingIdentifier, measurements[i]);
'Measurement is not rehydratable',
TrackingIdentifier,
measurements[i]
);
} }
console.log('No measurements found which were rehydratable'); console.log('No measurements found which were rehydratable');

View File

@ -7,11 +7,11 @@ import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSR
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui'; import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport'; import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import { useAppConfig } from '@state';
const { formatDate } = utils; const { formatDate } = utils;
const MEASUREMENT_TRACKING_EXTENSION_ID = const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
'@ohif/extension-measurement-tracking';
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup'; const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
@ -20,18 +20,18 @@ function OHIFCornerstoneSRViewport(props) {
children, children,
dataSource, dataSource,
displaySets, displaySets,
viewportIndex,
viewportLabel, viewportLabel,
viewportOptions, viewportOptions,
servicesManager, servicesManager,
extensionManager, extensionManager,
} = props; } = props;
const { const [appConfig] = useAppConfig();
displaySetService,
cornerstoneViewportService, const { displaySetService, cornerstoneViewportService, measurementService } =
measurementService, servicesManager.services;
} = servicesManager.services;
const viewportId = viewportOptions.viewportId;
// SR viewport will always have a single display set // SR viewport will always have a single display set
if (displaySets.length > 1) { if (displaySets.length > 1) {
@ -43,15 +43,10 @@ function OHIFCornerstoneSRViewport(props) {
const [viewportGrid, viewportGridService] = useViewportGrid(); const [viewportGrid, viewportGridService] = useViewportGrid();
const [measurementSelected, setMeasurementSelected] = useState(0); const [measurementSelected, setMeasurementSelected] = useState(0);
const [measurementCount, setMeasurementCount] = useState(1); const [measurementCount, setMeasurementCount] = useState(1);
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState( const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(null);
null const [referencedDisplaySetMetadata, setReferencedDisplaySetMetadata] = useState(null);
);
const [
referencedDisplaySetMetadata,
setReferencedDisplaySetMetadata,
] = useState(null);
const [element, setElement] = useState(null); const [element, setElement] = useState(null);
const { viewports, activeViewportIndex } = viewportGrid; const { viewports, activeViewportId } = viewportGrid;
// Optional hook into tracking extension, if present. // Optional hook into tracking extension, if present.
let trackedMeasurements; let trackedMeasurements;
@ -76,16 +71,14 @@ function OHIFCornerstoneSRViewport(props) {
sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => { sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => {
measurementService.clearMeasurements(); measurementService.clearMeasurements();
const { SeriesInstanceUIDs } = hydrateStructuredReport( const { SeriesInstanceUIDs } = hydrateStructuredReport(
{ servicesManager, extensionManager }, { servicesManager, extensionManager, appConfig },
displaySetInstanceUID displaySetInstanceUID
); );
const displaySets = displaySetService.getDisplaySetsForSeries( const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
SeriesInstanceUIDs[0]
);
if (displaySets.length) { if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([ viewportGridService.setDisplaySetsForViewports([
{ {
viewportIndex: activeViewportIndex, viewportId: activeViewportId,
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID], displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
}, },
]); ]);
@ -123,11 +116,7 @@ function OHIFCornerstoneSRViewport(props) {
const updateViewport = useCallback( const updateViewport = useCallback(
newMeasurementSelected => { newMeasurementSelected => {
const { const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet;
StudyInstanceUID,
displaySetInstanceUID,
sopClassUids,
} = srDisplaySet;
if (!StudyInstanceUID || !displaySetInstanceUID) { if (!StudyInstanceUID || !displaySetInstanceUID) {
return; return;
@ -136,9 +125,7 @@ function OHIFCornerstoneSRViewport(props) {
if (sopClassUids && sopClassUids.length > 1) { if (sopClassUids && sopClassUids.length > 1) {
// Todo: what happens if there are multiple SOP Classes? Why we are // Todo: what happens if there are multiple SOP Classes? Why we are
// not throwing an error? // not throwing an error?
console.warn( console.warn('More than one SOPClassUID in the same series is not yet supported.');
'More than one SOPClassUID in the same series is not yet supported.'
);
} }
_getViewportReferencedDisplaySetData( _getViewportReferencedDisplaySetData(
@ -160,19 +147,11 @@ function OHIFCornerstoneSRViewport(props) {
// imageIdIndex will handle it by updating the viewport, but if they // imageIdIndex will handle it by updating the viewport, but if they
// are the same we just need to use measurementService to jump to the // are the same we just need to use measurementService to jump to the
// new measurement // new measurement
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex( const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
viewportIndex
);
const csViewport = cornerstoneViewportService.getCornerstoneViewport(
viewportInfo.getViewportId()
);
const imageIds = csViewport.getImageIds(); const imageIds = csViewport.getImageIds();
const imageIdIndex = imageIds.indexOf( const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId);
measurements[newMeasurementSelected].imageId
);
if (imageIdIndex !== -1) { if (imageIdIndex !== -1) {
csViewport.setImageIdIndex(imageIdIndex); csViewport.setImageIdIndex(imageIdIndex);
@ -180,7 +159,7 @@ function OHIFCornerstoneSRViewport(props) {
} }
}); });
}, },
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportIndex] [dataSource, srDisplaySet, activeImageDisplaySetData, viewportId]
); );
const getCornerstoneViewport = useCallback(() => { const getCornerstoneViewport = useCallback(() => {
@ -225,9 +204,10 @@ function OHIFCornerstoneSRViewport(props) {
}} }}
onElementEnabled={onElementEnabled} onElementEnabled={onElementEnabled}
initialImageIndex={initialImageIndex} initialImageIndex={initialImageIndex}
isJumpToMeasurementDisabled={true}
></Component> ></Component>
); );
}, [activeImageDisplaySetData, viewportIndex, measurementSelected]); }, [activeImageDisplaySetData, viewportId, measurementSelected]);
const onMeasurementChange = useCallback( const onMeasurementChange = useCallback(
direction => { direction => {
@ -250,12 +230,7 @@ function OHIFCornerstoneSRViewport(props) {
setTrackingIdentifiers(newMeasurementSelected); setTrackingIdentifiers(newMeasurementSelected);
updateViewport(newMeasurementSelected); updateViewport(newMeasurementSelected);
}, },
[ [measurementSelected, measurementCount, updateViewport, setTrackingIdentifiers]
measurementSelected,
measurementCount,
updateViewport,
setTrackingIdentifiers,
]
); );
/** /**
@ -265,12 +240,10 @@ function OHIFCornerstoneSRViewport(props) {
const onDisplaySetsRemovedSubscription = displaySetService.subscribe( const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_REMOVED, displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => { ({ displaySetInstanceUIDs }) => {
const activeViewport = viewports[activeViewportIndex]; const activeViewport = viewports.get(activeViewportId);
if ( if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
) {
viewportGridService.setDisplaySetsForViewport({ viewportGridService.setDisplaySetsForViewport({
viewportIndex: activeViewportIndex, viewportId: activeViewportId,
displaySetInstanceUIDs: [], displaySetInstanceUIDs: [],
}); });
} }
@ -337,7 +310,7 @@ function OHIFCornerstoneSRViewport(props) {
return ( return (
child && child &&
React.cloneElement(child, { React.cloneElement(child, {
viewportIndex, viewportId,
key: index, key: index,
}) })
); );
@ -369,7 +342,7 @@ function OHIFCornerstoneSRViewport(props) {
getStatusComponent={() => getStatusComponent={() =>
_getStatusComponent({ _getStatusComponent({
srDisplaySet, srDisplaySet,
viewportIndex, viewportId,
isTracked: false, isTracked: false,
isRehydratable: srDisplaySet.isRehydratable, isRehydratable: srDisplaySet.isRehydratable,
isLocked, isLocked,
@ -383,23 +356,19 @@ function OHIFCornerstoneSRViewport(props) {
currentSeries: SeriesNumber, currentSeries: SeriesNumber,
seriesDescription: SeriesDescription || '', seriesDescription: SeriesDescription || '',
patientInformation: { patientInformation: {
patientName: PatientName patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientSex: PatientSex || '', patientSex: PatientSex || '',
patientAge: PatientAge || '', patientAge: PatientAge || '',
MRN: PatientID || '', MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '', thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
spacing: spacing:
SpacingBetweenSlices !== undefined SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
? `${SpacingBetweenSlices.toFixed(2)}mm`
: '',
scanner: ManufacturerModelName || '', scanner: ManufacturerModelName || '',
}, },
}} }}
/> />
<div className="relative flex flex-row w-full h-full overflow-hidden"> <div className="relative flex h-full w-full flex-row overflow-hidden">
{getCornerstoneViewport()} {getCornerstoneViewport()}
{childrenWithProps} {childrenWithProps}
</div> </div>
@ -409,7 +378,7 @@ function OHIFCornerstoneSRViewport(props) {
OHIFCornerstoneSRViewport.propTypes = { OHIFCornerstoneSRViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object), displaySets: PropTypes.arrayOf(PropTypes.object),
viewportIndex: PropTypes.number.isRequired, viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object, dataSource: PropTypes.object,
children: PropTypes.node, children: PropTypes.node,
viewportLabel: PropTypes.string, viewportLabel: PropTypes.string,
@ -434,9 +403,7 @@ async function _getViewportReferencedDisplaySetData(
const { displaySetInstanceUID } = measurement; const { displaySetInstanceUID } = measurement;
const referencedDisplaySet = displaySetService.getDisplaySetByUID( const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
displaySetInstanceUID
);
const image0 = referencedDisplaySet.images[0]; const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = { const referencedDisplaySetMetadata = {
@ -458,7 +425,7 @@ async function _getViewportReferencedDisplaySetData(
function _getStatusComponent({ function _getStatusComponent({
srDisplaySet, srDisplaySet,
viewportIndex, viewportId,
isRehydratable, isRehydratable,
isLocked, isLocked,
sendTrackedMeasurementsEvent, sendTrackedMeasurementsEvent,
@ -466,7 +433,7 @@ function _getStatusComponent({
const handleMouseUp = () => { const handleMouseUp = () => {
sendTrackedMeasurementsEvent('HYDRATE_SR', { sendTrackedMeasurementsEvent('HYDRATE_SR', {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID, displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportIndex, viewportId,
}); });
}; };
@ -476,8 +443,7 @@ function _getStatusComponent({
// 1 - Incompatible // 1 - Incompatible
// 2 - Locked // 2 - Locked
// 3 - Rehydratable / Open // 3 - Rehydratable / Open
const state = const state = isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
let ToolTipMessage = null; let ToolTipMessage = null;
let StatusIcon = null; let StatusIcon = null;
@ -507,22 +473,25 @@ function _getStatusComponent({
); );
break; break;
case 3: case 3:
StatusIcon = () => <Icon name="status-untracked" />; StatusIcon = () => (
<Icon
ToolTipMessage = () => ( className="text-aqua-pale"
<div>{`Click ${loadStr} to restore measurements.`}</div> name="status-untracked"
/>
); );
ToolTipMessage = () => <div>{`Click ${loadStr} to restore measurements.`}</div>;
} }
const StatusArea = () => ( const StatusArea = () => (
<div className="flex h-6 leading-6 cursor-default text-sm text-white"> <div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100"> <div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon /> <StatusIcon />
<span className="ml-1">SR</span> <span className="ml-1">SR</span>
</div> </div>
{state === 3 && ( {state === 3 && (
<div <div
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light" className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none // Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
> >
@ -535,7 +504,10 @@ function _getStatusComponent({
return ( return (
<> <>
{ToolTipMessage && ( {ToolTipMessage && (
<Tooltip content={<ToolTipMessage />} position="bottom-left"> <Tooltip
content={<ToolTipMessage />}
position="bottom-left"
>
<StatusArea /> <StatusArea />
</Tooltip> </Tooltip>
)} )}

View File

@ -40,13 +40,7 @@ module.exports = (env, argv) => {
libraryTarget: 'umd', libraryTarget: 'umd',
filename: pkg.main, filename: pkg.main,
}, },
externals: [ externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
/\b(vtk.js)/,
/\b(dcmjs)/,
/\b(gl-matrix)/,
/^@ohif/,
/^@cornerstonejs/,
],
plugins: [ plugins: [
new webpack.optimize.LimitChunkCountPlugin({ new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1, maxChunks: 1,

View File

@ -0,0 +1,493 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10)
### Bug Fixes
* **modules:** add stylus loader as an option to be uncommented ([#3710](https://github.com/OHIF/Viewers/issues/3710)) ([7c57f67](https://github.com/OHIF/Viewers/commit/7c57f67844b790fc6e47ac3f9708bf9d576389c8))
# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10)
### Bug Fixes
* **segmentation:** Various fixes for segmentation mode and other ([#3709](https://github.com/OHIF/Viewers/issues/3709)) ([a9a6ad5](https://github.com/OHIF/Viewers/commit/a9a6ad50eae67b43b8b34efc07182d788cacdcfe))
# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10)
### Bug Fixes
* **voi:** should publish voi change event on reset ([#3707](https://github.com/OHIF/Viewers/issues/3707)) ([52f34c6](https://github.com/OHIF/Viewers/commit/52f34c64d014f433ec1661a39b47e7fb27f15332))
# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09)
### Bug Fixes
* **modality unit:** fix the modality unit per target via upgrade of cs3d ([#3706](https://github.com/OHIF/Viewers/issues/3706)) ([0a42d57](https://github.com/OHIF/Viewers/commit/0a42d573bbca7f2551a831a46d3aa6b56674a580))
# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09)
### Bug Fixes
* **segmentation:** do not use SAB if not specified ([#3705](https://github.com/OHIF/Viewers/issues/3705)) ([4911e47](https://github.com/OHIF/Viewers/commit/4911e4796cef5e22cb7cc0ca73dc5c956bc75339))
# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06)
### Features
* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65))
# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06)
### Bug Fixes
* **bugs:** fixing lots of bugs regarding release candidate ([#3700](https://github.com/OHIF/Viewers/issues/3700)) ([8bc12a3](https://github.com/OHIF/Viewers/commit/8bc12a37d0353160ae5ea4624dc0b244b7d59c07))
# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04)
### Bug Fixes
* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155))
# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03)
### Features
* **debug:** Add timing information about time to first image/all images, and query time ([#3681](https://github.com/OHIF/Viewers/issues/3681)) ([108383b](https://github.com/OHIF/Viewers/commit/108383b9ef51e4bef82d9c932b9bc7aa5354e799))
# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03)
### Features
* **displayArea:** add display area to hanging protocol ([#3691](https://github.com/OHIF/Viewers/issues/3691)) ([5e7fe91](https://github.com/OHIF/Viewers/commit/5e7fe91617d7399f85702d82e7bfa028b8010a89))
# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03)
### Bug Fixes
* **dicom overlay:** Handle special cases of ArrayBuffer for various DICOM overlay attributes. ([#3684](https://github.com/OHIF/Viewers/issues/3684)) ([e36a604](https://github.com/OHIF/Viewers/commit/e36a6043315e900eeb6ce183772c7f852f478e96))
* **StackSync:** Miscellaneous fixes for stack image sync ([#3663](https://github.com/OHIF/Viewers/issues/3663)) ([8a335bd](https://github.com/OHIF/Viewers/commit/8a335bd03d14ba87d65d7468d93f74040aa828d9))
# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03)
### Bug Fixes
* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e))
# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29)
### Bug Fixes
* **no sab:** should work when shared array buffer is not required ([#3686](https://github.com/OHIF/Viewers/issues/3686)) ([a67d72d](https://github.com/OHIF/Viewers/commit/a67d72de85238b369a18c010bf6d147daefc6df5))
# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22)
### Features
* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1))
# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22)
### Performance Improvements
* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab))
# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15)
### Bug Fixes
* **measurements:** Update the calibration tool to match changes in CS3D ([#3505](https://github.com/OHIF/Viewers/issues/3505)) ([38af311](https://github.com/OHIF/Viewers/commit/38af3112ec1f94f36c0ef64ff1cf9d21c0981c81))
# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
### Features
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
### Features
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
### Features
* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f))
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
### Bug Fixes
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))

View File

@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-cornerstone", "name": "@ohif/extension-cornerstone",
"version": "3.6.0", "version": "3.7.0-beta.110",
"description": "OHIF extension for Cornerstone", "description": "OHIF extension for Cornerstone",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@ -36,9 +36,9 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2", "@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^0.6.8", "@cornerstonejs/dicom-image-loader": "^1.20.3",
"@ohif/core": "3.6.0", "@ohif/core": "3.7.0-beta.110",
"@ohif/ui": "3.6.0", "@ohif/ui": "3.7.0-beta.110",
"dcmjs": "^0.29.6", "dcmjs": "^0.29.6",
"dicom-parser": "^1.8.21", "dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -52,10 +52,10 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.1.0", "@cornerstonejs/adapters": "^1.20.3",
"@cornerstonejs/core": "^1.1.0", "@cornerstonejs/core": "^1.20.3",
"@cornerstonejs/streaming-image-volume-loader": "^1.1.0", "@cornerstonejs/streaming-image-volume-loader": "^1.20.3",
"@cornerstonejs/tools": "^1.1.0", "@cornerstonejs/tools": "^1.20.3",
"@kitware/vtk.js": "27.3.1", "@kitware/vtk.js": "27.3.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",

View File

@ -10,17 +10,8 @@ import {
utilities as csUtils, utilities as csUtils,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core'; import { MeasurementService } from '@ohif/core';
import { import { Notification, useViewportDialog } from '@ohif/ui';
CinePlayer, import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
useCine,
useViewportGrid,
Notification,
useViewportDialog,
} from '@ohif/ui';
import {
IStackViewport,
IVolumeViewport,
} from '@cornerstonejs/core/dist/esm/types';
import { setEnabledElement } from '../state'; import { setEnabledElement } from '../state';
@ -28,6 +19,8 @@ import './OHIFCornerstoneViewport.css';
import CornerstoneOverlays from './Overlays/CornerstoneOverlays'; import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import CornerstoneServices from '../types/CornerstoneServices'; import CornerstoneServices from '../types/CornerstoneServices';
import CinePlayer from '../components/CinePlayer';
import { Types } from '@ohif/core';
const STACK = 'stack'; const STACK = 'stack';
@ -46,24 +39,19 @@ function areEqual(prevProps, nextProps) {
return false; return false;
} }
if ( if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
prevProps.viewportOptions.orientation !==
nextProps.viewportOptions.orientation
) {
return false; return false;
} }
if ( if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
prevProps.viewportOptions.toolGroupId !==
nextProps.viewportOptions.toolGroupId
) {
return false; return false;
} }
if ( if (prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType) {
prevProps.viewportOptions.viewportType !== return false;
nextProps.viewportOptions.viewportType }
) {
if (nextProps.viewportOptions.needsRerendering) {
return false; return false;
} }
@ -79,8 +67,7 @@ function areEqual(prevProps, nextProps) {
const foundDisplaySet = nextDisplaySets.find( const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet => nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID === nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
prevDisplaySet.displaySetInstanceUID
); );
if (!foundDisplaySet) { if (!foundDisplaySet) {
@ -95,9 +82,7 @@ function areEqual(prevProps, nextProps) {
// check if their imageIds are the same // check if their imageIds are the same
if (foundDisplaySet.images?.length) { if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) { for (let j = 0; j < foundDisplaySet.images.length; j++) {
if ( if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId
) {
return false; return false;
} }
} }
@ -111,25 +96,24 @@ function areEqual(prevProps, nextProps) {
// Then we don't need to worry about the re-renders if the props change. // Then we don't need to worry about the re-renders if the props change.
const OHIFCornerstoneViewport = React.memo(props => { const OHIFCornerstoneViewport = React.memo(props => {
const { const {
viewportIndex,
displaySets, displaySets,
dataSource, dataSource,
viewportOptions, viewportOptions,
displaySetOptions, displaySetOptions,
servicesManager, servicesManager,
commandsManager,
onElementEnabled, onElementEnabled,
onElementDisabled, onElementDisabled,
isJumpToMeasurementDisabled,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation // Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used // of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render // to set the initial state of the viewport's first image to render
initialImageIndex, initialImageIndex,
} = props; } = props;
const viewportId = viewportOptions.viewportId;
const [scrollbarHeight, setScrollbarHeight] = useState('100px'); const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [{ isCineEnabled, cines }, cineService] = useCine();
const [{ activeViewportIndex }] = useViewportGrid();
const [enabledVPElement, setEnabledVPElement] = useState(null); const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef(); const elementRef = useRef();
const { const {
@ -145,74 +129,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
} = servicesManager.services as CornerstoneServices; } = servicesManager.services as CornerstoneServices;
const [viewportDialogState] = useViewportDialog(); const [viewportDialogState] = useViewportDialog();
const cineHandler = () => {
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
return;
}
const cine = cines[viewportIndex];
const isPlaying = cine.isPlaying || false;
const frameRate = cine.frameRate || 24;
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(enabledVPElement);
}
};
useEffect(() => {
eventTarget.addEventListener(
Enums.Events.STACK_VIEWPORT_NEW_STACK,
cineHandler
);
return () => {
cineService.setCine({ id: viewportIndex, isPlaying: false });
eventTarget.removeEventListener(
Enums.Events.STACK_VIEWPORT_NEW_STACK,
cineHandler
);
};
}, [enabledVPElement]);
useEffect(() => {
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
return;
}
cineHandler();
return () => {
if (enabledVPElement && cines?.[viewportIndex]?.isPlaying) {
cineService.stopClip(enabledVPElement);
}
};
}, [cines, viewportIndex, cineService, enabledVPElement, cineHandler]);
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
const handleCineClose = () => {
toolbarService.recordInteraction({
groupId: 'MoreTools',
itemId: 'cine',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleCine',
commandOptions: {},
context: 'CORNERSTONE',
},
],
});
};
// useCallback for scroll bar height calculation // useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => { const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`; const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
@ -227,54 +143,17 @@ const OHIFCornerstoneViewport = React.memo(props => {
} }
}, [elementRef]); }, [elementRef]);
const storePresentation = () => { const cleanUpServices = useCallback(
const currentPresentation = cornerstoneViewportService.getPresentation( viewportInfo => {
viewportIndex const renderingEngineId = viewportInfo.getRenderingEngineId();
); const syncGroups = viewportInfo.getSyncGroups();
if (!currentPresentation || !currentPresentation.presentationIds) return;
const {
lutPresentationStore,
positionPresentationStore,
} = stateSyncService.getState();
const { presentationIds } = currentPresentation;
const { lutPresentationId, positionPresentationId } = presentationIds || {};
const storeState = {};
if (lutPresentationId) {
storeState.lutPresentationStore = {
...lutPresentationStore,
[lutPresentationId]: currentPresentation,
};
}
if (positionPresentationId) {
storeState.positionPresentationStore = {
...positionPresentationStore,
[positionPresentationId]: currentPresentation,
};
}
stateSyncService.store(storeState);
};
const cleanUpServices = useCallback(() => { toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (!viewportInfo) { syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
return; },
} [viewportId]
);
const viewportId = viewportInfo.getViewportId();
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
syncGroupService.removeViewportFromSyncGroup(
viewportId,
renderingEngineId,
syncGroups
);
}, [viewportIndex, viewportOptions.viewportId]);
const elementEnabledHandler = useCallback( const elementEnabledHandler = useCallback(
evt => { evt => {
@ -284,71 +163,48 @@ const OHIFCornerstoneViewport = React.memo(props => {
} }
const { viewportId, element } = evt.detail; const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo( const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
viewportId setEnabledElement(viewportId, element);
);
const viewportIndex = viewportInfo.getViewportIndex();
setEnabledElement(viewportIndex, element);
setEnabledVPElement(element); setEnabledVPElement(element);
const renderingEngineId = viewportInfo.getRenderingEngineId(); const renderingEngineId = viewportInfo.getRenderingEngineId();
const toolGroupId = viewportInfo.getToolGroupId(); const toolGroupId = viewportInfo.getToolGroupId();
const syncGroups = viewportInfo.getSyncGroups(); const syncGroups = viewportInfo.getSyncGroups();
toolGroupService.addViewportToToolGroup( toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
viewportId,
renderingEngineId,
toolGroupId
);
syncGroupService.addViewportToSyncGroup( syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
viewportId,
renderingEngineId,
syncGroups
);
if (onElementEnabled) { if (onElementEnabled) {
onElementEnabled(evt); onElementEnabled(evt);
} }
}, },
[viewportIndex, onElementEnabled, toolGroupService] [viewportId, onElementEnabled, toolGroupService]
); );
// disable the element upon unmounting // disable the element upon unmounting
useEffect(() => { useEffect(() => {
cornerstoneViewportService.enableViewport( cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
viewportIndex,
viewportOptions,
elementRef.current
);
eventTarget.addEventListener( eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
Enums.Events.ELEMENT_ENABLED,
elementEnabledHandler
);
setImageScrollBarHeight(); setImageScrollBarHeight();
return () => { return () => {
storePresentation(); const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
cleanUpServices(); if (!viewportInfo) {
return;
}
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex( cleanUpServices(viewportInfo);
viewportIndex cornerstoneViewportService.storePresentation({ viewportId });
);
cornerstoneViewportService.disableElement(viewportIndex);
if (onElementDisabled) { if (onElementDisabled) {
onElementDisabled(viewportInfo); onElementDisabled(viewportInfo);
} }
eventTarget.removeEventListener( eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
Enums.Events.ELEMENT_ENABLED,
elementEnabledHandler
);
}; };
}, []); }, []);
@ -363,10 +219,15 @@ const OHIFCornerstoneViewport = React.memo(props => {
useEffect(() => { useEffect(() => {
const { unsubscribe } = displaySetService.subscribe( const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async invalidatedDisplaySetInstanceUID => { async ({
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex( displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
viewportIndex invalidateData,
); }: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
if (!invalidateData) {
return;
}
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) { if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData(); const viewportData = viewportInfo.getViewportData();
@ -378,18 +239,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
); );
const keepCamera = true; const keepCamera = true;
cornerstoneViewportService.updateViewport( cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
viewportIndex,
newViewportData,
keepCamera
);
} }
} }
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, [viewportIndex]); }, [viewportId]);
useEffect(() => { useEffect(() => {
// handle the default viewportType to be stack // handle the default viewportType to be stack
@ -408,33 +265,37 @@ const OHIFCornerstoneViewport = React.memo(props => {
// The presentation state will have been stored previously by closing // The presentation state will have been stored previously by closing
// a viewport. Otherwise, this viewport will be unchanged and the // a viewport. Otherwise, this viewport will be unchanged and the
// presentation information will be directly carried over. // presentation information will be directly carried over.
const { const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
lutPresentationStore,
positionPresentationStore,
} = stateSyncService.getState();
const { presentationIds } = viewportOptions; const { presentationIds } = viewportOptions;
const presentations = { const presentations = {
positionPresentation: positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
positionPresentationStore[presentationIds?.positionPresentationId], lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
lutPresentation:
lutPresentationStore[presentationIds?.lutPresentationId],
}; };
let measurement; let measurement;
if (cacheJumpToMeasurementEvent?.viewportIndex === viewportIndex) { if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
measurement = cacheJumpToMeasurementEvent.measurement; measurement = cacheJumpToMeasurementEvent.measurement;
// Delete the position presentation so that viewport navigates direct // Delete the position presentation so that viewport navigates direct
presentations.positionPresentation = null; presentations.positionPresentation = null;
cacheJumpToMeasurementEvent = null; cacheJumpToMeasurementEvent = null;
} }
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
// so we need to re-render (force update via change of the needsRerendering) so that React
// does the diffing and decides we should render this again (although the id and element has not changed)
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
if (viewportOptions.needsRerendering) {
viewportOptions.needsRerendering = false;
}
cornerstoneViewportService.setViewportData( cornerstoneViewportService.setViewportData(
viewportIndex, viewportId,
viewportData, viewportData,
viewportOptions, viewportOptions,
displaySetOptions, displaySetOptions,
presentations presentations
); );
if (measurement) { if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid); cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
} }
@ -454,11 +315,15 @@ const OHIFCornerstoneViewport = React.memo(props => {
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice. * the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/ */
useEffect(() => { useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents( const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
measurementService, measurementService,
displaySetService, displaySetService,
elementRef, elementRef,
viewportIndex, viewportId,
displaySets, displaySets,
viewportGridService, viewportGridService,
cornerstoneViewportService cornerstoneViewportService
@ -468,7 +333,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
measurementService, measurementService,
displaySetService, displaySetService,
elementRef, elementRef,
viewportIndex, viewportId,
displaySets, displaySets,
viewportGridService, viewportGridService,
cornerstoneViewportService cornerstoneViewportService
@ -477,17 +342,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
return () => { return () => {
unsubscribeFromJumpToMeasurementEvents(); unsubscribeFromJumpToMeasurementEvents();
}; };
}, [displaySets, elementRef, viewportIndex]); }, [displaySets, elementRef, viewportId]);
return ( return (
<React.Fragment> <React.Fragment>
<div className="viewport-wrapper"> <div className="viewport-wrapper">
<ReactResizeDetector <ReactResizeDetector
handleWidth refreshMode="debounce"
handleHeight refreshRate={50} // Wait 50 ms after last move to render
skipOnMount={true} // Todo: make these configurable
refreshMode={'debounce'}
refreshRate={200} // transition amount in side panel
onResize={onResize} onResize={onResize}
targetRef={elementRef.current} targetRef={elementRef.current}
/> />
@ -499,34 +361,20 @@ const OHIFCornerstoneViewport = React.memo(props => {
ref={elementRef} ref={elementRef}
></div> ></div>
<CornerstoneOverlays <CornerstoneOverlays
viewportIndex={viewportIndex} viewportId={viewportId}
toolBarService={toolbarService} toolBarService={toolbarService}
element={elementRef.current} element={elementRef.current}
scrollbarHeight={scrollbarHeight} scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager} servicesManager={servicesManager}
/> />
{isCineEnabled && ( <CinePlayer
<CinePlayer enabledVPElement={enabledVPElement}
className="absolute left-1/2 -translate-x-1/2 bottom-3" viewportId={viewportId}
isPlaying={isPlaying} servicesManager={servicesManager}
onClose={handleCineClose} />
onPlayPauseChange={isPlaying =>
cineService.setCine({
id: activeViewportIndex,
isPlaying,
})
}
onFrameRateChange={frameRate =>
cineService.setCine({
id: activeViewportIndex,
frameRate,
})
}
/>
)}
</div> </div>
<div className="absolute w-full"> <div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && ( {viewportDialogState.viewportId === viewportId && (
<Notification <Notification
id="viewport-notification" id="viewport-notification"
message={viewportDialogState.message} message={viewportDialogState.message}
@ -545,35 +393,35 @@ function _subscribeToJumpToMeasurementEvents(
measurementService, measurementService,
displaySetService, displaySetService,
elementRef, elementRef,
viewportIndex, viewportId,
displaySets, displaySets,
viewportGridService, viewportGridService,
cornerstoneViewportService cornerstoneViewportService
) { ) {
const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID
);
const { unsubscribe } = measurementService.subscribe( const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT, MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
props => { props => {
cacheJumpToMeasurementEvent = props; cacheJumpToMeasurementEvent = props;
const { viewportIndex: jumpIndex, measurement, isConsumed } = props; const { viewportId: jumpId, measurement, isConsumed } = props;
if (!measurement || isConsumed) return; if (!measurement || isConsumed) {
return;
}
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) { if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
// Decide on which viewport should handle this // Decide on which viewport should handle this
cacheJumpToMeasurementEvent.cornerstoneViewport = cornerstoneViewportService.getViewportIndexToJump( cacheJumpToMeasurementEvent.cornerstoneViewport =
jumpIndex, cornerstoneViewportService.getViewportIdToJump(
measurement.displaySetInstanceUID, jumpId,
{ referencedImageId: measurement.referencedImageId } measurement.displaySetInstanceUID,
); { referencedImageId: measurement.referencedImageId }
);
} }
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportIndex) { if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
return; return;
} }
_jumpToMeasurement( _jumpToMeasurement(
measurement, measurement,
elementRef, elementRef,
viewportIndex, viewportId,
measurementService, measurementService,
displaySetService, displaySetService,
viewportGridService, viewportGridService,
@ -590,20 +438,22 @@ function _checkForCachedJumpToMeasurementEvents(
measurementService, measurementService,
displaySetService, displaySetService,
elementRef, elementRef,
viewportIndex, viewportId,
displaySets, displaySets,
viewportGridService, viewportGridService,
cornerstoneViewportService cornerstoneViewportService
) { ) {
if (!cacheJumpToMeasurementEvent) return; if (!cacheJumpToMeasurementEvent) {
return;
}
if (cacheJumpToMeasurementEvent.isConsumed) { if (cacheJumpToMeasurementEvent.isConsumed) {
cacheJumpToMeasurementEvent = null; cacheJumpToMeasurementEvent = null;
return; return;
} }
const displaysUIDs = displaySets.map( const displaysUIDs = displaySets.map(displaySet => displaySet.displaySetInstanceUID);
displaySet => displaySet.displaySetInstanceUID if (!displaysUIDs?.length) {
); return;
if (!displaysUIDs?.length) return; }
// Jump to measurement if the measurement exists // Jump to measurement if the measurement exists
const { measurement } = cacheJumpToMeasurementEvent; const { measurement } = cacheJumpToMeasurementEvent;
@ -612,7 +462,7 @@ function _checkForCachedJumpToMeasurementEvents(
_jumpToMeasurement( _jumpToMeasurement(
measurement, measurement,
elementRef, elementRef,
viewportIndex, viewportId,
measurementService, measurementService,
displaySetService, displaySetService,
viewportGridService, viewportGridService,
@ -625,7 +475,7 @@ function _checkForCachedJumpToMeasurementEvents(
function _jumpToMeasurement( function _jumpToMeasurement(
measurement, measurement,
targetElementRef, targetElementRef,
viewportIndex, viewportId,
measurementService, measurementService,
displaySetService, displaySetService,
viewportGridService, viewportGridService,
@ -639,27 +489,19 @@ function _jumpToMeasurement(
return; return;
} }
const referencedDisplaySet = displaySetService.getDisplaySetByUID( const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
displaySetInstanceUID
);
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager // Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
// to set it properly // to set it properly
// setCornerstoneMeasurementActive(measurement); // setCornerstoneMeasurementActive(measurement);
viewportGridService.setActiveViewportIndex(viewportIndex); viewportGridService.setActiveViewportId(viewportId);
const enabledElement = getEnabledElement(targetElement); const enabledElement = getEnabledElement(targetElement);
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (enabledElement) { if (enabledElement) {
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param. // See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
const viewport = enabledElement.viewport as const viewport = enabledElement.viewport as IStackViewport | IVolumeViewport;
| IStackViewport
| IVolumeViewport;
let imageIdIndex = 0; let imageIdIndex = 0;
let viewportCameraDirectionMatch = true; let viewportCameraDirectionMatch = true;
@ -667,14 +509,9 @@ function _jumpToMeasurement(
if (viewport instanceof StackViewport) { if (viewport instanceof StackViewport) {
const imageIds = viewport.getImageIds(); const imageIds = viewport.getImageIds();
imageIdIndex = imageIds.findIndex(imageId => { imageIdIndex = imageIds.findIndex(imageId => {
const { const { SOPInstanceUID: aSOPInstanceUID, frameNumber: aFrameNumber } =
SOPInstanceUID: aSOPInstanceUID, getSOPInstanceAttributes(imageId);
frameNumber: aFrameNumber, return aSOPInstanceUID === SOPInstanceUID && (!frameNumber || frameNumber === aFrameNumber);
} = getSOPInstanceAttributes(imageId);
return (
aSOPInstanceUID === SOPInstanceUID &&
(!frameNumber || frameNumber === aFrameNumber)
);
}); });
} else { } else {
// for volume viewport we can't rely on the imageIdIndex since it can be // for volume viewport we can't rely on the imageIdIndex since it can be
@ -689,10 +526,7 @@ function _jumpToMeasurement(
// should compare abs for both planes since the direction can be flipped // should compare abs for both planes since the direction can be flipped
if ( if (
measurementViewPlane && measurementViewPlane &&
!csUtils.isEqual( !csUtils.isEqual(measurementViewPlane.map(Math.abs), viewportViewPlane.map(Math.abs))
measurementViewPlane.map(Math.abs),
viewportViewPlane.map(Math.abs)
)
) { ) {
viewportCameraDirectionMatch = false; viewportCameraDirectionMatch = false;
} }
@ -716,21 +550,22 @@ function _jumpToMeasurement(
// Component displayName // Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport'; OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
OHIFCornerstoneViewport.defaultProps = {
isJumpToMeasurementDisabled: false,
};
OHIFCornerstoneViewport.propTypes = { OHIFCornerstoneViewport.propTypes = {
viewportIndex: PropTypes.number.isRequired,
displaySets: PropTypes.array.isRequired, displaySets: PropTypes.array.isRequired,
dataSource: PropTypes.object.isRequired, dataSource: PropTypes.object.isRequired,
viewportOptions: PropTypes.object, viewportOptions: PropTypes.object,
displaySetOptions: PropTypes.arrayOf(PropTypes.any), displaySetOptions: PropTypes.arrayOf(PropTypes.any),
servicesManager: PropTypes.object.isRequired, servicesManager: PropTypes.object.isRequired,
onElementEnabled: PropTypes.func, onElementEnabled: PropTypes.func,
isJumpToMeasurementDisabled: PropTypes.bool,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation // Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used // of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render // to set the initial state of the viewport's first image to render
initialImageIdOrIndex: PropTypes.oneOfType([ initialImageIdOrIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
PropTypes.string,
PropTypes.number,
]),
}; };
export default OHIFCornerstoneViewport; export default OHIFCornerstoneViewport;

View File

@ -6,7 +6,7 @@ import ViewportOrientationMarkers from './ViewportOrientationMarkers';
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator'; import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
function CornerstoneOverlays(props) { function CornerstoneOverlays(props) {
const { viewportIndex, element, scrollbarHeight, servicesManager } = props; const { viewportId, element, scrollbarHeight, servicesManager } = props;
const { cornerstoneViewportService } = servicesManager.services; const { cornerstoneViewportService } = servicesManager.services;
const [imageSliceData, setImageSliceData] = useState({ const [imageSliceData, setImageSliceData] = useState({
imageIndex: 0, imageIndex: 0,
@ -18,7 +18,7 @@ function CornerstoneOverlays(props) {
const { unsubscribe } = cornerstoneViewportService.subscribe( const { unsubscribe } = cornerstoneViewportService.subscribe(
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED, cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
props => { props => {
if (props.viewportIndex !== viewportIndex) { if (props.viewportId !== viewportId) {
return; return;
} }
@ -29,15 +29,14 @@ function CornerstoneOverlays(props) {
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, [viewportIndex]); }, [viewportId]);
if (!element) { if (!element) {
return null; return null;
} }
if (viewportData) { if (viewportData) {
const viewportInfo = const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
cornerstoneViewportService.getViewportInfoByIndex(viewportIndex);
if (viewportInfo?.viewportOptions?.customViewportProps?.hideOverlays) { if (viewportInfo?.viewportOptions?.customViewportProps?.hideOverlays) {
return null; return null;
@ -47,7 +46,7 @@ function CornerstoneOverlays(props) {
return ( return (
<div className="noselect"> <div className="noselect">
<ViewportImageScrollbar <ViewportImageScrollbar
viewportIndex={viewportIndex} viewportId={viewportId}
viewportData={viewportData} viewportData={viewportData}
element={element} element={element}
imageSliceData={imageSliceData} imageSliceData={imageSliceData}
@ -59,7 +58,7 @@ function CornerstoneOverlays(props) {
<CustomizableViewportOverlay <CustomizableViewportOverlay
imageSliceData={imageSliceData} imageSliceData={imageSliceData}
viewportData={viewportData} viewportData={viewportData}
viewportIndex={viewportIndex} viewportId={viewportId}
servicesManager={servicesManager} servicesManager={servicesManager}
element={element} element={element}
/> />
@ -74,7 +73,7 @@ function CornerstoneOverlays(props) {
element={element} element={element}
viewportData={viewportData} viewportData={viewportData}
servicesManager={servicesManager} servicesManager={servicesManager}
viewportIndex={viewportIndex} viewportId={viewportId}
/> />
</div> </div>
); );

View File

@ -3,12 +3,7 @@ import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core'; import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui'; import { ViewportOverlay } from '@ohif/ui';
import { import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
formatPN,
formatDICOMDate,
formatDICOMTime,
formatNumberPrecision,
} from './utils';
import { InstanceMetadata } from 'platform/core/src/types'; import { InstanceMetadata } from 'platform/core/src/types';
import { ServicesManager } from '@ohif/core'; import { ServicesManager } from '@ohif/core';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types'; import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
@ -21,7 +16,6 @@ interface OverlayItemProps {
element: any; element: any;
viewportData: any; viewportData: any;
imageSliceData: ImageSliceData; imageSliceData: ImageSliceData;
viewportIndex: number | null;
servicesManager: ServicesManager; servicesManager: ServicesManager;
instance: InstanceMetadata; instance: InstanceMetadata;
customization: any; customization: any;
@ -56,13 +50,9 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
style={{ color: (customization && customization.color) || undefined }} style={{ color: (customization && customization.color) || undefined }}
> >
<span className="mr-1 shrink-0">W:</span> <span className="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 font-light shrink-0"> <span className="ml-1 mr-2 shrink-0 font-light">{windowWidth.toFixed(0)}</span>
{windowWidth.toFixed(0)}
</span>
<span className="mr-1 shrink-0">L:</span> <span className="mr-1 shrink-0">L:</span>
<span className="ml-1 font-light shrink-0"> <span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
{windowCenter.toFixed(0)}
</span>
</div> </div>
); );
} }
@ -114,14 +104,11 @@ function CustomizableViewportOverlay({
element, element,
viewportData, viewportData,
imageSliceData, imageSliceData,
viewportIndex, viewportId,
servicesManager, servicesManager,
}) { }) {
const { const { toolbarService, cornerstoneViewportService, customizationService } =
toolbarService, servicesManager.services;
cornerstoneViewportService,
customizationService,
} = servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null }); const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1); const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]); const [activeTools, setActiveTools] = useState([]);
@ -150,15 +137,10 @@ function CustomizableViewportOverlay({
const instanceNumber = useMemo(() => { const instanceNumber = useMemo(() => {
if (viewportData != null) { if (viewportData != null) {
return _getInstanceNumber( return _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService);
viewportData,
viewportIndex,
imageIndex,
cornerstoneViewportService
);
} }
return null; return null;
}, [viewportData, viewportIndex, imageIndex, cornerstoneViewportService]); }, [viewportData, viewportId, imageIndex, cornerstoneViewportService]);
/** /**
* Initial toolbar state * Initial toolbar state
@ -179,10 +161,7 @@ function CustomizableViewportOverlay({
} }
const { lower, upper } = range; const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel( const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
lower,
upper
);
setVOI({ windowCenter, windowWidth }); setVOI({ windowCenter, windowWidth });
}; };
@ -192,7 +171,7 @@ function CustomizableViewportOverlay({
return () => { return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI); element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
}; };
}, [viewportIndex, viewportData, voi, element]); }, [viewportId, viewportData, voi, element]);
/** /**
* Updating the scale when the viewport changes its zoom * Updating the scale when the viewport changes its zoom
@ -205,9 +184,7 @@ function CustomizableViewportOverlay({
previousCamera.parallelScale !== camera.parallelScale || previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale previousCamera.scale !== camera.scale
) { ) {
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex( const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
viewportIndex
);
if (!viewport) { if (!viewport) {
return; return;
@ -226,8 +203,7 @@ function CustomizableViewportOverlay({
const { spacing } = imageData; const { spacing } = imageData;
// convert parallel scale to scale // convert parallel scale to scale
const scale = const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
setScale(scale); setScale(scale);
} }
}; };
@ -237,7 +213,7 @@ function CustomizableViewportOverlay({
return () => { return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale); element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
}; };
}, [viewportIndex, viewportData, cornerstoneViewportService, element]); }, [viewportId, viewportData, cornerstoneViewportService, element]);
/** /**
* Updating the active tools when the toolbar changes * Updating the active tools when the toolbar changes
@ -262,7 +238,7 @@ function CustomizableViewportOverlay({
element, element,
viewportData, viewportData,
imageSliceData, imageSliceData,
viewportIndex, viewportId,
servicesManager, servicesManager,
customization: item, customization: item,
formatters: { formatters: {
@ -296,7 +272,7 @@ function CustomizableViewportOverlay({
element, element,
viewportData, viewportData,
imageSliceData, imageSliceData,
viewportIndex, viewportId,
servicesManager, servicesManager,
customizationService, customizationService,
instance, instance,
@ -343,9 +319,7 @@ function CustomizableViewportOverlay({
return ( return (
<> <>
{items.map((item, i) => ( {items.map((item, i) => (
<div key={`bottomLeftOverlayItem_${i}`}> <div key={`bottomLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
{_renderOverlayItem(item)}
</div>
))} ))}
</> </>
); );
@ -356,9 +330,7 @@ function CustomizableViewportOverlay({
return ( return (
<> <>
{items.map((item, i) => ( {items.map((item, i) => (
<div key={`bottomRightOverlayItem_${i}`}> <div key={`bottomRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
{_renderOverlayItem(item)}
</div>
))} ))}
</> </>
); );
@ -379,7 +351,7 @@ function _getViewportInstance(viewportData, imageIndex) {
if (viewportData.viewportType === Enums.ViewportType.STACK) { if (viewportData.viewportType === Enums.ViewportType.STACK) {
imageId = viewportData.data.imageIds[imageIndex]; imageId = viewportData.data.imageIds[imageIndex];
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) { } else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
const volumes = viewportData.volumes; const volumes = viewportData.data;
if (volumes && volumes.length == 1) { if (volumes && volumes.length == 1) {
const volume = volumes[0]; const volume = volumes[0];
imageId = volume.imageIds[imageIndex]; imageId = volume.imageIds[imageIndex];
@ -388,12 +360,7 @@ function _getViewportInstance(viewportData, imageIndex) {
return imageId ? metaData.get('instance', imageId) || {} : {}; return imageId ? metaData.get('instance', imageId) || {} : {};
} }
function _getInstanceNumber( function _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService) {
viewportData,
viewportIndex,
imageIndex,
cornerstoneViewportService
) {
let instanceNumber; let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) { if (viewportData.viewportType === Enums.ViewportType.STACK) {
@ -406,7 +373,7 @@ function _getInstanceNumber(
instanceNumber = _getInstanceNumberFromVolume( instanceNumber = _getInstanceNumberFromVolume(
viewportData, viewportData,
imageIndex, imageIndex,
viewportIndex, viewportId,
cornerstoneViewportService cornerstoneViewportService
); );
} }
@ -436,12 +403,7 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
// Since volume viewports can be in any view direction, they can render // Since volume viewports can be in any view direction, they can render
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber // a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber // Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
function _getInstanceNumberFromVolume( function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewportService) {
viewportData,
imageIndex,
viewportIndex,
cornerstoneViewportService
) {
const volumes = viewportData.volumes; const volumes = viewportData.volumes;
// Todo: support fusion of acquisition plane which has instanceNumber // Todo: support fusion of acquisition plane which has instanceNumber
@ -452,9 +414,7 @@ function _getInstanceNumberFromVolume(
const volume = volumes[0]; const volume = volumes[0];
const { direction, imageIds } = volume; const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex( const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
viewportIndex
);
if (!cornerstoneViewport) { if (!cornerstoneViewport) {
return; return;
@ -477,8 +437,7 @@ function _getInstanceNumberFromVolume(
return {}; return {};
} }
const { instanceNumber } = const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
metaData.get('generalImageModule', imageId) || {};
return parseInt(instanceNumber); return parseInt(instanceNumber);
} }
} }
@ -486,7 +445,7 @@ function _getInstanceNumberFromVolume(
CustomizableViewportOverlay.propTypes = { CustomizableViewportOverlay.propTypes = {
viewportData: PropTypes.object, viewportData: PropTypes.object,
imageIndex: PropTypes.number, imageIndex: PropTypes.number,
viewportIndex: PropTypes.number, viewportId: PropTypes.string,
}; };
export default CustomizableViewportOverlay; export default CustomizableViewportOverlay;

View File

@ -7,34 +7,24 @@ import { ServicesManger } from '@ohif/core';
function CornerstoneImageScrollbar({ function CornerstoneImageScrollbar({
viewportData, viewportData,
viewportIndex, viewportId,
element, element,
imageSliceData, imageSliceData,
setImageSliceData, setImageSliceData,
scrollbarHeight, scrollbarHeight,
servicesManager, servicesManager,
}) { }) {
const { const { cineService, cornerstoneViewportService } = (servicesManager as ServicesManger).services;
cineService,
cornerstoneViewportService,
} = (servicesManager as ServicesManger).services;
const onImageScrollbarChange = (imageIndex, viewportIndex) => { const onImageScrollbarChange = (imageIndex, viewportId) => {
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex( const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
viewportIndex
);
const viewportId = viewportInfo.getViewportId();
const viewport = cornerstoneViewportService.getCornerstoneViewport(
viewportId
);
const { isCineEnabled } = cineService.getState(); const { isCineEnabled } = cineService.getState();
if (isCineEnabled) { if (isCineEnabled) {
// on image scrollbar change, stop the CINE if it is playing // on image scrollbar change, stop the CINE if it is playing
cineService.stopClip(element); cineService.stopClip(element);
cineService.setCine({ id: viewportIndex, isPlaying: false }); cineService.setCine({ id: viewportId, isPlaying: false });
} }
csToolsUtils.jumpToSlice(viewport.element, { csToolsUtils.jumpToSlice(viewport.element, {
@ -48,9 +38,7 @@ function CornerstoneImageScrollbar({
return; return;
} }
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex( const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
viewportIndex
);
if (!viewport) { if (!viewport) {
return; return;
@ -79,7 +67,7 @@ function CornerstoneImageScrollbar({
const { imageIndex, numberOfSlices } = sliceData; const { imageIndex, numberOfSlices } = sliceData;
setImageSliceData({ imageIndex, numberOfSlices }); setImageSliceData({ imageIndex, numberOfSlices });
} }
}, [viewportIndex, viewportData]); }, [viewportId, viewportData]);
useEffect(() => { useEffect(() => {
if (viewportData?.viewportType !== Enums.ViewportType.STACK) { if (viewportData?.viewportType !== Enums.ViewportType.STACK) {
@ -95,16 +83,10 @@ function CornerstoneImageScrollbar({
}); });
}; };
element.addEventListener( element.addEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex);
Enums.Events.STACK_VIEWPORT_SCROLL,
updateStackIndex
);
return () => { return () => {
element.removeEventListener( element.removeEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex);
Enums.Events.STACK_VIEWPORT_SCROLL,
updateStackIndex
);
}; };
}, [viewportData, element]); }, [viewportData, element]);
@ -122,19 +104,14 @@ function CornerstoneImageScrollbar({
element.addEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex); element.addEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
return () => { return () => {
element.removeEventListener( element.removeEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
Enums.Events.VOLUME_NEW_IMAGE,
updateVolumeIndex
);
}; };
}, [viewportData, element]); }, [viewportData, element]);
return ( return (
<ImageScrollbar <ImageScrollbar
onChange={evt => onImageScrollbarChange(evt, viewportIndex)} onChange={evt => onImageScrollbarChange(evt, viewportId)}
max={ max={imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0}
imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0
}
height={scrollbarHeight} height={scrollbarHeight}
value={imageSliceData.imageIndex} value={imageSliceData.imageIndex}
/> />
@ -143,7 +120,7 @@ function CornerstoneImageScrollbar({
CornerstoneImageScrollbar.propTypes = { CornerstoneImageScrollbar.propTypes = {
viewportData: PropTypes.object, viewportData: PropTypes.object,
viewportIndex: PropTypes.number.isRequired, viewportId: PropTypes.string.isRequired,
element: PropTypes.instanceOf(Element), element: PropTypes.instanceOf(Element),
scrollbarHeight: PropTypes.string, scrollbarHeight: PropTypes.string,
imageSliceData: PropTypes.object.isRequired, imageSliceData: PropTypes.object.isRequired,

View File

@ -33,26 +33,14 @@ function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
}; };
useEffect(() => { useEffect(() => {
element.addEventListener( element.addEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, setLoadingState);
Enums.Events.STACK_VIEWPORT_SCROLL,
setLoadingState
);
element.addEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState); element.addEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState);
element.addEventListener( element.addEventListener(Enums.Events.STACK_NEW_IMAGE, setFinishLoadingState);
Enums.Events.STACK_NEW_IMAGE,
setFinishLoadingState
);
return () => { return () => {
element.removeEventListener( element.removeEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, setLoadingState);
Enums.Events.STACK_VIEWPORT_SCROLL,
setLoadingState
);
element.removeEventListener( element.removeEventListener(Enums.Events.STACK_NEW_IMAGE, setFinishLoadingState);
Enums.Events.STACK_NEW_IMAGE,
setFinishLoadingState
);
element.removeEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState); element.removeEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState);
}; };
@ -61,8 +49,8 @@ function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
if (error) { if (error) {
return ( return (
<> <>
<div className="bg-black opacity-50 absolute h-full w-full top-0 left-0"> <div className="absolute top-0 left-0 h-full w-full bg-black opacity-50">
<div className="flex transparent items-center justify-center w-full h-full"> <div className="transparent flex h-full w-full items-center justify-center">
<p className="text-primary-light text-xl font-light"> <p className="text-primary-light text-xl font-light">
<h4>Error Loading Image</h4> <h4>Error Loading Image</h4>
<p>An error has occurred.</p> <p>An error has occurred.</p>
@ -78,8 +66,8 @@ function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
return ( return (
// IMPORTANT: we need to use the pointer-events-none class to prevent the loading indicator from // IMPORTANT: we need to use the pointer-events-none class to prevent the loading indicator from
// interacting with the mouse, since scrolling should propagate to the viewport underneath // interacting with the mouse, since scrolling should propagate to the viewport underneath
<div className="pointer-events-none bg-black opacity-50 absolute h-full w-full top-0 left-0"> <div className="pointer-events-none absolute top-0 left-0 h-full w-full bg-black opacity-50">
<div className="flex transparent items-center justify-center w-full h-full"> <div className="transparent flex h-full w-full items-center justify-center">
<p className="text-primary-light text-xl font-light">Loading...</p> <p className="text-primary-light text-xl font-light">Loading...</p>
</div> </div>
</div> </div>

View File

@ -13,16 +13,13 @@ import { vec3 } from 'gl-matrix';
import './ViewportOrientationMarkers.css'; import './ViewportOrientationMarkers.css';
const { const { getOrientationStringLPS, invertOrientationStringLPS } = utilities.orientation;
getOrientationStringLPS,
invertOrientationStringLPS,
} = utilities.orientation;
function ViewportOrientationMarkers({ function ViewportOrientationMarkers({
element, element,
viewportData, viewportData,
imageSliceData, imageSliceData,
viewportIndex, viewportId,
servicesManager, servicesManager,
orientationMarkers = ['top', 'left'], orientationMarkers = ['top', 'left'],
}) { }) {
@ -33,9 +30,7 @@ function ViewportOrientationMarkers({
const { cornerstoneViewportService } = servicesManager.services; const { cornerstoneViewportService } = servicesManager.services;
useEffect(() => { useEffect(() => {
const cameraModifiedListener = ( const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
evt: Types.EventTypes.CameraModifiedEvent
) => {
const { rotation, previousCamera, camera } = evt.detail; const { rotation, previousCamera, camera } = evt.detail;
if (rotation !== undefined) { if (rotation !== undefined) {
@ -57,16 +52,10 @@ function ViewportOrientationMarkers({
} }
}; };
element.addEventListener( element.addEventListener(Enums.Events.CAMERA_MODIFIED, cameraModifiedListener);
Enums.Events.CAMERA_MODIFIED,
cameraModifiedListener
);
return () => { return () => {
element.removeEventListener( element.removeEventListener(Enums.Events.CAMERA_MODIFIED, cameraModifiedListener);
Enums.Events.CAMERA_MODIFIED,
cameraModifiedListener
);
}; };
}, []); }, []);
@ -85,8 +74,7 @@ function ViewportOrientationMarkers({
return false; return false;
} }
({ rowCosines, columnCosines } = ({ rowCosines, columnCosines } = metaData.get('imagePlaneModule', imageId) || {});
metaData.get('imagePlaneModule', imageId) || {});
} else { } else {
if (!element || !getEnabledElement(element)) { if (!element || !getEnabledElement(element)) {
return ''; return '';
@ -114,9 +102,7 @@ function ViewportOrientationMarkers({
flipHorizontal flipHorizontal
); );
const ohifViewport = cornerstoneViewportService.getViewportInfoByIndex( const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
viewportIndex
);
if (!ohifViewport) { if (!ohifViewport) {
console.log('ViewportOrientationMarkers::No viewport'); console.log('ViewportOrientationMarkers::No viewport');
@ -126,9 +112,7 @@ function ViewportOrientationMarkers({
// Todo: probably this can be done in a better way in which we identify bright // Todo: probably this can be done in a better way in which we identify bright
// background // background
const isLight = backgroundColor const isLight = backgroundColor ? csUtils.isEqual(backgroundColor, [1, 1, 1]) : false;
? csUtils.isEqual(backgroundColor, [1, 1, 1])
: false;
return orientationMarkers.map((m, index) => ( return orientationMarkers.map((m, index) => (
<div <div
@ -174,13 +158,7 @@ ViewportOrientationMarkers.defaultProps = {
* @param {*} rotation in degrees * @param {*} rotation in degrees
* @returns * @returns
*/ */
function _getOrientationMarkers( function _getOrientationMarkers(rowCosines, columnCosines, rotation, flipVertical, flipHorizontal) {
rowCosines,
columnCosines,
rotation,
flipVertical,
flipHorizontal
) {
const rowString = getOrientationStringLPS(rowCosines); const rowString = getOrientationStringLPS(rowCosines);
const columnString = getOrientationStringLPS(columnCosines); const columnString = getOrientationStringLPS(columnCosines);
const oppositeRowString = invertOrientationStringLPS(rowString); const oppositeRowString = invertOrientationStringLPS(rowString);

View File

@ -11,13 +11,10 @@ function CornerstoneViewportOverlay({
element, element,
viewportData, viewportData,
imageSliceData, imageSliceData,
viewportIndex, viewportId,
servicesManager, servicesManager,
}) { }) {
const { const { cornerstoneViewportService, toolbarService } = servicesManager.services;
cornerstoneViewportService,
toolbarService,
} = servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null }); const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1); const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]); const [activeTools, setActiveTools] = useState([]);
@ -60,10 +57,7 @@ function CornerstoneViewportOverlay({
} }
const { lower, upper } = range; const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel( const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
lower,
upper
);
setVOI({ windowCenter, windowWidth }); setVOI({ windowCenter, windowWidth });
}; };
@ -73,7 +67,7 @@ function CornerstoneViewportOverlay({
return () => { return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI); element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
}; };
}, [viewportIndex, viewportData, voi, element]); }, [viewportId, viewportData, voi, element]);
/** /**
* Updating the scale when the viewport changes its zoom * Updating the scale when the viewport changes its zoom
@ -86,9 +80,7 @@ function CornerstoneViewportOverlay({
previousCamera.parallelScale !== camera.parallelScale || previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale previousCamera.scale !== camera.scale
) { ) {
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex( const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
viewportIndex
);
if (!viewport) { if (!viewport) {
return; return;
@ -107,8 +99,7 @@ function CornerstoneViewportOverlay({
const { spacing } = imageData; const { spacing } = imageData;
// convert parallel scale to scale // convert parallel scale to scale
const scale = const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
setScale(scale); setScale(scale);
} }
}; };
@ -118,7 +109,7 @@ function CornerstoneViewportOverlay({
return () => { return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale); element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
}; };
}, [viewportIndex, viewportData]); }, [viewportId, viewportData]);
const getTopLeftContent = useCallback(() => { const getTopLeftContent = useCallback(() => {
const { windowWidth, windowCenter } = voi; const { windowWidth, windowCenter } = voi;
@ -168,7 +159,7 @@ function CornerstoneViewportOverlay({
instanceNumber = _getInstanceNumberFromVolume( instanceNumber = _getInstanceNumberFromVolume(
viewportData, viewportData,
imageIndex, imageIndex,
viewportIndex, viewportId,
cornerstoneViewportService cornerstoneViewportService
); );
} }
@ -183,15 +174,13 @@ function CornerstoneViewportOverlay({
</span> </span>
</div> </div>
); );
}, [imageSliceData, viewportData, viewportIndex]); }, [imageSliceData, viewportData, viewportId]);
if (!viewportData) { if (!viewportData) {
return null; return null;
} }
const ohifViewport = cornerstoneViewportService.getViewportInfoByIndex( const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
viewportIndex
);
if (!ohifViewport) { if (!ohifViewport) {
return null; return null;
@ -201,9 +190,7 @@ function CornerstoneViewportOverlay({
// Todo: probably this can be done in a better way in which we identify bright // Todo: probably this can be done in a better way in which we identify bright
// background // background
const isLight = backgroundColor const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
? utilities.isEqual(backgroundColor, [1, 1, 1])
: false;
return ( return (
<ViewportOverlay <ViewportOverlay
@ -240,7 +227,7 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
function _getInstanceNumberFromVolume( function _getInstanceNumberFromVolume(
viewportData, viewportData,
imageIndex, imageIndex,
viewportIndex, viewportId,
cornerstoneViewportService cornerstoneViewportService
) { ) {
const volumes = viewportData.volumes; const volumes = viewportData.volumes;
@ -253,9 +240,7 @@ function _getInstanceNumberFromVolume(
const volume = volumes[0]; const volume = volumes[0];
const { direction, imageIds } = volume; const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex( const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
viewportIndex
);
if (!cornerstoneViewport) { if (!cornerstoneViewport) {
return; return;
@ -278,8 +263,7 @@ function _getInstanceNumberFromVolume(
return {}; return {};
} }
const { instanceNumber } = const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
metaData.get('generalImageModule', imageId) || {};
return parseInt(instanceNumber); return parseInt(instanceNumber);
} }
} }
@ -287,7 +271,7 @@ function _getInstanceNumberFromVolume(
CornerstoneViewportOverlay.propTypes = { CornerstoneViewportOverlay.propTypes = {
viewportData: PropTypes.object, viewportData: PropTypes.object,
imageIndex: PropTypes.number, imageIndex: PropTypes.number,
viewportIndex: PropTypes.number, viewportId: PropTypes.string,
servicesManager: PropTypes.instanceOf(ServicesManager), servicesManager: PropTypes.instanceOf(ServicesManager),
}; };

View File

@ -75,22 +75,16 @@ export function formatPN(name) {
* Gets compression type * Gets compression type
* *
* @param {number} imageId * @param {number} imageId
* @returns {string} comrpession type. * @returns {string} compression type.
*/ */
export function getCompression(imageId) { export function getCompression(imageId) {
const generalImageModule = metaData.get('generalImageModule', imageId) || {}; const generalImageModule = metaData.get('generalImageModule', imageId) || {};
const { const { lossyImageCompression, lossyImageCompressionRatio, lossyImageCompressionMethod } =
lossyImageCompression, generalImageModule;
lossyImageCompressionRatio,
lossyImageCompressionMethod,
} = generalImageModule;
if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') { if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') {
const compressionMethod = lossyImageCompressionMethod || 'Lossy: '; const compressionMethod = lossyImageCompressionMethod || 'Lossy: ';
const compressionRatio = formatNumberPrecision( const compressionRatio = formatNumberPrecision(lossyImageCompressionRatio, 2);
lossyImageCompressionRatio,
2
);
return compressionMethod + compressionRatio + ' : 1'; return compressionMethod + compressionRatio + ' : 1';
} }

View File

@ -56,9 +56,7 @@ function commandsModule({
// filter by allowed selected tools from config property (if there is any) // filter by allowed selected tools from config property (if there is any)
const isToolAllowed = const isToolAllowed =
!optionsToUse.allowedSelectedTools || !optionsToUse.allowedSelectedTools ||
optionsToUse.allowedSelectedTools.includes( optionsToUse.allowedSelectedTools.includes(firstAnnotationSelected?.metadata?.toolName);
firstAnnotationSelected?.metadata?.toolName
);
if (isToolAllowed) { if (isToolAllowed) {
optionsToUse.nearbyToolData = firstAnnotationSelected; optionsToUse.nearbyToolData = firstAnnotationSelected;
} else { } else {
@ -88,10 +86,7 @@ function commandsModule({
}, },
getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) { getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
return ( return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates);
nearbyToolData ??
cstUtils.getAnnotationNearPoint(element, canvasCoordinates)
);
}, },
getNearbyAnnotation({ element, canvasCoordinates }) { getNearbyAnnotation({ element, canvasCoordinates }) {
const nearbyToolData = actions.getNearbyToolData({ const nearbyToolData = actions.getNearbyToolData({
@ -108,18 +103,14 @@ function commandsModule({
} }
const { renderingEngineId, viewportId } = enabledElement; const { renderingEngineId, viewportId } = enabledElement;
const toolGroup = ToolGroupManager.getToolGroupForViewport( const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId, renderingEngineId);
viewportId,
renderingEngineId
);
const toolInstance = toolGroup.getToolInstance(toolName); const toolInstance = toolGroup.getToolInstance(toolName);
return toolInstance?.constructor?.isAnnotation ?? true; return toolInstance?.constructor?.isAnnotation ?? true;
}; };
return nearbyToolData?.metadata?.toolName && return nearbyToolData?.metadata?.toolName && isAnnotation(nearbyToolData.metadata.toolName)
isAnnotation(nearbyToolData.metadata.toolName)
? nearbyToolData ? nearbyToolData
: null; : null;
}, },
@ -152,11 +143,7 @@ function commandsModule({
label, label,
}); });
measurementService.update( measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
updatedMeasurement.uid,
updatedMeasurement,
true
);
}, },
false false
); );
@ -167,7 +154,7 @@ function commandsModule({
* @param props - containing the updates to apply * @param props - containing the updates to apply
* @param props.measurementKey - chooses the measurement key to apply the * @param props.measurementKey - chooses the measurement key to apply the
* code to. This will typically be finding or site to apply a * code to. This will typically be finding or site to apply a
* finind code or a findingSites code. * finding code or a findingSites code.
* @param props.code - A coding scheme value from DICOM, including: * @param props.code - A coding scheme value from DICOM, including:
* * CodeValue - the language independent code, for example '1234' * * CodeValue - the language independent code, for example '1234'
* * CodingSchemeDesignator - the issue of the code value * * CodingSchemeDesignator - the issue of the code value
@ -219,39 +206,49 @@ function commandsModule({
} }
} }
} }
measurementService.update( measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
updatedMeasurement.uid,
updatedMeasurement,
true
);
}, },
// Retrieve value commands // Retrieve value commands
getActiveViewportEnabledElement: _getActiveViewportEnabledElement, getActiveViewportEnabledElement: _getActiveViewportEnabledElement,
setViewportActive: ({ viewportId }) => { setViewportActive: ({ viewportId }) => {
const viewportInfo = cornerstoneViewportService.getViewportInfo( const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
viewportId
);
if (!viewportInfo) { if (!viewportInfo) {
console.warn('No viewport found for viewportId:', viewportId); console.warn('No viewport found for viewportId:', viewportId);
return; return;
} }
const viewportIndex = viewportInfo.getViewportIndex(); viewportGridService.setActiveViewportId(viewportId);
viewportGridService.setActiveViewportIndex(viewportIndex);
}, },
arrowTextCallback: ({ callback, data }) => { arrowTextCallback: ({ callback, data }) => {
callInputDialog(uiDialogService, data, callback); callInputDialog(uiDialogService, data, callback);
}, },
cleanUpCrosshairs: () => {
// if the crosshairs tool is active, deactivate it and set window level active
// since we are going back to main non-mpr HP
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
if (activeViewportToolGroup._toolInstances?.Crosshairs?.mode === Enums.ToolModes.Active) {
actions.toolbarServiceRecordInteraction({
interactionType: 'tool',
commands: [
{
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
}
},
toggleCine: () => { toggleCine: () => {
const { viewports } = viewportGridService.getState(); const { viewports } = viewportGridService.getState();
const { isCineEnabled } = cineService.getState(); const { isCineEnabled } = cineService.getState();
cineService.setIsCineEnabled(!isCineEnabled); cineService.setIsCineEnabled(!isCineEnabled);
toolbarService.setButton('Cine', { props: { isActive: !isCineEnabled } }); toolbarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
viewports.forEach((_, index) => viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
cineService.setCine({ id: index, isPlaying: false })
);
}, },
setWindowLevel({ window, level, toolGroupId }) { setWindowLevel({ window, level, toolGroupId }) {
// convert to numbers // convert to numbers
@ -259,9 +256,7 @@ function commandsModule({
const windowCenterNum = Number(level); const windowCenterNum = Number(level);
const { viewportId } = _getActiveViewportEnabledElement(); const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = toolGroupService.getToolGroupForViewport( const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
viewportId
);
if (toolGroupId && toolGroupId !== viewportToolGroupId) { if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return; return;
@ -271,10 +266,7 @@ function commandsModule({
const renderingEngine = cornerstoneViewportService.getRenderingEngine(); const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewport = renderingEngine.getViewport(viewportId); const viewport = renderingEngine.getViewport(viewportId);
const { lower, upper } = csUtils.windowLevel.toLowHighRange( const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum);
windowWidthNum,
windowCenterNum
);
viewport.setProperties({ viewport.setProperties({
voiRange: { voiRange: {
@ -291,8 +283,12 @@ function commandsModule({
toolbarServiceRecordInteraction: props => { toolbarServiceRecordInteraction: props => {
toolbarService.recordInteraction(props); toolbarService.recordInteraction(props);
}, },
// Enable or disable a toggleable command, without calling the activation
setToolActive: ({ toolName, toolGroupId = null }) => { // Used to setup already active tools from hanging protocols
setToolbarToggled: props => {
toolbarService.setToggled(props.toolId, props.isActive ?? true);
},
setToolActive: ({ toolName, toolGroupId = null, toggledState }) => {
if (toolName === 'Crosshairs') { if (toolName === 'Crosshairs') {
const activeViewportToolGroup = toolGroupService.getToolGroup(null); const activeViewportToolGroup = toolGroupService.getToolGroup(null);
@ -309,29 +305,15 @@ function commandsModule({
} }
} }
const { viewports } = viewportGridService.getState() || { const { viewports } = viewportGridService.getState();
viewports: [],
};
const toolGroup = toolGroupService.getToolGroup(toolGroupId); if (!viewports.size) {
const toolGroupViewportIds = toolGroup?.getViewportIds?.();
// if toolGroup has been destroyed, or its viewports have been removed
if (!toolGroupViewportIds || !toolGroupViewportIds.length) {
return; return;
} }
const filteredViewports = viewports.filter(viewport => { const toolGroup = toolGroupService.getToolGroup(toolGroupId);
if (!viewport.viewportOptions) {
return false;
}
return toolGroupViewportIds.includes( if (!toolGroup) {
viewport.viewportOptions.viewportId
);
});
if (!filteredViewports.length) {
return; return;
} }
@ -357,6 +339,14 @@ function commandsModule({
toolGroup.setToolPassive(activeToolName); toolGroup.setToolPassive(activeToolName);
} }
} }
// If there is a toggle state, then simply set the enabled/disabled state without
// setting the tool active.
if (toggledState != null) {
toggledState ? toolGroup.setToolEnabled(toolName) : toolGroup.setToolDisabled(toolName);
return;
}
// Set the new toolName to be active // Set the new toolName to be active
toolGroup.setToolActive(toolName, { toolGroup.setToolActive(toolName, {
bindings: [ bindings: [
@ -367,13 +357,9 @@ function commandsModule({
}); });
}, },
showDownloadViewportModal: () => { showDownloadViewportModal: () => {
const { activeViewportIndex } = viewportGridService.getState(); const { activeViewportId } = viewportGridService.getState();
if ( if (!cornerstoneViewportService.getCornerstoneViewport(activeViewportId)) {
!cornerstoneViewportService.getCornerstoneViewportByIndex(
activeViewportIndex
)
) {
// Cannot download a non-cornerstone viewport (image). // Cannot download a non-cornerstone viewport (image).
uiNotificationService.show({ uiNotificationService.show({
title: 'Download Image', title: 'Download Image',
@ -390,7 +376,7 @@ function commandsModule({
content: CornerstoneViewportDownloadForm, content: CornerstoneViewportDownloadForm,
title: 'Download High Quality Image', title: 'Download High Quality Image',
contentProps: { contentProps: {
activeViewportIndex, activeViewportId,
onClose: uiModalService.hide, onClose: uiModalService.hide,
cornerstoneViewportService, cornerstoneViewportService,
}, },
@ -457,11 +443,9 @@ function commandsModule({
const { viewport } = enabledElement; const { viewport } = enabledElement;
if (viewport instanceof StackViewport) { const { invert } = viewport.getProperties();
const { invert } = viewport.getProperties(); viewport.setProperties({ invert: !invert });
viewport.setProperties({ invert: !invert }); viewport.render();
viewport.render();
}
}, },
resetViewport: () => { resetViewport: () => {
const enabledElement = _getActiveViewportEnabledElement(); const enabledElement = _getActiveViewportEnabledElement();
@ -476,7 +460,7 @@ function commandsModule({
viewport.resetProperties(); viewport.resetProperties();
viewport.resetCamera(); viewport.resetCamera();
} else { } else {
// Todo: add reset properties for volume viewport viewport.resetProperties();
viewport.resetCamera(); viewport.resetCamera();
} }
@ -514,9 +498,7 @@ function commandsModule({
} }
viewport = enabledElement.viewport; viewport = enabledElement.viewport;
} else { } else {
viewport = cornerstoneViewportService.getCornerstoneViewport( viewport = cornerstoneViewportService.getCornerstoneViewport(gridViewport.id);
gridViewport.id
);
} }
// Get number of slices // Get number of slices
@ -526,14 +508,12 @@ function commandsModule({
if (viewport instanceof StackViewport) { if (viewport instanceof StackViewport) {
numberOfSlices = viewport.getImageIds().length; numberOfSlices = viewport.getImageIds().length;
} else if (viewport instanceof VolumeViewport) { } else if (viewport instanceof VolumeViewport) {
numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport) numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport).numberOfSlices;
.numberOfSlices;
} else { } else {
throw new Error('Unsupported viewport type'); throw new Error('Unsupported viewport type');
} }
const jumpIndex = const jumpIndex = imageIndex < 0 ? numberOfSlices + imageIndex : imageIndex;
imageIndex < 0 ? numberOfSlices + imageIndex : imageIndex;
if (jumpIndex >= numberOfSlices || jumpIndex < 0) { if (jumpIndex >= numberOfSlices || jumpIndex < 0) {
throw new Error(`Can't jump to ${imageIndex}`); throw new Error(`Can't jump to ${imageIndex}`);
} }
@ -554,15 +534,8 @@ function commandsModule({
cstUtils.scroll(viewport, options); cstUtils.scroll(viewport, options);
}, },
setViewportColormap: ({ setViewportColormap: ({ viewportId, displaySetInstanceUID, colormap, immediate = false }) => {
viewportIndex, const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
displaySetInstanceUID,
colormap,
immediate = false,
}) => {
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
const actorEntries = viewport.getActors(); const actorEntries = viewport.getActors();
@ -578,37 +551,29 @@ function commandsModule({
viewport.render(); viewport.render();
} }
}, },
incrementActiveViewport: () => { changeActiveViewport: ({ direction = 1 }) => {
const { activeViewportIndex, viewports } = viewportGridService.getState(); const { activeViewportId, viewports } = viewportGridService.getState();
const nextViewportIndex = (activeViewportIndex + 1) % viewports.length; const viewportIds = Array.from(viewports.keys());
viewportGridService.setActiveViewportIndex(nextViewportIndex); const currentIndex = viewportIds.indexOf(activeViewportId);
},
decrementActiveViewport: () => {
const { activeViewportIndex, viewports } = viewportGridService.getState();
const nextViewportIndex = const nextViewportIndex =
(activeViewportIndex - 1 + viewports.length) % viewports.length; (currentIndex + direction + viewportIds.length) % viewportIds.length;
viewportGridService.setActiveViewportIndex(nextViewportIndex); viewportGridService.setActiveViewportId(viewportIds[nextViewportIndex] as string);
}, },
toggleStackImageSync: ({ toggledState }) => { toggleStackImageSync: ({ toggledState }) => {
toggleStackImageSync({ toggleStackImageSync({
getEnabledElement,
servicesManager, servicesManager,
toggledState, toggledState,
}); });
}, },
toggleReferenceLines: ({ toggledState }) => { setSourceViewportForReferenceLinesTool: ({ toggledState, viewportId }) => {
const { activeViewportIndex } = viewportGridService.getState(); if (!viewportId) {
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex( const { activeViewportId } = viewportGridService.getState();
activeViewportIndex viewportId = activeViewportId;
);
const viewportId = viewportInfo.getViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toggledState) {
toolGroup.setToolDisabled(ReferenceLinesTool.toolName);
} }
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
toolGroup.setToolConfiguration( toolGroup.setToolConfiguration(
ReferenceLinesTool.toolName, ReferenceLinesTool.toolName,
{ {
@ -616,7 +581,9 @@ function commandsModule({
}, },
true // overwrite true // overwrite
); );
toolGroup.setToolEnabled(ReferenceLinesTool.toolName); },
storePresentation: ({ viewportId }) => {
cornerstoneViewportService.storePresentation({ viewportId });
}, },
}; };
@ -673,10 +640,11 @@ function commandsModule({
options: { rotation: -90 }, options: { rotation: -90 },
}, },
incrementActiveViewport: { incrementActiveViewport: {
commandFn: actions.incrementActiveViewport, commandFn: actions.changeActiveViewport,
}, },
decrementActiveViewport: { decrementActiveViewport: {
commandFn: actions.decrementActiveViewport, commandFn: actions.changeActiveViewport,
options: { direction: -1 },
}, },
flipViewportHorizontal: { flipViewportHorizontal: {
commandFn: actions.flipViewportHorizontal, commandFn: actions.flipViewportHorizontal,
@ -739,8 +707,17 @@ function commandsModule({
toggleStackImageSync: { toggleStackImageSync: {
commandFn: actions.toggleStackImageSync, commandFn: actions.toggleStackImageSync,
}, },
toggleReferenceLines: { setSourceViewportForReferenceLinesTool: {
commandFn: actions.toggleReferenceLines, commandFn: actions.setSourceViewportForReferenceLinesTool,
},
storePresentation: {
commandFn: actions.storePresentation,
},
setToolbarToggled: {
commandFn: actions.setToolbarToggled,
},
cleanUpCrosshairs: {
commandFn: actions.cleanUpCrosshairs,
}, },
}; };

View File

@ -0,0 +1,97 @@
import React, { useEffect } from 'react';
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
import { Enums, eventTarget } from '@cornerstonejs/core';
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
const { toolbarService, customizationService } = servicesManager.services;
const [{ isCineEnabled, cines }, cineService] = useCine();
const [{ activeViewportId }] = useViewportGrid();
const { component: CinePlayerComponent = CinePlayer } =
customizationService.get('cinePlayer') ?? {};
const handleCineClose = () => {
toolbarService.recordInteraction({
groupId: 'MoreTools',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleCine',
commandOptions: {},
toolName: 'cine',
context: 'CORNERSTONE',
},
],
});
};
const cineHandler = () => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
return;
}
const cine = cines[viewportId];
const isPlaying = cine.isPlaying || false;
const frameRate = cine.frameRate || 24;
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(enabledVPElement);
}
};
useEffect(() => {
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, cineHandler);
return () => {
cineService.setCine({ id: viewportId, isPlaying: false });
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, cineHandler);
};
}, [enabledVPElement]);
useEffect(() => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
return;
}
cineHandler();
return () => {
if (enabledVPElement && cines?.[viewportId]?.isPlaying) {
cineService.stopClip(enabledVPElement);
}
};
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
const cine = cines[viewportId];
const isPlaying = (cine && cine.isPlaying) || false;
return (
isCineEnabled && (
<CinePlayerComponent
className="absolute left-1/2 bottom-3 -translate-x-1/2"
isPlaying={isPlaying}
onClose={handleCineClose}
onPlayPauseChange={isPlaying =>
cineService.setCine({
id: activeViewportId,
isPlaying,
})
}
onFrameRateChange={frameRate =>
cineService.setCine({
id: activeViewportId,
frameRate,
})
}
/>
)
);
}
export default WrappedCinePlayer;

View File

@ -0,0 +1,3 @@
import CinePlayer from './CinePlayer';
export default CinePlayer;

Some files were not shown because too many files have changed in this diff Show More