Compare commits

..
16 Commits
Author SHA1 Message Date
ohif-bot 19bdcbb453 chore(version): Update package versions [skip ci] 2024-04-29 16:49:17 +00:00
ohif-bot 0386c29480 chore(version): version.json [skip ci] 2024-04-29 16:49:07 +00:00
Alireza 1dcdc9dabe feat(v3p8): advanced 3D layouts and 4d data support (#4071) 2024-04-29 12:36:12 -04:00
Alireza feaebd7f61 Merge branch 'release' of github.com:OHIF/Viewers into candidate-3p8-rc-6 2024-04-29 12:16:39 -04:00
ohif-bot df32a05a4d chore(version): Update package versions [skip ci] 2023-10-11 16:59:05 +00:00
ohif-bot e205bf5c4e chore(version): version.json [skip ci] 2023-10-11 16:58:56 +00:00
Alireza 5ddf8a1602 feat(v3p7): segmentation mode and cloud server configuration (#3720) 2023-10-11 12:49:12 -04:00
Alireza 83fccad46b Merge branch 'release' of github.com:OHIF/Viewers into candidate-3p7-rc-5 2023-10-11 12:18:10 -04:00
ohif-bot 72ec0bffb5 chore(version): Update package versions [skip ci] 2023-06-07 20:01:22 +00:00
ohif-bot 82c6c3c353 chore(version): version.json [skip ci] 2023-06-07 20:01:18 +00:00
Alireza 1d38fe30a4 feat(app): modal headers and fix storybook (#3460) 2023-06-07 15:54:08 -04:00
Alireza 666964b6d7 Merge branch 'release' of github.com:OHIF/Viewers into release-fore-newsletter 2023-06-07 13:26:13 -04:00
ohif-bot fa4ee5c559 chore(version): Update package versions [skip ci] 2023-06-07 00:14:02 +00:00
ohif-bot cd58560e9c chore(version): version.json [skip ci] 2023-06-07 00:13:59 +00:00
Alireza 16afa740b6 feat(app): change ohif viewer to ohif app (#3451) 2023-06-06 20:06:31 -04:00
Alireza 6715ad9bd6 chore(build): try to build demo and docs 2023-06-01 08:13:36 -04:00
2023 changed files with 26024 additions and 106098 deletions

No files matched your search

+290 -76
View File
@@ -1,52 +1,71 @@
version: 2.1
### ABOUT
#
# This configuration powers our Circleci.io integration
#
# Note:
# Netlify works independently from this configuration to
# create pull request previews and to update `https://docs.ohif.org`
###
## https://github.com/cypress-io/circleci-orb
##
orbs:
codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@3.3.1
executors:
cypress-custom:
description: |
Single Docker container used to run Cypress Tests
docker:
- image: cimg/node:<< parameters.node-version >>-browsers
parameters:
node-version:
default: '18.16.1'
description: |
The version of Node to run your tests with.
type: string
defaults: &defaults
docker:
- image: cimg/node:18.18
environment:
TERM: xterm
TERM: xterm # Enable colors in term
QUICK_BUILD: true
working_directory: ~/repo
commands:
install_bun:
steps:
- restore_cache:
keys:
- bun-cache-v2-{{ arch }}-latest
- run:
name: Install Bun
command: |
if [ ! -d "$HOME/.bun" ]; then
curl -fsSL https://bun.sh/install | bash
fi
echo 'export BUN_INSTALL="$HOME/.bun"' >> $BASH_ENV
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> $BASH_ENV
source $BASH_ENV
- save_cache:
key: bun-cache-v2-{{ arch }}-latest
paths:
- ~/.bun
jobs:
###
# Workflow: PR_CHECKS
###
UNIT_TESTS:
<<: *defaults
resource_class: large
steps:
- install_bun
# Update yarn
- run: yarn -v
- run: node --version
# 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: bun install --no-save
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
# RUN TESTS
- run:
name: 'JavaScript Test Suite'
command: bun run test:unit:ci
command: yarn run test:unit:ci
# platform/app
- run:
name: 'VIEWER: Combine report output'
@@ -74,23 +93,35 @@ jobs:
file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core'
###
# Workflow: DEPLOY
###
BUILD:
<<: *defaults
resource_class: large
steps:
# Checkout code and ALL Git Tags
- checkout
- install_bun
- 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: bun install --no-save
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
# Build & Test
- run:
name: 'Perform the versioning before build'
command: bun ./version.mjs
command: node ./version.mjs
- run:
name: 'Build the OHIF Viewer'
command: bun run build
command: yarn run build
no_output_timeout: 45m
- run:
name: 'Upload SourceMaps, Send Deploy Notification'
@@ -112,18 +143,29 @@ jobs:
- commit.txt
- version.json
# just to make sure later on we can publish them
BUILD_PACKAGES_QUICK:
<<: *defaults
resource_class: large
steps:
- install_bun
- run: yarn -v
# Checkout code and ALL Git Tags
- checkout
- attach_workspace:
at: ~/repo
# Use increasingly general patterns to restore cache
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: bun install --frozen-lockfile
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache/yarn
key: yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Avoid hosts unknown for github
command: |
@@ -135,27 +177,44 @@ jobs:
- run:
name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Increase the event emitter limit
command: |
node ./increaseEventEmitterLimit.mjs
- run:
name: build half of the packages (to avoid out of memory in circleci)
command: |
bun run build:package-all
yarn run build:package-all
- run:
name: build the other half of the packages
command: |
bun run build:package-all-1
yarn run build:package-all-1
###
# Workflow: RELEASE
###
NPM_PUBLISH:
<<: *defaults
resource_class: large
steps:
- install_bun
- run: yarn -v
# Checkout code and ALL Git Tags
- checkout
- attach_workspace:
at: ~/repo
# Use increasingly general patterns to restore cache
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: bun install --no-save
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache/yarn
key: yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Avoid hosts unknown for github
command: |
@@ -167,14 +226,18 @@ jobs:
- run:
name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Increase the event emitter limit
command: |
node ./increaseEventEmitterLimit.mjs
- run:
name: build half of the packages (to avoid out of memory in circleci)
command: |
bun run build:package-all
yarn run build:package-all
- run:
name: build the other half of the packages
command: |
bun run build:package-all-1
yarn run build:package-all-1
- run:
name: increase min time out
command: |
@@ -186,14 +249,14 @@ jobs:
- run:
name: publish package versions
command: |
bun ./publish-version.mjs
node ./publish-version.mjs
- run:
name: Again set the NPM registry (was deleted in the version script)
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: publish package dist
command: |
bun ./publish-package.mjs
node ./publish-package.mjs
- persist_to_workspace:
root: ~/repo
paths:
@@ -201,7 +264,6 @@ jobs:
DOCKER_RELEASE_PUBLISH:
<<: *defaults
resource_class: large
steps:
- attach_workspace:
at: ~/repo
@@ -231,7 +293,6 @@ jobs:
DOCKER_BETA_PUBLISH:
<<: *defaults
resource_class: large
steps:
- attach_workspace:
at: ~/repo
@@ -265,31 +326,92 @@ jobs:
docker push ohif/app:$IMAGE_VERSION_FULL
fi
CYPRESS:
<<: *defaults
resource_class: large
parallelism: 8
# This is copied from the Cypress orb since the default for cypress/run is node 16 and
# 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:
- install_bun
- run:
name: Install Xvfb
command: sudo apt-get update && sudo apt-get install -y xvfb
- run:
name: Start Xvfb
command: Xvfb :99 -screen 0 1920x1080x24 &
background: true
- run:
name: Export Display Variable
command: export DISPLAY=:99
- cypress/install:
install-browsers: true
install-command: bun install --no-save
package-manager: yarn
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:
npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run --record
--browser chrome --parallel
start-command: bun run test:data && bun run test:e2e:serve
cypress-command: << parameters.cypress-command >>
start-command: << parameters.start-command >>
working-directory: << parameters.working-directory >>
workflows:
PR_CHECKS:
@@ -298,12 +420,61 @@ workflows:
filters:
branches:
ignore: master
- UNIT_TESTS
- CYPRESS:
- UNIT_TESTS:
requires:
- BUILD_PACKAGES_QUICK
- CYPRESS_CUSTOM_RUN:
name: 'Cypress Tests'
context: cypress
matrix:
parameters:
start-command:
- yarn run test:data && yarn run test:e2e:serve
install-browsers:
- true
cypress-command:
- 'npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run
--record --browser chrome --parallel'
package-manager:
- 'yarn'
cypress-cache-key:
- 'yarn-packages-{{ checksum "yarn.lock" }}'
cypress-cache-path:
- '~/.cache/Cypress'
requires:
- BUILD_PACKAGES_QUICK
# viewer-dev.ohif.org
# 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
# deployed from the release branch which is more stable and less frequently updated.
DEPLOY_MASTER:
jobs:
- BUILD:
@@ -317,20 +488,63 @@ workflows:
requires:
- NPM_PUBLISH
# viewer.ohif.org
# Our release branch deploys to viewer.ohif.org and is more stable and less
# frequently updated after being tested in the staging environment.
DEPLOY_RELEASE:
jobs:
- BUILD:
filters:
branches:
only: /^release\/.*/
- HOLD_FOR_APPROVAL:
type: approval
requires:
- BUILD
only: release
- NPM_PUBLISH:
requires:
- HOLD_FOR_APPROVAL
- BUILD
- DOCKER_RELEASE_PUBLISH:
requires:
- NPM_PUBLISH
###
# Unit and E2E tests have already run for PR_CHECKS
# Re-running should not gain us any confidence here
###
# RELEASE:
# jobs:
# - NPM_PUBLISH:
# filters:
# branches:
# only: master
# - DOCS_PUBLISH:
# filters:
# branches:
# only: master
# # Update base branch snapshots
# # and record a Cypress dashboard test run
# - cypress/run:
# name: 'Generate Percy Snapshots'
# executor: cypress/browsers-chrome76
# browser: chrome
# pre-steps:
# - run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
# add wait-on' # Use yarn latest
# yarn: true
# store_artifacts: false
# working_directory: platform/app
# build:
# npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.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
# - store_test_results:
# path: platform/app/cypress/results
# filters:
# branches:
# only: master
# - DOCKER_MASTER_PUBLISH:
# requires:
# - NPM_PUBLISH
+1
View File
@@ -0,0 +1 @@
PERCY_TOKEN=<your token here>
-76
View File
@@ -1,76 +0,0 @@
name: Playwright Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
playwright-tests:
timeout-minutes: 60
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.49.0-noble
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shardTotal: [10]
steps:
- uses: actions/checkout@v4
- name: Install unzip
run: apt-get update && apt-get install -y unzip
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Set up Git safe directory
run: git config --global --add safe.directory /__w/Viewers/Viewers
- name: Run Playwright tests
run:
export NODE_OPTIONS="--max_old_space_size=8192" && bun x playwright test --shard=${{
matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
merge-reports:
if: ${{ !cancelled() }}
needs: [playwright-tests]
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.48.1-focal
steps:
- name: Install unzip
run: apt-get update && apt-get install -y unzip
- uses: oven-sh/setup-bun@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Set up Git safe directory
run: git config --global --add safe.directory /__w/Viewers/Viewers
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into HTML Report
run: bun x playwright merge-reports --reporter html ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14
+2 -12
View File
@@ -10,8 +10,6 @@ junit.xml
coverage/
.docz/
.yarn/
.nx/
addOns/yarn.lock
# YALC (for Erik)
.yalc
@@ -35,7 +33,7 @@ docker/dcm4che/dcm4che-arc
# Cypress test results
videos/
screenshots/
# Locize settings
.locize
@@ -43,16 +41,8 @@ videos/
# autogenerated files
platform/app/src/pluginImports.js
/Viewers.iml
platform/app/.recipes/Nginx-Dcm4Chee/logs/*
platform/app/.recipes/Nginx-Dcm4Che/dcm4che/dcm4che-arc/*
platform/app/.recipes/OpenResty-Orthanc/logs/*
.vercel
.vs
# PlayWright
node_modules/
tests/test-results/
tests/playwright-report/
/blob-report/
/playwright/.cache/
+10 -18
View File
@@ -4,34 +4,26 @@
* The JSON file can be used to load the study into the OHIF Viewer. You can get more detail
* in the DICOM JSON Data source on docs.ohif.org
*
* Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> <optional scheme>
* Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>
*
* params:
* - studyFolder: path to the study folder which contains the DICOM files
* - studyFolder: path to the study folder
* - urlPrefix: prefix to the url that will be used to load the study into the viewer. For instance
* we use https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data as the urlPrefix for the
* example since the data is hosted on S3 and each study is in a folder. So the url in the generated
* json file for the first instance of the first series of the first study will be
* dicomweb:https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data/Series1/Instance1
*
* as you see the dicomweb is a prefix that is used to load the data into the viewer, which is suited when
* the .dcm file is hosted statically and can be accessed via a URL (like our example above)
* However, you can specify a new scheme bellow.
*
* - outputJSONPath: path to the output JSON file
* - scheme: default dicomweb if not provided
*/
const dcmjs = require('dcmjs');
const path = require('path');
const fs = require('fs').promises;
const args = process.argv.slice(2);
const [studyDirectory, urlPrefix, outputPath, scheme = 'dicomweb'] = args;
const [studyDirectory, urlPrefix, outputPath] = args;
if (args.length < 3 || args.length > 4) {
console.error(
'Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> [scheme]'
);
if (args.length !== 3) {
console.error('Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>');
process.exit(1);
}
@@ -39,7 +31,7 @@ const model = {
studies: [],
};
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme) {
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath) {
try {
const files = await recursiveReadDir(studyDirectory);
console.debug('Processing...');
@@ -50,7 +42,7 @@ async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme)
const dicomDict = dcmjs.data.DicomMessage.readFile(arrayBuffer.buffer);
const instance = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomDict.dict);
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory, scheme);
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory);
processInstance(instance);
}
}
@@ -85,10 +77,10 @@ async function recursiveReadDir(dir) {
return results;
}
function createImageId(fileLocation, urlPrefix, studyDirectory, scheme) {
function createImageId(fileLocation, urlPrefix, studyDirectory) {
const relativePath = path.relative(studyDirectory, fileLocation);
const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/');
return `${scheme}:${urlPrefix}${normalizedPath}`;
return `dicomweb:${urlPrefix}${normalizedPath}`;
}
function processInstance(instance) {
@@ -270,4 +262,4 @@ function createInstanceMetaDataMultiFrame(instance) {
return instances;
}
convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme);
convertDICOMToJSON(studyDirectory, urlPrefix, outputPath);
-1
View File
@@ -1,7 +1,6 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"mikestead.dotenv",
"bungcip.better-toml",
+12 -90
View File
@@ -1,5 +1,4 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.rulers": [80, 120],
// ===
// Spacing
@@ -15,100 +14,23 @@
// Event Triggers
// ===
"editor.formatOnSave": true,
"eslint.autoFixOnSave": true,
"eslint.run": "onSave",
"eslint.validate": [
{
"language": "javascript",
"autoFix": true
},
{
"language": "javascriptreact",
"autoFix": true
}
],
"jest.autoRun": "off",
"prettier.disableLanguages": ["html"],
"prettier.endOfLine": "lf",
"workbench.colorCustomizations": {},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"cSpell.userWords": [
"aabb",
"architectured",
"attrname",
"Barksy",
"browserslist",
"bulkdata",
"Cacheable",
"cfun",
"clonedeep",
"Colormap",
"Colormaps",
"Comlink",
"cornerstonejs",
"Crosshairs",
"datasource",
"dcmjs",
"decache",
"decached",
"decaching",
"deepmerge",
"Dicom",
"dicomweb",
"DISPLAYSETS",
"glwindow",
"grababble",
"grabbable",
"Hounsfield",
"Interactable",
"Interactor",
"istyle",
"kitware",
"labelmap",
"labelmaps",
"livewire",
"Mergeable",
"multiframe",
"nifti",
"ofun",
"OHIF",
"polylines",
"POLYSEG",
"prapogation",
"precisionmetrics",
"prefetch",
"Prescaled",
"pydicom",
"Radiopharmaceutical",
"rasterizing",
"reconstructable",
"Rehydratable",
"renderable",
"resampler",
"resemblejs",
"reslice",
"resliced",
"Reslices",
"roadmap",
"ROADMAPS",
"Segmentations",
"semibold",
"sitk",
"SUBRESOLUTION",
"suvbsa",
"suvbw",
"suvlbm",
"textbox",
"thresholded",
"thresholding",
"timepoint",
"timepoints",
"TMTV",
"TOOLGROUP",
"tqdm",
"transferables",
"typedoc",
"unsubscriptions",
"uuidv",
"viewplane",
"viewports",
"Voxel",
"Voxels",
"Vtkjs",
"wado",
"wadors",
"wadouri",
"workerpool"
]
}
}
+9 -49
View File
@@ -7,20 +7,18 @@ const fs = require('fs');
const webpack = require('webpack');
// ~~ PLUGINS
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const TerserJSPlugin = require('terser-webpack-plugin');
// ~~ PackageJSON
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
// .rules;
// ~~ RULES
// const loadShadersRule = require('./rules/loadShaders.js');
const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
const cssToJavaScript = require('./rules/cssToJavaScript.js');
// Only uncomment for old v2 stylus
// const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
// ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV;
@@ -98,51 +96,12 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
type: 'filesystem',
},
module: {
noParse: [/(dicomicc)/],
noParse: [/(codec)/, /(dicomicc)/],
rules: [
...(isProdBuild
? []
: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: ['react-refresh/babel'],
},
},
]),
{
test: /\.svg?$/,
oneOf: [
{
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
prettier: false,
svgo: true,
titleProp: true,
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
],
test: /\.js$/,
enforce: 'pre',
use: 'source-map-loader',
},
transpileJavaScriptRule(mode),
loadWebWorkersRule,
@@ -185,6 +144,8 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
'@state': path.resolve(__dirname, '../platform/app/src/state'),
'dicom-microscopy-viewer':
'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js',
'@cornerstonejs/dicom-image-loader':
'@cornerstonejs/dicom-image-loader/dist/dynamic-import/cornerstoneDICOMImageLoader.min.js',
},
// Which directories to search when resolving modules
modules: [
@@ -212,7 +173,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]),
// Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(),
],
+5 -1229
View File
File diff suppressed because it is too large. Load diff
-148
View File
@@ -10,7 +10,6 @@ Please find below the list of datasets used on the demo page, along with their r
## Platforms
### NIH NCI IDC
- Fedorov, A., Longabaugh, W.J., Pot, D., Clunie, D.A., Pieper, S., Aerts, H.J., Homeyer, A., Lewis, R., Akbarzadeh, A., Bontempi, D. and Clifford, W., 2021. NCI imaging data commons. Cancer research, 81(16), p.4188.
@@ -148,150 +147,3 @@ Stony Brook University COVID-19 Positive Cases (COVID-19-NY-SBU)
### 2.16.840.1.114362.1.11972228.22789312658.616067305.306.2
https://data.kitware.com/
### 1.2.276.0.7230010.3.1.2.296485376.1.1665793212.499772
### 2.25.269859997690759739055099378767846712697
### 1.3.6.1.4.1.14519.5.2.1.5099.8010.217836670708542506360829799868
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.232252967813565730694525674696
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.105216574054253895819671475627
### 1.3.6.1.4.1.5962.99.1.1117.5035.1620319789811.1.2.1
### 1.3.6.1.4.1.5962.99.1.1123.9231.1620326176300.1.2.1
### 1.3.6.1.4.1.5962.99.1.1126.3483.1620329455972.1.2.1
https://github.com/ImagingInformatics/hackathon-images
### 2.16.124.113543.6004.101.103.20021117.162333.1
### 2.16.124.113543.6004.101.103.20021117.190619.1
### 2.16.124.113543.6004.101.103.20021117.123455.1
### 2.16.124.113543.6004.101.103.20021117.061159.1
https://www.aapm.org/
### 1.2.840.113619.2.30.1.1762295590.1623.978668949.886
### 1.2.276.0.7230010.3.1.2.447481088.1.1669202398.851612
Custom data SPECT, specifically I123-FP-CIT (DaTSCAN) SPECT, evaluates the dopaminergic system to diagnose Parkinson's disease, especially when tremor symptoms are unclear. It helps distinguish Parkinson's disease from treatment-related tremor.
### 1.3.6.1.4.1.9328.50.1.54652
https://www.cancerimagingarchive.net/collection/rider-pilot/
Lung Image Database Consortium (LIDC). (2023) RIDER Pilot [Data set]. The Cancer Imaging Archive (TCIA). https://doi.org/10.7937/m87f-mz83
### 1.3.6.1.4.1.14519.5.2.1.331759366792756327296606233801322964986
Mayr, N., Yuh, W. T. C., Bowen, S., Harkenrider, M., Knopp, M. V., Lee, E. Y.-P., Leung, E., Lo, S. S., Small Jr., W., & Wolfson, A. H. (2023). Cervical Cancer – Tumor Heterogeneity: Serial Functional and Molecular Imaging Across the Radiation Therapy Course in Advanced Cervical Cancer (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/ERZ5-QZ59
https://www.cancerimagingarchive.net/collection/cc-tumor-heterogeneity/
### 1.3.6.1.4.1.14519.5.2.1.297577087050970310787702792940607009472
Eslick, E. M., Kipritidis, J., Gradinscak, D., Stevens, M. J., Bailey, D. L., Harris, B., Booth, J. T., & Keall, P. J. (2022). CT Ventilation as a functional imaging modality for lung cancer radiotherapy (CT-vs-PET-Ventilation-Imaging) (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/3ppx-7s22
https://www.cancerimagingarchive.net/collection/ct-vs-pet-ventilation-imaging/
### 1.3.6.1.4.1.14519.5.2.1.2103.7010.634114621738943599785009586807
### 1.3.6.1.4.1.14519.5.2.1.2103.7010.135953723682765205394176991681
Huang, W., Tudorica, A., Chui, S., Kemmer, K., Naik, A., Troxell, M., Oh, K., Roy, N., Afzal, A., & Holtorf, M. (2014). Variations of dynamic contrast-enhanced magnetic resonance imaging in evaluation of breast cancer therapy response: a multicenter data analysis challenge (QIN Breast DCE-MRI) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2014.a2n1ixox
https://www.cancerimagingarchive.net/collection/qin-breast-dce-mri/
### 1.3.6.1.4.1.14519.5.2.1.1.24766180081901755714059656629507905556
Cancer Moonshot Biobank. (2023). Cancer Moonshoot Biobank – Acute Myeloid Leukemia (CMB-AML) (Version 4) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/PCTE-6M66
https://www.cancerimagingarchive.net/collection/cmb-aml/
### 1.3.6.1.4.1.14519.5.2.1.3098.5025.285242291560760827564488897577
https://www.cancerimagingarchive.net/collection/anti-pd-1_lung/
Madhavi, P., Patel, S., & Tsao, A. S. (2019). Data from Anti-PD-1 Immunotherapy Lung [Data set]. The Cancer Imaging Archive. DOI: 10.7937/tcia.2019.zjjwb9ip
### 1.3.6.1.4.1.14519.5.2.1.1.84416332615988066829602832830236187384
https://www.cancerimagingarchive.net/collection/cmb-pca/
Cancer Moonshot Biobank. (2022). Cancer Moonshot Biobank – Prostate Cancer Collection (CMB-PCA) (Version 7) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/25T7-6Y12
### 1.3.6.1.4.1.32722.99.99.239341353911714368772597187099978969331
Aerts, H. J. W. L., Wee, L., Rios Velazquez, E., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2014). Data From NSCLC-Radiomics (version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2015.PF0M9REI
https://www.cancerimagingarchive.net/collection/nsclc-radiomics/
### 1.3.6.1.4.1.14519.5.2.1.7085.2626.494695569589117268722281491772
https://www.cancerimagingarchive.net/collection/cptac-ucec/
National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC). (2019). The Clinical Proteomic Tumor Analysis Consortium Uterine Corpus Endometrial Carcinoma Collection (CPTAC-UCEC) (Version 12) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2018.3R3JUISW
### 1.3.6.1.4.1.14519.5.2.1.207544490797667703011829289839681390478
https://www.cancerimagingarchive.net/collection/remind/
Juvekar, P., Dorent, R., Kögl, F., Torio, E., Barr, C., Rigolo, L., Galvin, C., Jowkar, N., Kazi, A., Haouchine, N., Cheema, H., Navab, N., Pieper, S., Wells, W. M., Bi, W. L., Golby, A., Frisken, S., & Kapur, T. (2023). The Brain Resection Multimodal Imaging Database (ReMIND) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/3RAG-D070
### 1.3.12.2.1107.5.1.4.60175.30000008042114404745300000010
Gavrielides, M. A., Kinnard, L. M., Myers, K. J., Peregoy, J., Pritchard, W. F., Zeng, R., Esparza, J., Karanian, J., & Petrick, N. (2015). Data From Phantom FDA [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/TCIA.2015.orbjkmux
https://www.cancerimagingarchive.net/collection/phantom-fda/
### 1.3.6.1.4.1.14519.5.2.1.6834.5010.992793141464713669479982159310
https://www.cancerimagingarchive.net/collection/4d-lung/
Hugo, G. D., Weiss, E., Sleeman, W. C., Balik, S., Keall, P. J., Lu, J., & Williamson, J. F. (2016). Data from 4D Lung Imaging of NSCLC Patients (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.ELN8YGLE
### 1.3.6.1.4.1.9328.50.17.15423521354819720574322014551955370036
https://www.cancerimagingarchive.net/collection/rider-lung-pet-ct/
Muzi P, Wanner M, & Kinahan P. (2015). Data From RIDER Lung PET-CT. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2015.ofip7tvm
### 1.3.6.1.4.1.14519.5.2.1.9823.1001.134394060407147891170882809392
https://www.cancerimagingarchive.net/collection/prostate-mri/
Choyke P, Turkbey B, Pinto P, Merino M, Wood B. (2016). Data From PROSTATE-MRI. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2016.6046GUDv
### 1.3.6.1.4.1.14519.5.2.1.191696062987463500085282581898315738844
https://www.cancerimagingarchive.net/collection/upenn-gbm/
Bakas, S., Sako, C., Akbari, H., Bilello, M., Sotiras, A., Shukla, G., Rudie, J. D., Flores Santamaria, N., Fathi Kazerooni, A., Pati, S., Rathore, S., Mamourian, E., Ha, S. M., Parker, W., Doshi, J., Baid, U., Bergman, M., Binder, Z. A., Verma, R., … Davatzikos, C. (2021). Multi-parametric magnetic resonance imaging (mpMRI) scans for de novo Glioblastoma (GBM) patients from the University of Pennsylvania Health System (UPENN-GBM) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.709X-DN49
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.921758700577562664959693695481
https://www.cancerimagingarchive.net/collection/breast-diagnosis/
Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Data set]. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2015.SDNRQXXR
### 1.3.6.1.4.1.14519.5.2.1.1620.1225.189514895974227080410265976065
Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33
https://www.cancerimagingarchive.net/collection/ea1141/
### 1.2.276.0.7230010.3.1.2.2155604110.4180.1021041295.21
From OFFIS DICOM-Team
https://www.offis.de/
OFFIS DICOM-Team
+13 -9
View File
@@ -51,9 +51,10 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-video.webp?raw=true" alt="VIDEO" width="350"/> | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/microscopy.webp?raw=true" alt="microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) |
## About
The OHIF Viewer can retrieve
The OHIF Medical Imaging Viewer is for viewing medical images. It can retrieve
and load images from most sources and formats; render sets in 2D, 3D, and
reconstructed representations; allows for the manipulation, annotation, and
serialization of observations; supports internationalization, OpenID Connect,
@@ -118,16 +119,19 @@ This is typically where the latest development happens. Code that is in the mast
Each package is tagged with beta version numbers, and published to npm such as `@ohif/ui@3.6.0-beta.1`
### `release/*` branches - The latest stable releases
Once the `master` branch code reaches a stable, release-ready state, we conduct a comprehensive code review and QA testing. Upon approval, we create a new release branch from `master`. These branches represent the latest stable version considered ready for production.
### `release` branch - The latest stable release
For example, `release/3.5` is the branch for version 3.5.0, and `release/3.6` is for version 3.6.0. After each release, we wait a few days to ensure no critical bugs. If any are found, we fix them in the release branch and create a new release with a minor version bump, e.g., 3.5.1 in the `release/3.5` branch.
This branch represents the latest stable version of the project that is considered ready for production. The code in this branch should be fully tested and vetted for release. Once the code in the master branch reaches a state where it's stable and ready to be released to users,
we do a comprehensive code review and QA testing. Once the code is approved,
we merge it into the release branch and tag a new release.
Each package is tagged with version numbers and published to npm, such as `@ohif/ui@3.5.0`. Note that `master` is always ahead of the `release` branch. We publish docker builds for both beta and stable releases.
Each package is tagged with version numbers, and published to npm such as `@ohif/ui@3.5.0`
Note: `master` is always ahead of `release` branch. We publish both docker builds for beta and stable releases.
Here is a schematic representation of our development workflow:
![alt text](platform/docs/docs/assets/img/github-readme-branches-Jun2024.png)
![Alt text](platform/docs/docs/assets/img/github-readme-branches.png)
@@ -136,7 +140,7 @@ Here is a schematic representation of our development workflow:
### Requirements
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
- [Node 18+](https://nodejs.org/en/)
- [Node 16+](https://nodejs.org/en/)
- Yarn Workspaces should be enabled on your machine:
- `yarn config set workspaces-experimental true`
@@ -176,7 +180,7 @@ also supports a number of commands that can be found in their respective
| **Deploy** | |
| `build`\* | Builds production output for our PWA Viewer | |
\* - For more information on different builds, check out our [Deploy
\* - For more information on our different builds, check out our [Deploy
Docs][deployment-docs]
## Project
@@ -333,4 +337,4 @@ MIT © [OHIF](https://github.com/OHIF)
[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk
<!-- prettier-ignore-end -->
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large&issueType=license)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large)
-3
View File
@@ -1,3 +0,0 @@
# External Dependencies
This module contains optional dependencies and external dependencies for including in OHIF, such as the DICOM Microscopy Viewer component.
-579
View File
@@ -1,579 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22)
**Note:** Version bump only for package @externals/devDependencies
# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14)
**Note:** Version bump only for package @externals/devDependencies
# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13)
**Note:** Version bump only for package @externals/devDependencies
# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12)
**Note:** Version bump only for package @externals/devDependencies
# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21)
### Bug Fixes
* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd))
-94
View File
@@ -1,94 +0,0 @@
{
"name": "@externals/devDependencies",
"description": "External dev dependencies - put dev build dependencies here",
"version": "3.10.0-beta.6",
"license": "MIT",
"private": true,
"engines": {
"node": ">=12",
"yarn": ">=1.19.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@kitware/vtk.js": "32.1.0",
"clsx": "^2.1.1",
"core-js": "^3.2.1",
"moment": "^2.9.4"
},
"peerDependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@svgr/webpack": "^8.1.0",
"@types/jest": "^27.5.0",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"autoprefixer": "^10.4.4",
"babel-eslint": "9.x",
"babel-loader": "^8.2.4",
"babel-plugin-module-resolver": "^5.0.0",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^9.0.1",
"cross-env": "^5.2.0",
"css-loader": "^6.8.1",
"dotenv": "^8.1.0",
"eslint": "^8.39.0",
"eslint-config-prettier": "^7.2.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-flowtype": "^7.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-promise": "^5.2.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"eslint-plugin-tsdoc": "^0.2.11",
"eslint-webpack-plugin": "^2.5.3",
"execa": "^8.0.1",
"extract-css-chunks-webpack-plugin": "^4.5.4",
"html-webpack-plugin": "^5.3.2",
"husky": "^3.0.0",
"jest": "^29.5.0",
"jest-canvas-mock": "^2.1.0",
"jest-environment-jsdom": "^29.5.0",
"jest-junit": "^6.4.0",
"lerna": "^7.2.0",
"lint-staged": "^9.0.2",
"mini-css-extract-plugin": "^2.1.0",
"optimize-css-assets-webpack-plugin": "^6.0.1",
"postcss": "^8.3.5",
"postcss-import": "^14.0.2",
"postcss-loader": "^6.1.1",
"postcss-preset-env": "^7.4.3",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"react-refresh": "^0.14.2",
"semver": "^7.5.1",
"serve": "^14.2.0",
"shader-loader": "^1.3.1",
"shx": "^0.3.3",
"source-map-loader": "^4.0.1",
"start-server-and-test": "^1.10.0",
"style-loader": "^1.0.0",
"stylus": "^0.59.0",
"stylus-loader": "^7.1.3",
"terser-webpack-plugin": "^5.1.4",
"typescript": "5.5.4",
"unused-webpack-plugin": "2.4.0",
"webpack": "5.94.0",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "4.7.3",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^5.7.3",
"workbox-webpack-plugin": "^6.1.5",
"worker-loader": "^3.0.8"
},
"scripts": {
"build": "Included as direct dependency"
}
}
-579
View File
@@ -1,579 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21)
### Bug Fixes
* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd))
-9
View File
@@ -1,9 +0,0 @@
{
"name": "@externals/dicom-microscopy-viewer",
"description": "External reference to dicom-microscopy-viewer",
"version": "3.10.0-beta.6",
"license": "MIT",
"dependencies": {
"dicom-microscopy-viewer": "^0.46.1"
}
}
-51
View File
@@ -1,51 +0,0 @@
{
"name": "ohif-monorepo-root",
"private": true,
"packageManager": "yarn@1.22.22",
"workspaces": {
"packages": [
"../platform/i18n",
"../platform/core",
"../platform/ui",
"../platform/ui-next",
"../platform/app",
"../extensions/*",
"../modes/*",
"../addOns/externals/*"
],
"nohoist": [
"**/html-minifier-terser"
]
},
"scripts": {
"preinstall": "cd .. && node preinstall.js"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0"
},
"resolutions": {
"**/@babel/runtime": "^7.20.13",
"commander": "8.3.0",
"dcmjs": ">=0.33.0",
"dicomweb-client": ">=0.10.4",
"nth-check": "^2.1.1",
"trim-newlines": "^5.0.0",
"glob-parent": "^6.0.2",
"trim": "^1.0.0",
"package-json": "^8.1.0",
"typescript": "5.5.4",
"sharp": "^0.32.6"
}
}
+18 -2
View File
@@ -5,6 +5,23 @@ module.exports = {
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
plugins: [
[
'inline-react-svg',
{
svgo: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
},
],
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
@@ -32,7 +49,6 @@ module.exports = {
'@babel/transform-destructuring',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
'@babel/plugin-transform-class-static-block',
],
},
production: {
@@ -51,7 +67,7 @@ module.exports = {
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-refresh/babel'],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
+1 -1
View File
@@ -1 +1 @@
27ae385fd7787bf34af00366c5d490ac33abeff9
1dcdc9dabe9d4179f223912d6adc00bc95629a85
@@ -1,12 +0,0 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
const ENTRY = {
app: `${SRC_DIR}/index.tsx`,
};
module.exports = (env, argv) => {
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
};
@@ -1,54 +0,0 @@
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const pkg = require('./../package.json');
const ROOT_DIR = path.join(__dirname, '../');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
const ENTRY = {
app: `${SRC_DIR}/index.tsx`,
};
const outputName = `ohif-${pkg.name.split('/').pop()}`;
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
return merge(commonConfig, {
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: ROOT_DIR,
library: 'ohif-extension-cornerstone-dicom-pmap',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
new MiniCssExtractPlugin({
filename: `./dist/${outputName}.css`,
chunkFilename: `./dist/${outputName}.css`,
}),
],
});
};
@@ -1,442 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
### Bug Fixes
* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed))
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
### Bug Fixes
* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30))
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
### Bug Fixes
* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90))
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
### Bug Fixes
* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742))
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
### Features
* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3))
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24)
### Features
* **pmap:** added support for parametric map ([#4284](https://github.com/OHIF/Viewers/issues/4284)) ([fc0064f](https://github.com/OHIF/Viewers/commit/fc0064fd9d8cdc8fde81b81f0e71fd5d077ca22b))
-20
View File
@@ -1,20 +0,0 @@
MIT License
Copyright (c) 2023 Open Health Imaging Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,12 +0,0 @@
# dicom-pmap
## Description
DICOM PMAP read workflow. This extension will allow you to load a DICOM Parametric
Map image and display it on OHIF.
## Author
OHIF
## License
MIT
@@ -1,44 +0,0 @@
module.exports = {
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
[
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
'@babel/preset-env',
{
modules: 'commonjs',
debug: false,
},
'@babel/preset-typescript',
],
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
],
},
production: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};
@@ -1,54 +0,0 @@
{
"name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.10.0-beta.6",
"description": "DICOM Parametric Map read workflow",
"author": "OHIF",
"license": "MIT",
"main": "dist/ohif-extension-cornerstone-dicom-pmap.umd.js",
"module": "src/index.tsx",
"files": [
"dist/**",
"public/**",
"README.md"
],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-pmap": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build",
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.6",
"@ohif/extension-cornerstone": "3.10.0-beta.6",
"@ohif/extension-default": "3.10.0-beta.6",
"@ohif/i18n": "3.10.0-beta.6",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^2.2.3",
"@cornerstonejs/core": "^2.2.3",
"@kitware/vtk.js": "32.1.0",
"react-color": "^2.19.3"
}
}
@@ -1,244 +0,0 @@
import { utils } from '@ohif/core';
import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
import { adaptersPMAP } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
import { dicomLoaderService } from '@ohif/extension-cornerstone';
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30'];
function _getDisplaySetsFromSeries(
instances,
servicesManager: AppTypes.ServicesManager,
extensionManager
) {
const instance = instances[0];
const {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
SeriesDescription,
SeriesNumber,
SeriesDate,
SOPClassUID,
wadoRoot,
wadoUri,
wadoUriRoot,
} = instance;
const displaySet = {
// Parametric map use to have the same modality as its referenced volume but
// "PMAP" is used in the viewer even though this is not a valid DICOM modality
Modality: 'PMAP',
isReconstructable: true, // by default for now
displaySetInstanceUID: `pmap.${utils.guid()}`,
SeriesDescription,
SeriesNumber,
SeriesDate,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
SOPClassHandlerId,
SOPClassUID,
referencedImages: null,
referencedSeriesInstanceUID: null,
referencedDisplaySetInstanceUID: null,
referencedVolumeURI: null,
referencedVolumeId: null,
isDerivedDisplaySet: true,
loadStatus: {
loading: false,
loaded: false,
},
sopClassUids,
instance,
instances: [instance],
wadoRoot,
wadoUriRoot,
wadoUri,
isOverlayDisplaySet: true,
};
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
if (!referencedSeriesSequence) {
console.error('ReferencedSeriesSequence is missing for the parametric map');
return;
}
const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence;
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
// Does not get the referenced displaySet during parametric displaySet creation
// because it is still not available (getDisplaySetByUID returns `undefined`).
displaySet.getReferenceDisplaySet = () => {
const { displaySetService } = servicesManager.services;
if (displaySet.referencedDisplaySetInstanceUID) {
return displaySetService.getDisplaySetByUID(displaySet.referencedDisplaySetInstanceUID);
}
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced displaySet is missing for the parametric map');
}
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
return referencedDisplaySet;
};
// Does not get the referenced volumeId during parametric displaySet creation because the
// referenced displaySet is still not available (getDisplaySetByUID returns `undefined`).
displaySet.getReferencedVolumeId = () => {
if (displaySet.referencedVolumeId) {
return displaySet.referencedVolumeId;
}
const referencedDisplaySet = displaySet.getReferenceDisplaySet();
const referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
const referencedVolumeId = `${VOLUME_LOADER_SCHEME}:${referencedVolumeURI}`;
displaySet.referencedVolumeURI = referencedVolumeURI;
displaySet.referencedVolumeId = referencedVolumeId;
return referencedVolumeId;
};
displaySet.load = async ({ headers }) =>
await _load(displaySet, servicesManager, extensionManager, headers);
return [displaySet];
}
const getRangeFromPixelData = (pixelData: Float32Array) => {
let lowest = pixelData[0];
let highest = pixelData[0];
for (let i = 1; i < pixelData.length; i++) {
if (pixelData[i] < lowest) {
lowest = pixelData[i];
}
if (pixelData[i] > highest) {
highest = pixelData[i];
}
}
return [lowest, highest];
};
async function _load(
displaySet,
servicesManager: AppTypes.ServicesManager,
extensionManager,
headers
) {
const volumeId = `${VOLUME_LOADER_SCHEME}:${displaySet.displaySetInstanceUID}`;
const volumeLoadObject = cache.getVolumeLoadObject(volumeId);
if (volumeLoadObject) {
return volumeLoadObject.promise;
}
displaySet.loading = true;
displaySet.isLoaded = false;
// We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers.
const promise = _loadParametricMap({
extensionManager,
displaySet,
headers,
});
cache.putVolumeLoadObject(volumeId, { promise }).catch(err => {
throw err;
});
promise
.then(() => {
displaySet.loading = false;
displaySet.isLoaded = true;
// Broadcast that loading is complete
servicesManager.services.segmentationService._broadcastEvent(
servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
{
pmapDisplaySet: displaySet,
}
);
})
.catch(err => {
displaySet.loading = false;
displaySet.isLoaded = false;
throw err;
});
return promise;
}
async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
const referencedVolumeId = displaySet.getReferencedVolumeId();
const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
// Parametric map can be loaded only if its referenced volume exists otherwise it will fail
if (!cachedReferencedVolume) {
throw new Error(
'Referenced Volume is missing for the PMAP, and stack viewport PMAP is not supported yet'
);
}
const { imageIds } = cachedReferencedVolume;
const results = await adaptersPMAP.Cornerstone3D.ParametricMap.generateToolState(
imageIds,
arrayBuffer,
metaData
);
const { pixelData } = results;
const TypedArrayConstructor = pixelData.constructor;
const paramMapId = displaySet.displaySetInstanceUID;
const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(referencedVolumeId, {
volumeId: paramMapId,
targetBuffer: {
type: TypedArrayConstructor.name,
},
});
const newPixelData = new TypedArrayConstructor(pixelData.length);
for (let i = 0; i < pixelData.length; i++) {
newPixelData[i] = pixelData[i] * 100;
}
derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData);
const range = getRangeFromPixelData(newPixelData);
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
derivedVolume.metadata.voiLut = [windowLevel];
derivedVolume.loadStatus = { loaded: true };
return derivedVolume;
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
return [
{
name: 'dicom-pmap',
sopClassUids,
getDisplaySetsFromSeries,
},
];
}
export default getSopClassHandlerModule;
@@ -1,7 +0,0 @@
import packageJson from '../package.json';
const id = packageJson.name;
const SOPClassHandlerName = 'dicom-pmap';
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
export { id, SOPClassHandlerId, SOPClassHandlerName };
@@ -1,39 +0,0 @@
import { id } from './id';
import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstonePMAPViewport');
});
const OHIFCornerstonePMAPViewport = props => {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Component {...props} />
</React.Suspense>
);
};
/**
* You can remove any of the following modules if you don't need them.
*/
const extension = {
id,
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
const ExtendedOHIFCornerstonePMAPViewport = props => {
return (
<OHIFCornerstonePMAPViewport
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
{...props}
/>
);
};
return [{ name: 'dicom-pmap', component: ExtendedOHIFCornerstonePMAPViewport }];
},
getSopClassHandlerModule,
};
export default extension;
@@ -1,205 +0,0 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const {
displaySets,
children,
viewportOptions,
displaySetOptions,
servicesManager,
extensionManager,
} = props;
const viewportId = viewportOptions.viewportId;
const { displaySetService, segmentationService, uiNotificationService } =
servicesManager.services;
// PMAP viewport will always have a single display set
if (displaySets.length !== 1) {
throw new Error('PMAP viewport must have a single display set');
}
const pmapDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
const referencedDisplaySetRef = useRef(null);
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = pmapDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet,
pmapDisplaySet
);
referencedDisplaySetRef.current = {
displaySet: referencedDisplaySet,
metadata: referencedDisplaySetMetadata,
};
const [pmapIsLoading, setPmapIsLoading] = useState(!pmapDisplaySet.isLoaded);
// Add effect to listen for loading complete
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
if (evt.pmapDisplaySet?.displaySetInstanceUID === pmapDisplaySet.displaySetInstanceUID) {
setPmapIsLoading(false);
}
}
);
return () => {
unsubscribe();
};
}, [pmapDisplaySet]);
const getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
displaySetOptions.unshift({});
const [pmapDisplaySetOptions] = displaySetOptions;
// Make sure `options` exists
pmapDisplaySetOptions.options = pmapDisplaySetOptions.options ?? {};
Object.assign(pmapDisplaySetOptions.options, {
colormap: {
name: 'rainbow_2',
opacity: [
{ value: 0, opacity: 0 },
{ value: 0.25, opacity: 0.25 },
{ value: 0.5, opacity: 0.5 },
{ value: 0.75, opacity: 0.75 },
{ value: 0.9, opacity: 0.99 },
],
},
voi: {
windowCenter: 50,
windowWidth: 100,
},
});
uiNotificationService.show({
title: 'Parametric Map',
type: 'warning',
message: 'The values are multiplied by 100 in the viewport for better visibility',
});
return (
<Component
{...props}
// Referenced + PMAP displaySets must be passed as parameter in this order
displaySets={[referencedDisplaySet, pmapDisplaySet]}
viewportOptions={{
viewportType: 'volume',
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
}}
displaySetOptions={[{}, pmapDisplaySetOptions]}
></Component>
);
}, [
extensionManager,
displaySetOptions,
props,
pmapDisplaySet,
viewportOptions.orientation,
viewportOptions.viewportId,
]);
// Cleanup the PMAP viewport when the viewport is destroyed
useEffect(() => {
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => {
const activeViewport = viewports.get(activeViewportId);
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
viewportGridService.setDisplaySetsForViewport({
viewportId: activeViewportId,
displaySetInstanceUIDs: [],
});
}
}
);
return () => {
onDisplaySetsRemovedSubscription.unsubscribe();
};
}, [activeViewportId, displaySetService, viewportGridService, viewports]);
let childrenWithProps = null;
if (children && children.length) {
childrenWithProps = children.map((child, index) => {
return (
child &&
React.cloneElement(child, {
viewportId,
key: index,
})
);
});
}
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{pmapIsLoading && (
<LoadingIndicatorTotalPercent
className="h-full w-full"
totalNumbers={null}
percentComplete={null}
loadingText="Loading Parametric Map..."
/>
)}
{getCornerstoneViewport()}
{childrenWithProps}
</div>
</>
);
}
OHIFCornerstonePMAPViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet, pmapDisplaySet) {
const { SharedFunctionalGroupsSequence } = pmapDisplaySet.instance;
const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence)
? SharedFunctionalGroupsSequence[0]
: SharedFunctionalGroupsSequence;
const { PixelMeasuresSequence } = SharedFunctionalGroup;
const PixelMeasures = Array.isArray(PixelMeasuresSequence)
? PixelMeasuresSequence[0]
: PixelMeasuresSequence;
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = {
PatientID: image0.PatientID,
PatientName: image0.PatientName,
PatientSex: image0.PatientSex,
PatientAge: image0.PatientAge,
SliceThickness: image0.SliceThickness || SliceThickness,
StudyDate: image0.StudyDate,
SeriesDescription: image0.SeriesDescription,
SeriesInstanceUID: image0.SeriesInstanceUID,
SeriesNumber: image0.SeriesNumber,
ManufacturerModelName: image0.ManufacturerModelName,
SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices,
};
return referencedDisplaySetMetadata;
}
export default OHIFCornerstonePMAPViewport;
+1 -968
View File
@@ -3,974 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21)
### Bug Fixes
* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd))
# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28)
### Bug Fixes
* **rt:** dont convert to volume for RTSTRUCT ([#4157](https://github.com/OHIF/Viewers/issues/4157)) ([7745c09](https://github.com/OHIF/Viewers/commit/7745c092bb3edf0090f32fbbbae2f0776128d5a2))
# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27)
### Features
* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77))
# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08)
### Features
* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb))
# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06)
### Bug Fixes
* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2))
# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29)
# [3.8.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0) (2024-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
@@ -1,5 +1,5 @@
module.exports = {
plugins: ['@babel/plugin-proposal-class-properties'],
plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
@@ -37,7 +37,7 @@ module.exports = {
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-refresh/babel'],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.10.0-beta.6",
"version": "3.8.0",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,16 +33,16 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.6",
"@ohif/extension-cornerstone": "3.10.0-beta.6",
"@ohif/extension-default": "3.10.0-beta.6",
"@ohif/i18n": "3.10.0-beta.6",
"@ohif/core": "3.8.0",
"@ohif/extension-cornerstone": "3.8.0",
"@ohif/extension-default": "3.8.0",
"@ohif/i18n": "3.8.0",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^10.11.0",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1"
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
@@ -1,55 +0,0 @@
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
const services = servicesManager.services;
const { displaySetService, viewportGridService } = services;
const actions = {
hydrateRTSDisplaySet: ({ displaySet, viewportId }) => {
if (displaySet.Modality !== 'RTSTRUCT') {
throw new Error('Display set is not an RTSTRUCT');
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: displaySet,
type: SegmentationRepresentations.Contour,
});
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
});
viewportGridService.setDisplaySetsForViewport({
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
},
};
const definitions = {
hydrateRTSDisplaySet: {
commandFn: actions.hydrateRTSDisplaySet,
storeContexts: [],
options: {},
},
};
return {
actions,
definitions,
defaultContext: 'cornerstone-dicom-rt',
};
};
export default commandsModule;
@@ -5,13 +5,9 @@ import loadRTStruct from './loadRTStruct';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.481.3'];
const loadPromises = {};
let loadPromises = {};
function _getDisplaySetsFromSeries(
instances,
servicesManager: AppTypes.ServicesManager,
extensionManager
) {
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
const instance = instances[0];
const {
@@ -72,34 +68,29 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
// Instead of throwing error, subscribe to display sets added
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
({ displaySetsAdded }) => {
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
unsubscribe();
}
}
displaySet.getReferenceDisplaySet = () => {
const { DisplaySetService } = servicesManager.services;
const referencedDisplaySets = DisplaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
} else {
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced DisplaySet is missing for the RT');
}
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
}
return referencedDisplaySet;
};
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
return [displaySet];
}
function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) {
function _load(rtDisplaySet, servicesManager, extensionManager, headers) {
const { SOPInstanceUID } = rtDisplaySet;
const { segmentationService } = servicesManager.services;
if (
@@ -116,13 +107,19 @@ function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensio
// and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if (!rtDisplaySet.structureSet) {
const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers);
const structureSet = await loadRTStruct(
extensionManager,
rtDisplaySet,
rtDisplaySet.getReferenceDisplaySet(),
headers
);
rtDisplaySet.structureSet = structureSet;
}
const suppressEvents = true;
segmentationService
.createSegmentationForRTDisplaySet(rtDisplaySet)
.createSegmentationForRTDisplaySet(rtDisplaySet, null, suppressEvents)
.then(() => {
rtDisplaySet.loading = false;
resolve();
@@ -171,10 +168,7 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
return ReferencedSeriesSequence;
}
function _segmentationExistsInCache(
rtDisplaySet,
segmentationService: AppTypes.SegmentationService
) {
function _segmentationExistsInCache(rtDisplaySet, segmentationService) {
// Todo: fix this
return false;
// This should be abstracted with the CornerstoneCacheService
@@ -2,7 +2,6 @@ import { id } from './id';
import React from 'react';
import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getCommandsModule from './getCommandsModule';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
@@ -25,7 +24,6 @@ const extension: Types.Extensions.Extension = {
* You ID can be anything you want, but it should be unique.
*/
id,
getCommandsModule,
/**
* PanelModule should provide a list of panels that will be available in OHIF
@@ -89,7 +89,12 @@ async function checkAndLoadContourData(instance, datasource) {
});
}
export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) {
export default async function loadRTStruct(
extensionManager,
rtStructDisplaySet,
referencedDisplaySet,
headers
) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
@@ -97,6 +102,8 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs =
_getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
// Set here is loading is asynchronous.
// If this function throws its set back to false.
@@ -142,7 +149,8 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
const contourPoints = [];
for (let c = 0; c < ContourSequenceArray.length; c++) {
const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c];
const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } =
ContourSequenceArray[c];
let isSupported = false;
@@ -186,6 +194,24 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
return structureSet;
}
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
imageIdSopInstanceUidPairsEntry =>
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
);
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
};
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
return referencedDisplaySet.images.map(image => {
return {
imageId: image.imageId,
sopInstanceUID: image.SOPInstanceUID,
};
});
}
function _setROIContourMetadata(
structureSet,
StructureSetROISequence,
@@ -10,16 +10,14 @@ function promptHydrateRT({
servicesManager,
rtDisplaySet,
viewportId,
toolGroupId = 'default',
preHydrateCallbacks,
hydrateRTDisplaySet,
}: withAppTypes) {
}) {
const { uiViewportDialogService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
const appConfig = extensionManager._appConfig;
return new Promise(async function (resolve, reject) {
const promptResult = appConfig?.disableConfirmationPrompts
? RESPONSE.HYDRATE_SEG
: await _askHydrate(uiViewportDialogService, viewportId);
const promptResult = await _askHydrate(uiViewportDialogService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
@@ -29,6 +27,7 @@ function promptHydrateRT({
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
toolGroupId,
servicesManager,
});
@@ -37,18 +36,16 @@ function promptHydrateRT({
});
}
function _askHydrate(uiViewportDialogService: AppTypes.UIViewportDialogService, viewportId) {
function _askHydrate(uiViewportDialogService, viewportId) {
return new Promise(function (resolve, reject) {
const message = 'Do you want to open this Segmentation?';
const actions = [
{
id: 'no-hydrate',
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
@@ -60,7 +57,6 @@ function _askHydrate(uiViewportDialogService: AppTypes.UIViewportDialogService,
};
uiViewportDialogService.show({
id: 'promptHydrateRT',
viewportId,
type: 'info',
message,
@@ -4,13 +4,11 @@ import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } f
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
function OHIFCornerstoneRTViewport(props: withAppTypes) {
function OHIFCornerstoneRTViewport(props) {
const {
children,
displaySets,
@@ -64,10 +62,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
referencedDisplaySetRef.current = {
@@ -96,15 +91,12 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}, [viewportGrid]);
const hydrateRTDisplaySet = useCallback(
({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('hydrateRTSDisplaySet', {
displaySet: rtDisplaySet,
viewportId,
});
},
[commandsManager]
);
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [rtDisplaySet],
viewportId,
});
};
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
@@ -119,7 +111,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
{...props}
displaySets={[referencedDisplaySet, rtDisplaySet]}
viewportOptions={{
viewportType: 'stack',
viewportType: 'volume',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
@@ -151,10 +143,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment, segmentationService]
[selectedSegment]
);
useEffect(() => {
@@ -251,7 +243,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentations(viewportId);
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
toolGroupService.destroyToolGroup(toolGroupId);
};
@@ -262,27 +254,11 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentations(viewportId);
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
referencedDisplaySetRef.current = null;
};
}, [rtDisplaySet]);
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -306,8 +282,24 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.addComponents([
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -326,7 +318,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
}
></ViewportActionArrows>
),
@@ -366,6 +358,11 @@ OHIFCornerstoneRTViewport.propTypes = {
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
};
OHIFCornerstoneRTViewport.defaultProps = {
customProps: {},
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet) {
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,5 @@
module.exports = {
plugins: ['@babel/plugin-proposal-class-properties'],
plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
@@ -37,7 +37,7 @@ module.exports = {
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-refresh/babel'],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.10.0-beta.6",
"version": "3.8.0",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,22 +33,22 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.6",
"@ohif/extension-cornerstone": "3.10.0-beta.6",
"@ohif/extension-default": "3.10.0-beta.6",
"@ohif/i18n": "3.10.0-beta.6",
"@ohif/core": "3.8.0",
"@ohif/extension-cornerstone": "3.8.0",
"@ohif/extension-default": "3.8.0",
"@ohif/i18n": "3.8.0",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^12.2.2",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1"
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^2.2.3",
"@cornerstonejs/core": "^2.2.3",
"@kitware/vtk.js": "32.1.0",
"@cornerstonejs/adapters": "^1.70.14",
"@cornerstonejs/core": "^1.70.14",
"@kitware/vtk.js": "30.4.1",
"react-color": "^2.19.3"
}
}
@@ -1,6 +1,6 @@
import dcmjs from 'dcmjs';
import { createReportDialogPrompt } from '@ohif/extension-default';
import { Types } from '@ohif/core';
import { ServicesManager, Types } from '@ohif/core';
import { cache, metaData } from '@cornerstonejs/core';
import {
segmentation as cornerstoneToolsSegmentation,
@@ -14,22 +14,18 @@ import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchi
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 { segmentation: segmentationUtils } = utilities;
const { datasetToBlob } = dcmjs.data;
const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
const targetViewportId = viewportId || activeViewportId;
const viewport = viewports.get(targetViewportId);
return viewport;
};
const {
Cornerstone3D: {
Segmentation: { generateSegmentation },
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation },
},
} = adaptersSEG;
@@ -46,14 +42,85 @@ const commandsModule = ({
extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const {
uiNotificationService,
segmentationService,
uiDialogService,
displaySetService,
viewportGridService,
toolGroupService,
} = servicesManager.services as AppTypes.Services;
} = (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.
@@ -65,30 +132,103 @@ const commandsModule = ({
*
*/
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 viewport = getTargetViewport({ viewportId, viewportGridService });
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const displaySet = displaySets[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
const segmentation = segmentations[0];
const segmentationId = segmentation.segmentationId;
const label = segmentation.config.label;
const segments = segmentation.config.segments;
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
referencedDisplaySetInstanceUID: displaySet.referencedDisplaySetInstanceUID,
loadFn: async () => {
const segDisplaySet = displaySet;
const suppressEvents = false;
const serviceFunction =
segDisplaySet.Modality === 'SEG'
? 'createSegmentationForSEGDisplaySet'
: 'createSegmentationForRTDisplaySet';
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
await segmentationService.createLabelmapForDisplaySet(displaySet, {
segmentationId,
segments,
label,
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
const segmentation = segmentationService.getSegmentation(segmentationId);
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
return segmentationId;
},
});
segmentationService.addOrUpdateSegmentation(segmentation);
await segmentationService.addSegmentationRepresentation(viewport.viewportId, {
segmentationId,
});
return segmentationId;
},
/**
* Generates a segmentation from a given segmentation ID.
@@ -106,61 +246,22 @@ const commandsModule = ({
generateSegmentation: ({ segmentationId, options = {} }) => {
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
const { imageIds } = segmentation.representationData.Labelmap;
const { referencedVolumeId } = segmentation.representationData.LABELMAP;
const segImages = imageIds.map(imageId => cache.getImage(imageId));
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
const segmentationVolume = cache.getVolume(segmentationId);
const referencedVolume = cache.getVolume(referencedVolumeId);
const referencedImages = referencedVolume.getCornerstoneImages();
const labelmaps2D = [];
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume);
let z = 0;
for (const segImage of segImages) {
const segmentsOnLabelmap = new Set();
const pixelData = segImage.getPixelData();
const { rows, columns } = segImage;
// Use a single pass through the pixel data
for (let i = 0; i < pixelData.length; i++) {
const segment = pixelData[i];
if (segment !== 0) {
segmentsOnLabelmap.add(segment);
}
}
labelmaps2D[z++] = {
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
pixelData,
rows,
columns,
};
}
const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap);
const labelmap3D = {
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
metadata: [],
labelmaps2D,
};
// Generate fake metadata as an example
labelmapObj.metadata = [];
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
const representations = segmentationService.getRepresentationsForSegmentation(segmentationId);
Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => {
labelmapObj.segmentsOnLabelmap.forEach(segmentIndex => {
// segmentation service already has a color for each segment
if (!segment) {
return;
}
const { label } = segment;
const firstRepresentation = representations[0];
const color = segmentationService.getSegmentColor(
firstRepresentation.viewportId,
segmentationId,
segment.segmentIndex
);
const segment = segmentationInOHIF?.segments[segmentIndex];
const { label, color } = segment;
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
color.slice(0, 3).map(value => value / 255)
@@ -169,8 +270,8 @@ const commandsModule = ({
const segmentMetadata = {
SegmentNumber: segmentIndex.toString(),
SegmentLabel: label,
SegmentAlgorithmType: segment?.algorithmType || 'MANUAL',
SegmentAlgorithmName: segment?.algorithmName || 'OHIF Brush',
SegmentAlgorithmType: 'MANUAL',
SegmentAlgorithmName: 'OHIF Brush',
RecommendedDisplayCIELabValue,
SegmentedPropertyCategoryCodeSequence: {
CodeValue: 'T-D0050',
@@ -183,12 +284,12 @@ const commandsModule = ({
CodeMeaning: 'Tissue',
},
};
labelmap3D.metadata[segmentIndex] = segmentMetadata;
labelmapObj.metadata[segmentIndex] = segmentMetadata;
});
const generatedSegmentation = generateSegmentation(
referencedImages,
labelmap3D,
labelmapObj,
metaData,
options
);
@@ -230,7 +331,7 @@ const commandsModule = ({
extensionManager,
});
if (promptResult.action !== 1 && !promptResult.value) {
if (promptResult.action !== 1 && promptResult.value) {
return;
}
@@ -336,19 +437,18 @@ const commandsModule = ({
};
const definitions = {
/**
* Obsolete?
*/
getUpdatedViewportsForSegmentation: {
commandFn: actions.getUpdatedViewportsForSegmentation,
},
loadSegmentationDisplaySetsForViewport: {
commandFn: actions.loadSegmentationDisplaySetsForViewport,
},
/**
* Obsolete?
*/
loadSegmentationsForViewport: {
commandFn: actions.loadSegmentationsForViewport,
},
createEmptySegmentationForViewport: {
commandFn: actions.createEmptySegmentationForViewport,
},
generateSegmentation: {
commandFn: actions.generateSegmentation,
},
@@ -20,17 +20,6 @@ const segProtocol: Types.HangingProtocol.Protocol = {
viewportType: 'stack',
toolGroupId: 'default',
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
displaySets: [
{
@@ -63,20 +52,7 @@ const segProtocol: Types.HangingProtocol.Protocol = {
},
viewports: [
{
viewportOptions: {
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
viewportOptions: { allowUnmatchedView: true },
displaySets: [
{
id: 'segDisplaySetId',
@@ -0,0 +1,80 @@
import React from 'react';
import { useAppConfig } from '@state';
import { Toolbox } from '@ohif/ui';
import PanelSegmentation from './panels/PanelSegmentation';
const getPanelModule = ({
commandsManager,
servicesManager,
extensionManager,
configuration,
title,
}) => {
const { customizationService } = servicesManager.services;
const wrappedPanelSegmentation = configuration => {
const [appConfig] = useAppConfig();
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
/>
);
};
const wrappedPanelSegmentationWithTools = configuration => {
const [appConfig] = useAppConfig();
return (
<>
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="segmentationToolbox"
title="Segmentation Tools"
configuration={{
...configuration,
}}
/>
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
/>
</>
);
};
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;
@@ -1,6 +1,6 @@
import { utils } from '@ohif/core';
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
@@ -8,13 +8,9 @@ import { dicomlabToRGB } from './utils/dicomlabToRGB';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
const loadPromises = {};
let loadPromises = {};
function _getDisplaySetsFromSeries(
instances,
servicesManager: AppTypes.ServicesManager,
extensionManager
) {
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
const instance = instances[0];
const {
@@ -70,32 +66,28 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
const referencedDisplaySet = referencedDisplaySets[0];
if (!referencedDisplaySet) {
// subscribe to display sets added which means at some point it will be available
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
({ displaySetsAdded }) => {
// here we can also do a little bit of search, since sometimes DICOM SEG
// does not contain the referenced display set uid , and we can just
// see which of the display sets added is more similar and assign it
// to the referencedDisplaySet
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
unsubscribe();
}
}
displaySet.getReferenceDisplaySet = () => {
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
} else {
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced DisplaySet is missing for the SEG');
}
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
}
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
const referencedVolumeId = `cornerstoneStreamingImageVolume:${displaySet.referencedVolumeURI}`;
displaySet.referencedVolumeId = referencedVolumeId;
return referencedDisplaySet;
};
displaySet.load = async ({ headers }) =>
await _load(displaySet, servicesManager, extensionManager, headers);
@@ -103,19 +95,14 @@ function _getDisplaySetsFromSeries(
return [displaySet];
}
function _load(
segDisplaySet,
servicesManager: AppTypes.ServicesManager,
extensionManager,
headers
) {
function _load(segDisplaySet, servicesManager, extensionManager, headers) {
const { SOPInstanceUID } = segDisplaySet;
const { segmentationService } = servicesManager.services;
if (
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
loadPromises[SOPInstanceUID] &&
_segmentationExists(segDisplaySet)
_segmentationExists(segDisplaySet, segmentationService)
) {
return loadPromises[SOPInstanceUID];
}
@@ -126,21 +113,17 @@ function _load(
// and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) {
try {
await _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
});
} catch (e) {
segDisplaySet.loading = false;
return reject(e);
}
await _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
});
}
const suppressEvents = true;
segmentationService
.createSegmentationForSEGDisplaySet(segDisplaySet)
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents)
.then(() => {
segDisplaySet.loading = false;
resolve();
@@ -154,12 +137,7 @@ function _load(
return loadPromises[SOPInstanceUID];
}
async function _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
}: withAppTypes) {
async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, headers }) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
@@ -169,20 +147,20 @@ async function _loadSegments({
const { dicomLoaderService } = utilityModule.exports;
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
segDisplaySet.referencedDisplaySetInstanceUID
);
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
if (!referencedDisplaySet) {
throw new Error('referencedDisplaySet is missing for SEG');
if (!cachedReferencedVolume) {
throw new Error(
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
);
}
const { instances: images } = referencedDisplaySet;
const imageIds = images.map(({ imageId }) => imageId);
const { imageIds } = cachedReferencedVolume;
// 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, {
@@ -234,8 +212,9 @@ async function _loadSegments({
Object.assign(segDisplaySet, results);
}
function _segmentationExists(segDisplaySet) {
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
function _segmentationExists(segDisplaySet, segmentationService) {
// This should be abstracted with the CornerstoneCacheService
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
@@ -1,4 +1,4 @@
export function getToolbarModule({ servicesManager }: withAppTypes) {
export function getToolbarModule({ servicesManager }) {
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
return [
{
@@ -8,7 +8,7 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
// forcing the button to have black background since initially
// it is designed for the toolbox not the toolbar on top
// we should then branch the buttonSectionId to have different styles
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
const segmentations = segmentationService.getSegmentations();
if (!segmentations?.length) {
return {
disabled: true,
@@ -3,6 +3,7 @@ import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import getPanelModule from './getPanelModule';
import getCommandsModule from './commandsModule';
import { getToolbarModule } from './getToolbarModule';
@@ -27,6 +28,13 @@ const extension = {
* You ID can be anything you want, but it should be unique.
*/
id,
/**
* PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name,
* iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/
getPanelModule,
getCommandsModule,
getToolbarModule,
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
@@ -0,0 +1,386 @@
import { createReportAsync } from '@ohif/extension-default';
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui';
import { SegmentationPanelMode } from '../types/segmentation';
import callInputDialog from './callInputDialog';
import callColorPickerDialog from './colorPickerDialog';
import { useTranslation } from 'react-i18next';
const components = {
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
[SegmentationPanelMode.Dropdown]: SegmentationGroupTable,
};
export default function PanelSegmentation({
servicesManager,
commandsManager,
extensionManager,
configuration,
}) {
const {
segmentationService,
viewportGridService,
uiDialogService,
displaySetService,
cornerstoneViewportService,
} = servicesManager.services;
const { t } = useTranslation('PanelSegmentation');
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
const [addSegmentationClassName, setAddSegmentationClassName] = useState('');
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
segmentationService.getConfiguration()
);
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
useEffect(() => {
// ~~ Subscription
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
const subscriptions = [];
[added, updated, removed].forEach(evt => {
const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = segmentationService.getSegmentations();
setSegmentations(segmentations);
setSegmentationConfiguration(segmentationService.getConfiguration());
});
subscriptions.push(unsubscribe);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, []);
// temporary measure to not allow add segmentation when the selected viewport
// is stack viewport
useEffect(() => {
const handleActiveViewportChange = viewportId => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
viewportId || viewportGridService.getActiveViewportId()
);
if (!displaySetUIDs) {
return;
}
const isReconstructable =
displaySetUIDs?.some(displaySetUID => {
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
return displaySet?.isReconstructable;
}) || false;
if (isReconstructable) {
setAddSegmentationClassName('');
} else {
setAddSegmentationClassName('ohif-disabled');
}
};
// Handle initial state
handleActiveViewportChange();
const changedGrid = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED;
const ready = viewportGridService.EVENTS.VIEWPORTS_READY;
const subsGrid = [];
[ready, changedGrid].forEach(evt => {
const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => {
handleActiveViewportChange(viewportId);
});
subsGrid.push(unsubscribe);
});
const changedData = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const subsData = [];
[changedData].forEach(evt => {
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, () => {
handleActiveViewportChange();
});
subsData.push(unsubscribe);
});
// Clean up
return () => {
subsGrid.forEach(unsub => unsub());
subsData.forEach(unsub => unsub());
};
}, []);
const getToolGroupIds = segmentationId => {
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
return toolGroupIds;
};
const onSegmentationAdd = async () => {
commandsManager.runCommand('createEmptySegmentationForViewport', {
viewportId: viewportGridService.getActiveViewportId(),
});
};
const onSegmentationClick = (segmentationId: string) => {
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
};
const onSegmentationDelete = (segmentationId: string) => {
segmentationService.remove(segmentationId);
};
const onSegmentAdd = segmentationId => {
segmentationService.addSegment(segmentationId);
};
const onSegmentClick = (segmentationId, segmentIndex) => {
segmentationService.setActiveSegment(segmentationId, segmentIndex);
const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => {
// const toolGroupId =
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
});
};
const onSegmentEdit = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex];
const { label } = segment;
callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') {
return;
}
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
});
};
const onSegmentationEdit = segmentationId => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const { label } = segmentation;
callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') {
return;
}
segmentationService.addOrUpdateSegmentation(
{
id: segmentationId,
label,
},
false, // suppress event
true // notYetUpdatedAtSource
);
});
};
const onSegmentColorClick = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
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) => {
segmentationService.removeSegment(segmentationId, segmentIndex);
};
// segment hide
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segmentInfo = segmentation.segments[segmentIndex];
const isVisible = !segmentInfo.isVisible;
const toolGroupIds = getToolGroupIds(segmentationId);
// Todo: right now we apply the visibility to all tool groups
toolGroupIds.forEach(toolGroupId => {
segmentationService.setSegmentVisibility(
segmentationId,
segmentIndex,
isVisible,
toolGroupId
);
});
};
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
};
const onToggleSegmentationVisibility = segmentationId => {
segmentationService.toggleSegmentationVisibility(segmentationId);
const segmentation = segmentationService.getSegmentation(segmentationId);
const isVisible = segmentation.isVisible;
const segments = segmentation.segments;
const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => {
segments.forEach((segment, segmentIndex) => {
segmentationService.setSegmentVisibility(
segmentationId,
segmentIndex,
isVisible,
toolGroupId
);
});
});
};
const _setSegmentationConfiguration = useCallback(
(segmentationId, key, value) => {
segmentationService.setConfiguration({
segmentationId,
[key]: value,
});
},
[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,
});
};
const SegmentationGroupTableComponent =
components[configuration?.segmentationPanelMode] || SegmentationGroupTable;
const allowAddSegment = configuration?.addSegment;
const onSegmentationAddWrapper =
configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function'
? configuration?.onSegmentationAdd
: onSegmentationAdd;
return (
<SegmentationGroupTableComponent
title={t('Segmentations')}
segmentations={segmentations}
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAddWrapper}
addSegmentationClassName={addSegmentationClassName}
showAddSegment={allowAddSegment}
onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete}
onSegmentationDownload={onSegmentationDownload}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
storeSegmentation={storeSegmentation}
onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
onSegmentAdd={onSegmentAdd}
onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentLock={onToggleSegmentLock}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
showDeleteSegment={true}
segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
}
setOutlineOpacityActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
}
setRenderFill={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
}
setRenderInactiveSegmentations={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value)
}
setOutlineWidthActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
}
setFillAlpha={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
}
setFillAlphaInactive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
}
/>
);
}
PanelSegmentation.propTypes = {
commandsManager: PropTypes.shape({
runCommand: PropTypes.func.isRequired,
}),
servicesManager: PropTypes.shape({
services: PropTypes.shape({
segmentationService: PropTypes.shape({
getSegmentation: PropTypes.func.isRequired,
getSegmentations: PropTypes.func.isRequired,
toggleSegmentationVisibility: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
EVENTS: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
};
@@ -0,0 +1,62 @@
import React from 'react';
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
function callInputDialog(uiDialogService, label, callback) {
const dialogId = 'enter-segment-label';
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
callback(value.label, action.id);
break;
case 'cancel':
callback('', action.id);
break;
}
uiDialogService.dismiss({ id: dialogId });
};
if (uiDialogService) {
uiDialogService.create({
id: dialogId,
centralize: true,
isDraggable: false,
showOverlay: true,
content: Dialog,
contentProps: {
title: 'Segment',
value: { label },
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
return (
<Input
label="Enter the segment label"
labelClassName="text-white text-[14px] leading-[1.2]"
autoFocus
className="border-primary-main bg-black"
type="text"
value={value.label}
onChange={event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
}}
onKeyPress={event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
}
}}
/>
);
},
},
});
}
}
export default callInputDialog;
@@ -4,7 +4,7 @@ import { ChromePicker } from 'react-color';
import './colorPickerDialog.css';
function colorPickerDialog(uiDialogService, rgbaColor, callback) {
function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
const dialogId = 'pick-color';
const onSubmitHandler = ({ action, value }) => {
@@ -55,4 +55,4 @@ function colorPickerDialog(uiDialogService, rgbaColor, callback) {
}
}
export default colorPickerDialog;
export default callColorPickerDialog;
@@ -0,0 +1,33 @@
// Todo: use defaults in cs3d
const initialState = {
renderOutline: true,
renderFill: true,
outlineOpacity: 0.9,
outlineWidth: 3,
fillOpacity: 0.9,
fillOpacityInactive: 0.8,
renderInactiveSegmentations: true,
};
const reducer = (state, action) => {
switch (action.type) {
case 'RENDER_OUTLINE':
return { ...state, renderOutline: action.payload.value };
case 'RENDER_FILL':
return { ...state, renderFill: action.payload.value };
case 'SET_OUTLINE_OPACITY':
return { ...state, outlineOpacity: action.payload.value };
case 'SET_OUTLINE_WIDTH':
return { ...state, outlineWidth: action.payload.value };
case 'SET_FILL_OPACITY':
return { ...state, fillOpacity: action.payload.value };
case 'SET_FILL_OPACITY_INACTIVE':
return { ...state, fillOpacityInactive: action.payload.value };
case 'RENDER_INACTIVE_SEGMENTATIONS':
return { ...state, renderInactiveSegmentations: action.payload.value };
default:
return state;
}
};
export { initialState, reducer };
@@ -0,0 +1,192 @@
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, isHangingProtocolLayout } = 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,
isHangingProtocolLayout
);
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.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
}
export {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
};
@@ -11,30 +11,24 @@ function promptHydrateSEG({
segDisplaySet,
viewportId,
preHydrateCallbacks,
hydrateCallback,
}: withAppTypes) {
hydrateSEGDisplaySet,
}) {
const { uiViewportDialogService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
const appConfig = extensionManager._appConfig;
return new Promise(async function (resolve, reject) {
const promptResult = appConfig?.disableConfirmationPrompts
? RESPONSE.HYDRATE_SEG
: await _askHydrate(uiViewportDialogService, viewportId);
const promptResult = await _askHydrate(uiViewportDialogService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
window.setTimeout(async () => {
const isHydrated = await hydrateCallback({
segDisplaySet,
viewportId,
});
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportId,
});
resolve(isHydrated);
}, 0);
resolve(isHydrated);
}
});
}
@@ -44,13 +38,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
const message = 'Do you want to open this Segmentation?';
const actions = [
{
id: 'no-hydrate',
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
@@ -1,14 +1,14 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
function OHIFCornerstoneSEGViewport(props: withAppTypes) {
function OHIFCornerstoneSEGViewport(props) {
const {
children,
displaySets,
@@ -37,6 +37,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
}
const segDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
@@ -60,11 +61,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet,
segDisplaySet
@@ -101,17 +98,18 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment
return (
<Component
{...props}
displaySets={[segDisplaySet]}
displaySets={[referencedDisplaySet, segDisplaySet]}
viewportOptions={{
viewportType: viewportOptions.viewportType,
viewportType: 'volume',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
@@ -141,35 +139,12 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
);
const hydrateSEG = useCallback(() => {
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: segDisplaySet,
type: SegmentationRepresentations.Labelmap,
});
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
});
viewportGridService.setDisplaySetsForViewport({
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
}, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]);
useEffect(() => {
if (segIsLoading) {
return;
@@ -180,18 +155,15 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
viewportId,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateCallback: hydrateSEG,
hydrateSEGDisplaySet,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
});
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
}, [servicesManager, viewportId, segDisplaySet, segIsLoading]);
useEffect(() => {
// on new seg display set, remove all segmentations from all viewports
segmentationService.clearSegmentationRepresentations(viewportId);
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
@@ -251,31 +223,39 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
return;
}
// keep the already stored segmentationPresentation for this viewport in memory
// so that we can restore it after hydrating the SEG
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: segDisplaySet,
type: SegmentationRepresentations.Labelmap,
});
// always start fresh for this viewport since it is special type of viewport
// that should only show one segmentation at a time.
segmentationService.clearSegmentationRepresentations(viewportId);
// This creates a custom tool group which has the lifetime of this view
// only, and does NOT interfere with currently displayed segmentations.
toolGroup = createSEGToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
return () => {
// remove the segmentation representations if seg displayset changed
// e.g., another seg displayset is dragged into the viewport
segmentationService.clearSegmentationRepresentations(viewportId);
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
// Only destroy the viewport specific implementation
toolGroupService.destroyToolGroup(toolGroupId);
};
}, []);
useEffect(() => {
setIsHydrated(segDisplaySet.isHydrated);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
referencedDisplaySetRef.current = null;
};
}, [segDisplaySet]);
const hydrateSEGDisplaySet = useCallback(
({ segDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [segDisplaySet],
viewportId,
});
},
[commandsManager]
);
const onStatusClick = useCallback(async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
@@ -284,11 +264,16 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
hydrateSEG();
}, [storePresentationState, hydrateSEG]);
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.addComponents([
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -307,7 +292,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
}
></ViewportActionArrows>
),
@@ -347,6 +332,18 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
} = referencedDisplaySetRef.current.metadata;
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">
@@ -365,6 +362,18 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
);
}
OHIFCornerstoneSEGViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
};
OHIFCornerstoneSEGViewport.defaultProps = {
customProps: {},
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
File diff suppressed because it is too large. Load diff
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.10.0-beta.6",
"version": "3.8.0",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@@ -34,21 +34,21 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.6",
"@ohif/extension-cornerstone": "3.10.0-beta.6",
"@ohif/extension-measurement-tracking": "3.10.0-beta.6",
"@ohif/ui": "3.10.0-beta.6",
"dcmjs": "*",
"@ohif/core": "3.8.0",
"@ohif/extension-cornerstone": "3.8.0",
"@ohif/extension-measurement-tracking": "3.8.0",
"@ohif/ui": "3.8.0",
"dcmjs": "^0.29.12",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^18.3.1"
"react": "^17.0.2"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^2.2.3",
"@cornerstonejs/core": "^2.2.3",
"@cornerstonejs/tools": "^2.2.3",
"@cornerstonejs/adapters": "^1.70.14",
"@cornerstonejs/core": "^1.70.14",
"@cornerstonejs/tools": "^1.70.14",
"classnames": "^2.3.2"
}
}
@@ -40,7 +40,7 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
return dataset;
};
const commandsModule = (props: withAppTypes) => {
const commandsModule = props => {
const { servicesManager } = props;
const { customizationService } = servicesManager.services;
const actions = {
@@ -57,7 +57,7 @@ const commandsModule = (props: withAppTypes) => {
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary.
const objectUrl = URL.createObjectURL(reportBlob);
var objectUrl = URL.createObjectURL(reportBlob);
window.location.assign(objectUrl);
},
@@ -1,78 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { OHIFCornerstoneSRContentItem } from './OHIFCornerstoneSRContentItem';
export function OHIFCornerstoneSRContainer(props) {
const { container, nodeIndexesTree = [0], containerNumberedTree = [1] } = props;
const { ContinuityOfContent, ConceptNameCodeSequence } = container;
const { CodeMeaning } = ConceptNameCodeSequence ?? {};
let childContainerIndex = 1;
const contentItems = container.ContentSequence?.map((contentItem, i) => {
const { ValueType } = contentItem;
const childNodeLevel = [...nodeIndexesTree, i];
const key = childNodeLevel.join('.');
let Component;
let componentProps;
if (ValueType === 'CONTAINER') {
const childContainerNumberedTree = [...containerNumberedTree, childContainerIndex++];
Component = OHIFCornerstoneSRContainer;
componentProps = {
container: contentItem,
nodeIndexesTree: childNodeLevel,
containerNumberedTree: childContainerNumberedTree,
};
} else {
Component = OHIFCornerstoneSRContentItem;
componentProps = {
contentItem,
nodeIndexesTree: childNodeLevel,
continuityOfContent: ContinuityOfContent,
};
}
return (
<Component
key={key}
{...componentProps}
/>
);
});
return (
<div>
<div className="font-bold">
{containerNumberedTree.join('.')}.&nbsp;
{CodeMeaning}
</div>
<div className="ml-4 mb-2">{contentItems}</div>
</div>
);
}
OHIFCornerstoneSRContainer.propTypes = {
/**
* A tree node that may contain another container or one or more content items
* (text, code, uidref, pname, etc.)
*/
container: PropTypes.object,
/**
* A 0-based index list
*/
nodeIndexesTree: PropTypes.arrayOf(PropTypes.number),
/**
* A 1-based index list that represents a container in a multi-level numbered
* list (tree).
*
* Example:
* 1. History
* 1.1. Chief Complaint
* 1.2. Present Illness
* 1.3. Past History
* 1.4. Family History
* 2. Findings
* */
containerNumberedTree: PropTypes.arrayOf(PropTypes.number),
};
@@ -1,63 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { CodeNameCodeSequenceValues } from '../enums';
import formatContentItemValue from '../utils/formatContentItem';
const EMPTY_TAG_VALUE = '[empty]';
function OHIFCornerstoneSRContentItem(props) {
const { contentItem, nodeIndexesTree, continuityOfContent } = props;
const { ConceptNameCodeSequence } = contentItem;
const { CodeValue, CodeMeaning } = ConceptNameCodeSequence;
const isChildFirstNode = nodeIndexesTree[nodeIndexesTree.length - 1] === 0;
const formattedValue = formatContentItemValue(contentItem) ?? EMPTY_TAG_VALUE;
const startWithAlphaNumCharRegEx = /^[a-zA-Z0-9]/;
const isContinuous = continuityOfContent === 'CONTINUOUS';
const isFinding = CodeValue === CodeNameCodeSequenceValues.Finding;
const addExtraSpace =
isContinuous && !isChildFirstNode && startWithAlphaNumCharRegEx.test(formattedValue?.[0]);
// Collapse sequences of white space preserving newline characters
let className = 'whitespace-pre-line';
if (CodeValue === CodeNameCodeSequenceValues.Finding) {
// Preserve spaces because it is common to see tabular text in a
// "Findings" ConceptNameCodeSequence
className = 'whitespace-pre-wrap';
}
if (isContinuous) {
return (
<>
<span
className={className}
title={CodeMeaning}
>
{addExtraSpace ? ' ' : ''}
{formattedValue}
</span>
</>
);
}
return (
<>
<div className="mb-2">
<span className="font-bold">{CodeMeaning}: </span>
{isFinding ? (
<pre>{formattedValue}</pre>
) : (
<span className={className}>{formattedValue}</span>
)}
</div>
</>
);
}
OHIFCornerstoneSRContentItem.propTypes = {
contentItem: PropTypes.object,
nodeIndexesTree: PropTypes.arrayOf(PropTypes.number),
continuityOfContent: PropTypes.string,
};
export { OHIFCornerstoneSRContentItem };
@@ -1,32 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { ExtensionManager } from '@ohif/core';
import { OHIFCornerstoneSRContainer } from './OHIFCornerstoneSRContainer';
function OHIFCornerstoneSRTextViewport(props: withAppTypes) {
const { displaySets } = props;
const displaySet = displaySets[0];
const instance = displaySet.instances[0];
return (
<div className="relative flex h-full w-full flex-col overflow-auto p-4 text-white">
<div>
{/* The root level is always a container */}
<OHIFCornerstoneSRContainer container={instance} />
</div>
</div>
);
}
OHIFCornerstoneSRTextViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
viewportLabel: PropTypes.string,
viewportOptions: PropTypes.object,
servicesManager: PropTypes.object.isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
export default OHIFCornerstoneSRTextViewport;
@@ -1,30 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { ExtensionManager } from '@ohif/core';
import OHIFCornerstoneSRMeasurementViewport from './OHIFCornerstoneSRMeasurementViewport';
import OHIFCornerstoneSRTextViewport from './OHIFCornerstoneSRTextViewport';
function OHIFCornerstoneSRViewport(props: withAppTypes) {
const { displaySets } = props;
const { isImagingMeasurementReport } = displaySets[0];
if (isImagingMeasurementReport) {
return <OHIFCornerstoneSRMeasurementViewport {...props}></OHIFCornerstoneSRMeasurementViewport>;
}
return <OHIFCornerstoneSRTextViewport {...props}></OHIFCornerstoneSRTextViewport>;
}
OHIFCornerstoneSRViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
viewportLabel: PropTypes.string,
viewportOptions: PropTypes.object,
servicesManager: PropTypes.object.isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
export default OHIFCornerstoneSRViewport;
@@ -0,0 +1,7 @@
export default {
POINT: 'POINT',
MULTIPOINT: 'MULTIPOINT',
POLYLINE: 'POLYLINE',
CIRCLE: 'CIRCLE',
ELLIPSE: 'ELLIPSE',
};
@@ -1,44 +0,0 @@
import { adaptersSR } from '@cornerstonejs/adapters';
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
export const SCOORDTypes = {
POINT: 'POINT',
MULTIPOINT: 'MULTIPOINT',
POLYLINE: 'POLYLINE',
CIRCLE: 'CIRCLE',
ELLIPSE: 'ELLIPSE',
};
export const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
ImagingMeasurements: '126010',
MeasurementGroup: '125007',
ImageLibraryGroup: '126200',
TrackingUniqueIdentifier: '112040',
TrackingIdentifier: '112039',
Finding: '121071',
FindingSite: 'G-C0E3', // SRT
FindingSiteSCT: '363698007', // SCT
};
export const CodingSchemeDesignators = {
SRT: 'SRT',
SCT: 'SCT',
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
};
export const RelationshipType = {
INFERRED_FROM: 'INFERRED FROM',
CONTAINS: 'CONTAINS',
};
const enums = {
CodeNameCodeSequenceValues,
CodingSchemeDesignators,
RelationshipType,
SCOORDTypes,
};
export default enums;
@@ -1,37 +1,30 @@
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import { adaptersSR } from '@cornerstonejs/adapters';
import addSRAnnotation from './utils/addSRAnnotation';
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
import isRehydratable from './utils/isRehydratable';
import {
SOPClassHandlerName,
SOPClassHandlerId,
SOPClassHandlerId3D,
SOPClassHandlerName3D,
} from './id';
import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums';
const { sopClassDictionary } = utils;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { ImageSet, MetadataProvider: metadataProvider } = classes;
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
import { adaptersSR } from '@cornerstonejs/adapters';
type InstanceMetadata = Types.InstanceMetadata;
/**
* TODO
* - [ ] Add SR thumbnail
* - [ ] Make viewport
* - [ ] Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport
*/
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
const { ImageSet, MetadataProvider: metadataProvider } = classes;
// TODO ->
// Add SR thumbnail
// Make viewport
// Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport.
const sopClassUids = [
sopClassDictionary.BasicTextSR,
sopClassDictionary.EnhancedSR,
sopClassDictionary.ComprehensiveSR,
'1.2.840.10008.5.1.4.1.1.88.11', //BASIC_TEXT_SR:
'1.2.840.10008.5.1.4.1.1.88.22', //ENHANCED_SR:
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
'1.2.840.10008.5.1.4.1.1.88.34', //COMPREHENSIVE_3D_SR:
];
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const validateSameStudyUID = (uid: string, instances): void => {
instances.forEach(it => {
if (it.StudyInstanceUID !== uid) {
@@ -41,6 +34,31 @@ const validateSameStudyUID = (uid: string, instances): void => {
});
};
const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
ImagingMeasurements: '126010',
MeasurementGroup: '125007',
ImageLibraryGroup: '126200',
TrackingUniqueIdentifier: '112040',
TrackingIdentifier: '112039',
Finding: '121071',
FindingSite: 'G-C0E3', // SRT
CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, //
};
const CodingSchemeDesignators = {
SRT: 'SRT',
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
};
const RELATIONSHIP_TYPE = {
INFERRED_FROM: 'INFERRED FROM',
CONTAINS: 'CONTAINS',
};
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
/**
* Adds instances to the DICOM SR series, rather than creating a new
* series, so that as SR's are saved, they append to the series, and the
@@ -67,11 +85,7 @@ function addInstances(instances: InstanceMetadata[], displaySetService: DisplayS
* @param servicesManager is the services that can be used for creating
* @returns The list of display sets created for the given instances object
*/
function _getDisplaySetsFromSeries(
instances,
servicesManager: AppTypes.ServicesManager,
extensionManager
) {
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
// If the series has no instances, stop here
if (!instances || !instances.length) {
throw new Error('No instances were provided');
@@ -95,12 +109,22 @@ function _getDisplaySetsFromSeries(
} = instance;
validateSameStudyUID(instance.StudyInstanceUID, instances);
const is3DSR = SOPClassUID === sopClassDictionary.Comprehensive3DSR;
const isImagingMeasurementReport =
ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurementReport;
if (
!ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !== CodeNameCodeSequenceValues.ImagingMeasurementReport
) {
servicesManager.services.uiNotificationService.show({
title: 'DICOM SR',
message:
'OHIF only supports TID1500 Imaging Measurement Report Structured Reports. The SR you’re trying to view is not supported.',
type: 'warning',
duration: 6000,
});
return [];
}
const displaySet = {
//plugin: id,
Modality: 'SR',
displaySetInstanceUID: utils.guid(),
SeriesDescription,
@@ -109,14 +133,13 @@ function _getDisplaySetsFromSeries(
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
SOPClassHandlerId: is3DSR ? SOPClassHandlerId3D : SOPClassHandlerId,
SOPClassHandlerId,
SOPClassUID,
instances,
referencedImages: null,
measurements: null,
isDerivedDisplaySet: true,
isLoaded: false,
isImagingMeasurementReport,
sopClassUids,
instance,
addInstances,
@@ -127,21 +150,12 @@ function _getDisplaySetsFromSeries(
return [displaySet];
}
/**
* Loads the display set with the given services and extension manager.
* @param srDisplaySet - The display set to load.
* @param servicesManager - The services manager containing displaySetService and measurementService.
* @param extensionManager - The extension manager containing data sources.
*/
async function _load(
srDisplaySet: Types.DisplaySet,
servicesManager: AppTypes.ServicesManager,
extensionManager: AppTypes.ExtensionManager
) {
async function _load(displaySet, servicesManager, extensionManager) {
const { displaySetService, measurementService } = servicesManager.services;
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const { ContentSequence } = srDisplaySet.instance;
const { ContentSequence } = displaySet.instance;
async function retrieveBulkData(obj, parentObj = null, key = null) {
for (const prop in obj) {
@@ -152,9 +166,9 @@ async function _load(
} else if (prop === 'BulkDataURI') {
const value = await dataSource.retrieve.bulkDataURI({
BulkDataURI: obj[prop],
StudyInstanceUID: srDisplaySet.instance.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.instance.SeriesInstanceUID,
SOPInstanceUID: srDisplaySet.instance.SOPInstanceUID,
StudyInstanceUID: displaySet.instance.StudyInstanceUID,
SeriesInstanceUID: displaySet.instance.SeriesInstanceUID,
SOPInstanceUID: displaySet.instance.SOPInstanceUID,
});
if (parentObj && key) {
parentObj[key] = new Float32Array(value);
@@ -163,47 +177,40 @@ async function _load(
}
}
if (srDisplaySet.isLoaded !== true) {
if (displaySet.isLoaded !== true) {
await retrieveBulkData(ContentSequence);
}
if (srDisplaySet.isImagingMeasurementReport) {
srDisplaySet.referencedImages = _getReferencedImagesList(ContentSequence);
srDisplaySet.measurements = _getMeasurements(ContentSequence);
} else {
srDisplaySet.referencedImages = [];
srDisplaySet.measurements = [];
}
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
displaySet.measurements = _getMeasurements(ContentSequence);
const mappings = measurementService.getSourceMappings(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
srDisplaySet.isHydrated = false;
srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings);
srDisplaySet.isLoaded = true;
displaySet.isHydrated = false;
displaySet.isRehydratable = isRehydratable(displaySet, mappings);
displaySet.isLoaded = true;
/** 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 => {
_checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
displaySet,
activeDisplaySet,
dataSource,
servicesManager
);
});
/** 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.EVENTS.DISPLAY_SETS_ADDED, data => {
const { displaySetsAdded } = data;
/**
* If there are still some measurements that have not yet been loaded into cornerstone,
* See if we can load them onto any of the new displaySets.
*/
// If there are still some measurements that have not yet been loaded into cornerstone,
// See if we can load them onto any of the new displaySets.
displaySetsAdded.forEach(newDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
displaySet,
newDisplaySet,
dataSource,
servicesManager
@@ -212,118 +219,115 @@ async function _load(
});
}
/**
* Checks if measurements can be added to a display set.
*
* @param srDisplaySet - The source display set containing measurements.
* @param newDisplaySet - The new display set to check if measurements can be added.
* @param dataSource - The data source used to retrieve image IDs.
* @param servicesManager - The services manager.
*/
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
dataSource,
servicesManager: AppTypes.ServicesManager
servicesManager
) {
const { customizationService } = servicesManager.services;
const unloadedMeasurements = srDisplaySet.measurements.filter(
let unloadedMeasurements = srDisplaySet.measurements.filter(
measurement => measurement.loaded === false
);
if (
unloadedMeasurements.length === 0 ||
!(newDisplaySet instanceof ImageSet) ||
newDisplaySet.unsupported
) {
if (unloadedMeasurements.length === 0) {
// All already loaded!
return;
}
// const { sopClassUids } = newDisplaySet;
// Create a Set for faster lookups
// const sopClassUidSet = new Set(sopClassUids);
if ((!newDisplaySet) instanceof ImageSet) {
// This also filters out _this_ displaySet, as it is not an ImageSet.
return;
}
// Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number
const imageIdMap = new Map<string, string>();
const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet);
if (newDisplaySet.unsupported) {
return;
}
const { sopClassUids } = newDisplaySet;
// Check if any have the newDisplaySet is the correct SOPClass.
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
measurement.coords.some(coord =>
sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID)
)
);
if (unloadedMeasurements.length === 0) {
// New displaySet isn't the correct SOPClass, so can't contain the referenced images.
return;
}
const SOPInstanceUIDs = [];
unloadedMeasurements.forEach(measurement => {
const { coords } = measurement;
coords.forEach(coord => {
const SOPInstanceUID = coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
SOPInstanceUIDs.push(SOPInstanceUID);
}
});
});
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet);
for (const imageId of imageIdsForDisplaySet) {
if (!unloadedMeasurements.length) {
// All measurements loaded.
return;
}
for (const imageId of imageIds) {
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
const key = `${SOPInstanceUID}:${frameNumber || 1}`;
imageIdMap.set(key, imageId);
}
if (!unloadedMeasurements?.length) {
return;
}
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j];
const is3DSR = srDisplaySet.SOPClassUID === sopClassDictionary.Comprehensive3DSR;
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
'onBeforeSRAddMeasurement'
)?.value;
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j];
if (typeof onBeforeSRAddMeasurement === 'function') {
measurement = onBeforeSRAddMeasurement({
measurement,
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
});
}
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
'onBeforeSRAddMeasurement'
)?.value;
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
const frame =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
1;
if (typeof onBeforeSRAddMeasurement === 'function') {
measurement = onBeforeSRAddMeasurement({
measurement,
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
});
}
/** Add DICOMSRDisplay annotation for the SR viewport (only) */
addDICOMSRDisplayAnnotation(measurement, imageId, frame);
// if it is 3d SR we can just add the SR annotation
if (is3DSR) {
addSRAnnotation(measurement, null, null);
measurement.loaded = true;
continue;
}
/** Update measurement properties */
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
measurement.ReferencedSOPInstanceUID =
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
measurement.frameNumber = frame;
delete measurement.coords;
const referencedSOPSequence = measurement.coords[0].ReferencedSOPSequence;
if (!referencedSOPSequence) {
continue;
}
const { ReferencedSOPInstanceUID } = referencedSOPSequence;
const frame = referencedSOPSequence.ReferencedFrameNumber || 1;
const key = `${ReferencedSOPInstanceUID}:${frame}`;
const imageId = imageIdMap.get(key);
if (
imageId &&
_measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame)
) {
addSRAnnotation(measurement, imageId, frame);
// Update measurement properties
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID;
measurement.frameNumber = frame;
unloadedMeasurements.splice(j, 1);
unloadedMeasurements.splice(j, 1);
}
}
}
}
}
/**
* Checks if a measurement references a specific SOP Instance UID.
* @param measurement - The measurement object.
* @param SOPInstanceUID - The SOP Instance UID to check against.
* @param frameNumber - The frame number to check against (optional).
* @returns True if the measurement references the specified SOP Instance UID, false otherwise.
*/
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
const { coords } = measurement;
/**
* NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
* Standard. But for now, we will support only one ReferenceFrameNumber.
*/
// NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
// Standard. But for now, we will support only one ReferenceFrameNumber.
const ReferencedFrameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
@@ -336,62 +340,40 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
for (let j = 0; j < coords.length; j++) {
const coord = coords[j];
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
if (ReferencedSOPInstanceUID === SOPInstanceUID) {
return true;
}
}
return false;
}
/**
* Retrieves the SOP class handler module.
*
* @param {Object} options - The options for retrieving the SOP class handler module.
* @param {Object} options.servicesManager - The services manager.
* @param {Object} options.extensionManager - The extension manager.
* @returns {Array} An array containing the SOP class handler module.
*/
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
return [
{
name: SOPClassHandlerName,
sopClassUids,
getDisplaySetsFromSeries,
},
{
name: SOPClassHandlerName3D,
sopClassUids: [sopClassDictionary.Comprehensive3DSR],
getDisplaySetsFromSeries,
},
];
}
/**
* Retrieves the measurements from the ImagingMeasurementReportContentSequence.
*
* @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array.
* @returns {Array} - The array of measurements.
*/
function _getMeasurements(ImagingMeasurementReportContentSequence) {
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements
);
if (!ImagingMeasurements) {
return [];
}
const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup
);
const mergedContentSequencesByTrackingUniqueIdentifiers =
_getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups);
const measurements = [];
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
@@ -400,6 +382,7 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier];
const measurement = _processMeasurement(mergedContentSequence);
if (measurement) {
measurements.push(measurement);
}
@@ -409,12 +392,6 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
return measurements;
}
/**
* Retrieves merged content sequences by tracking unique identifiers.
*
* @param {Array} MeasurementGroups - The measurement groups.
* @returns {Object} - The merged content sequences by tracking unique identifiers.
*/
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
@@ -426,6 +403,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
);
if (!TrackingUniqueIdentifierItem) {
console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.');
}
@@ -454,15 +432,6 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
return mergedContentSequencesByTrackingUniqueIdentifiers;
}
/**
* Processes the measurement based on the merged content sequence.
* If the merged content sequence contains SCOORD or SCOORD3D value types,
* it calls the _processTID1410Measurement function.
* Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function.
*
* @param {Array<Object>} mergedContentSequence - The merged content sequence to process.
* @returns {any} - The processed measurement result.
*/
function _processMeasurement(mergedContentSequence) {
if (
mergedContentSequence.some(
@@ -475,21 +444,11 @@ function _processMeasurement(mergedContentSequence) {
return _processNonGeometricallyDefinedMeasurement(mergedContentSequence);
}
/**
* Processes TID 1410 style measurements from the mergedContentSequence.
* TID 1410 style measurements have a SCOORD or SCOORD3D at the top level,
* and non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D.
*
* @param mergedContentSequence - The merged content sequence containing the measurements.
* @returns The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier.
*/
function _processTID1410Measurement(mergedContentSequence) {
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
const graphicItem = mergedContentSequence.find(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
);
const graphicItem = mergedContentSequence.find(group => group.ValueType === 'SCOORD');
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
@@ -516,6 +475,7 @@ function _processTID1410Measurement(mergedContentSequence) {
NUMContentItems.forEach(item => {
const { ConceptNameCodeSequence, MeasuredValueSequence } = item;
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
@@ -523,29 +483,12 @@ function _processTID1410Measurement(mergedContentSequence) {
}
});
const findingSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SCT &&
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSiteSCT
);
if (findingSites.length) {
measurement.labels.push({
label: CodeNameCodeSequenceValues.FindingSiteSCT,
value: findingSites[0].ConceptCodeSequence.CodeMeaning,
});
}
return measurement;
}
/**
* Processes the non-geometrically defined measurement from the merged content sequence.
*
* @param mergedContentSequence The merged content sequence containing the measurement data.
* @returns The processed measurement object.
*/
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
const TrackingIdentifierContentItem = mergedContentSequence.find(
@@ -575,10 +518,10 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
finding.ConceptCodeSequence.CodingSchemeDesignator
) &&
finding.ConceptCodeSequence.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
finding.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
) {
measurement.labels.push({
label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
label: CORNERSTONE_FREETEXT_CODE_VALUE,
value: finding.ConceptCodeSequence.CodeMeaning,
});
}
@@ -590,13 +533,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) &&
FindingSite.ConceptCodeSequence.CodeValue ===
Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
);
if (cornerstoneFreeTextFindingSite) {
measurement.labels.push({
label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
label: CORNERSTONE_FREETEXT_CODE_VALUE,
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
});
}
@@ -606,12 +548,15 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
const { ValueType } = ContentSequence;
if (!ValueType === 'SCOORD') {
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
return;
}
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
if (coords) {
measurement.coords.push(coords);
}
@@ -626,62 +571,59 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
return measurement;
}
/**
* Extracts coordinates from a graphic item of type SCOORD or SCOORD3D.
* @param {object} graphicItem - The graphic item containing the coordinates.
* @returns {object} - The extracted coordinates.
*/
const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => {
const { ValueType, GraphicType, GraphicData } = graphicItem;
const coords = { ValueType, GraphicType, GraphicData };
coords.ReferencedSOPSequence = graphicItem.ContentSequence?.ReferencedSOPSequence;
coords.ReferencedFrameOfReferenceSequence =
graphicItem.ReferencedFrameOfReferenceUID ||
graphicItem.ContentSequence?.ReferencedFrameOfReferenceSequence;
return coords;
};
function _getCoordsFromSCOORDOrSCOORD3D(item) {
const { ValueType, RelationshipType, GraphicType, GraphicData } = item;
if (
!(
RelationshipType == RELATIONSHIP_TYPE.INFERRED_FROM ||
RelationshipType == RELATIONSHIP_TYPE.CONTAINS
)
) {
console.warn(
`Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM" or "CONTAINS"`
);
return;
}
const coords = { ValueType, GraphicType, GraphicData };
// ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM'
if (ValueType === 'SCOORD') {
const { ReferencedSOPSequence } = item.ContentSequence;
coords.ReferencedSOPSequence = ReferencedSOPSequence;
} else if (ValueType === 'SCOORD3D') {
const { ReferencedFrameOfReferenceSequence } = item.ContentSequence;
coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence;
}
return coords;
}
/**
* Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence.
* @param {Object} ConceptNameCodeSequence - The ConceptNameCodeSequence object.
* @param {Object} MeasuredValueSequence - The MeasuredValueSequence object.
* @returns {Object} - An object containing the label and value.
* The label represents the CodeMeaning from the ConceptNameCodeSequence.
* The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence.
* Example: { label: 'Long Axis', value: '31.00 mm' }
*/
function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) {
const { CodeMeaning } = ConceptNameCodeSequence;
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
const { CodeValue } = MeasurementUnitsCodeSequence;
const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : '';
return {
label: CodeMeaning,
value: `${formatedNumericValue} ${CodeValue}`,
}; // E.g. Long Axis: 31.0 mm
}
/**
* Retrieves a list of referenced images from the Imaging Measurement Report Content Sequence.
*
* @param {Array} ImagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence.
* @returns {Array} - The list of referenced images.
*/
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
);
if (!ImageLibrary) {
return [];
}
const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
);
if (!ImageLibraryGroup) {
return [];
}
const referencedImages = [];
@@ -705,15 +647,6 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
return referencedImages;
}
/**
* Converts a DICOM sequence to an array.
* If the sequence is null or undefined, an empty array is returned.
* If the sequence is already an array, it is returned as is.
* Otherwise, the sequence is wrapped in an array and returned.
*
* @param {any} sequence - The DICOM sequence to convert.
* @returns {any[]} - The converted array.
*/
function _getSequenceAsArray(sequence) {
if (!sequence) {
return [];
+1 -4
View File
@@ -5,7 +5,4 @@ const id = packageJson.name;
const SOPClassHandlerName = 'dicom-sr';
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
const SOPClassHandlerName3D = 'dicom-sr-3d';
const SOPClassHandlerId3D = `${id}.sopClassHandlerModule.${SOPClassHandlerName3D}`;
export { SOPClassHandlerName, SOPClassHandlerId, SOPClassHandlerName3D, SOPClassHandlerId3D, id };
export { SOPClassHandlerName, SOPClassHandlerId, id };
@@ -8,10 +8,9 @@ import { id } from './id.js';
import toolNames from './tools/toolNames';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
import Enums from './enums';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './components/OHIFCornerstoneSRViewport');
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport');
});
const OHIFCornerstoneSRViewport = props => {
@@ -71,4 +70,4 @@ const dicomSRExtension = {
export default dicomSRExtension;
// Put static exports here so they can be type checked
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol, Enums, toolNames };
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol };
+2 -54
View File
@@ -9,28 +9,16 @@ import {
LengthTool,
PlanarFreehandROITool,
RectangleROITool,
utilities as csToolsUtils,
} from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/core';
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper';
import addToolInstance from './utils/addToolInstance';
import { Types } from '@ohif/core';
import toolNames from './tools/toolNames';
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
/**
* @param {object} configuration
*/
export default function init({
configuration = {},
servicesManager,
}: Types.Extensions.ExtensionParams): void {
const { measurementService, cornerstoneViewportService } = servicesManager.services;
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool);
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
@@ -40,26 +28,10 @@ export default function init({
addToolInstance(toolNames.SRAngle, AngleTool);
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
addToolInstance(toolNames.SRSCOORD3DPoint, SCOORD3DPointTool);
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
const csTools3DVer1MeasurementSource = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
const { POINT } = measurementService.VALUE_TYPES;
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'SRSCOORD3DPoint',
POINT,
SRSCOOR3DProbeMapper.toAnnotation,
SRSCOOR3DProbeMapper.toMeasurement
);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
@@ -77,28 +49,4 @@ export default function init({
SRRectangleROI: dashedLine,
global: {},
});
measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
({ viewportId, measurement, isConsumed }) => {
if (isConsumed) {
return;
}
try {
const currentViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { viewPlaneNormal } = currentViewport.getCamera();
const referencedImageId = csToolsUtils.getClosestImageIdForStackViewport(
currentViewport as StackViewport,
measurement.points[0],
viewPlaneNormal
);
const imageIndex = (currentViewport as StackViewport)
.getImageIds()
.indexOf(referencedImageId);
csUtils.jumpToSlice(currentViewport.element, { imageIndex });
} catch (error) {
console.warn('Unable to jump to image based on measurement coordinate', error);
}
}
);
}
@@ -1,11 +1,11 @@
import { SOPClassHandlerId, SOPClassHandlerId3D } from './id';
import { SOPClassHandlerId } from './id';
export default function onModeEnter({ servicesManager }) {
const { displaySetService } = servicesManager.services;
const displaySetCache = displaySetService.getDisplaySetCache();
const srDisplaySets = [...displaySetCache.values()].filter(
ds => ds.SOPClassHandlerId === SOPClassHandlerId || ds.SOPClassHandlerId === SOPClassHandlerId3D
ds => ds.SOPClassHandlerId === SOPClassHandlerId
);
srDisplaySets.forEach(ds => {
@@ -7,11 +7,10 @@ import {
Types as cs3DToolsTypes,
} from '@cornerstonejs/tools';
import { getTrackingUniqueIdentifiersForElement } from './modules/dicomSRModule';
import { SCOORDTypes } from '../enums';
import toolNames from './toolNames';
import SCOORD_TYPES from '../constants/scoordTypes';
export default class DICOMSRDisplayTool extends AnnotationTool {
static toolName = toolNames.DICOMSRDisplay;
static toolName = 'DICOMSRDisplay';
constructor(
toolProps = {},
@@ -113,19 +112,19 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
let canvasCoordinatesAdapter;
switch (GraphicType) {
case SCOORDTypes.POINT:
case SCOORD_TYPES.POINT:
renderMethod = this.renderPoint;
break;
case SCOORDTypes.MULTIPOINT:
case SCOORD_TYPES.MULTIPOINT:
renderMethod = this.renderMultipoint;
break;
case SCOORDTypes.POLYLINE:
case SCOORD_TYPES.POLYLINE:
renderMethod = this.renderPolyLine;
break;
case SCOORDTypes.CIRCLE:
case SCOORD_TYPES.CIRCLE:
renderMethod = this.renderEllipse;
break;
case SCOORDTypes.ELLIPSE:
case SCOORD_TYPES.ELLIPSE:
renderMethod = this.renderEllipse;
canvasCoordinatesAdapter = utilities.math.ellipse.getCanvasEllipseCorners;
break;
@@ -230,11 +229,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
// This gives us one point for arrow
canvasCoordinates.push(viewport.worldToCanvas(point));
if (data[1] !== undefined) {
canvasCoordinates.push(viewport.worldToCanvas(data[1]));
}
else{
// We get the other point for the arrow by using the image size
// We get the other point for the arrow by using the image size
const imagePixelModule = metaData.get('imagePixelModule', referencedImageId);
let xOffset = 10;
@@ -253,9 +248,6 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
]);
canvasCoordinates.push(viewport.worldToCanvas(arrowEnd));
}
const arrowUID = `${index}`;
@@ -343,7 +335,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
}
const { annotationUID, data = {} } = annotation;
const { labels } = data;
const { label } = data;
const { color } = options;
let adaptedCanvasCoordinates = canvasCoordinates;
@@ -351,7 +343,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
if (typeof canvasCoordinatesAdapter === 'function') {
adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates);
}
const textLines = this._getTextBoxLinesFromLabels(labels);
const textLines = this._getTextBoxLinesFromLabels(label);
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
if (!annotation.data?.handles?.textBox?.worldPosition) {
@@ -1,203 +0,0 @@
import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core';
import {
annotation,
drawing,
utilities,
Types as cs3DToolsTypes,
AnnotationDisplayTool,
} from '@cornerstonejs/tools';
import toolNames from './toolNames';
import { Annotation } from '@cornerstonejs/tools/dist/types/types';
export default class SCOORD3DPointTool extends AnnotationDisplayTool {
static toolName = toolNames.SRSCOORD3DPoint;
constructor(
toolProps = {},
defaultToolProps = {
configuration: {},
}
) {
super(toolProps, defaultToolProps);
}
_getTextBoxLinesFromLabels(labels) {
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
const labelLength = Math.min(labels.length, 5);
const lines = [];
return lines;
}
// This tool should not inherit from AnnotationTool and we should not need
// to add the following lines.
isPointNearTool = () => null;
getHandleNearImagePoint = () => null;
renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => {
const { viewport } = enabledElement;
const { element } = viewport;
const annotations = annotation.state.getAnnotations(this.getToolName(), element);
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
if (!annotations?.length) {
return;
}
// Filter toolData to only render the data for the active SR.
const filteredAnnotations = annotations;
if (!viewport._actors?.size) {
return;
}
const styleSpecifier: cs3DToolsTypes.AnnotationStyle.StyleSpecifier = {
toolGroupId: this.toolGroupId,
toolName: this.getToolName(),
viewportId: enabledElement.viewport.id,
};
for (let i = 0; i < filteredAnnotations.length; i++) {
const annotation = filteredAnnotations[i];
const annotationUID = annotation.annotationUID;
const { renderableData } = annotation.data;
const { POINT: points } = renderableData;
styleSpecifier.annotationUID = annotationUID;
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
const color = this.getStyle('color', styleSpecifier, annotation);
const options = {
color,
lineDash,
lineWidth,
};
const point = points[0][0];
// check if viewport can render it
const viewable = viewport.isReferenceViewable(
{ FrameOfReferenceUID: annotation.metadata.FrameOfReferenceUID, cameraFocalPoint: point },
{ asNearbyProjection: true }
);
if (!viewable) {
continue;
}
// render the point
const arrowPointCanvas = viewport.worldToCanvas(point);
// Todo: configure this
const arrowEndCanvas = [arrowPointCanvas[0] + 20, arrowPointCanvas[1] + 20];
const canvasCoordinates = [arrowPointCanvas, arrowEndCanvas];
drawing.drawArrow(
svgDrawingHelper,
annotationUID,
'1',
canvasCoordinates[1],
canvasCoordinates[0],
{
color: options.color,
width: options.lineWidth,
}
);
this.renderTextBox(
svgDrawingHelper,
viewport,
canvasCoordinates,
annotation,
styleSpecifier,
options
);
}
};
renderTextBox(
svgDrawingHelper,
viewport,
canvasCoordinates,
annotation,
styleSpecifier,
options = {}
) {
if (!canvasCoordinates || !annotation) {
return;
}
const { annotationUID, data = {} } = annotation;
const { labels } = data;
const textLines = [];
for (const label of labels) {
// make this generic
// fix this
if (label.label === '363698007') {
textLines.push(`Finding Site: ${label.value}`);
}
}
const { color } = options;
const adaptedCanvasCoordinates = canvasCoordinates;
// adapt coordinates if there is an adapter
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
if (!annotation.data?.handles?.textBox?.worldPosition) {
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
}
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
const textBoxUID = '1';
const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
const boundingBox = drawing.drawLinkedTextBox(
svgDrawingHelper,
annotationUID,
textBoxUID,
textLines,
textBoxPosition,
canvasCoordinates,
{},
{
...textBoxOptions,
color,
}
);
const { x: left, y: top, width, height } = boundingBox;
annotation.data.handles.textBox.worldBoundingBox = {
topLeft: viewport.canvasToWorld([left, top]),
topRight: viewport.canvasToWorld([left + width, top]),
bottomLeft: viewport.canvasToWorld([left, top + height]),
bottomRight: viewport.canvasToWorld([left + width, top + height]),
};
}
public getLinkedTextBoxStyle(
specifications: cs3DToolsTypes.AnnotationStyle.StyleSpecifier,
annotation?: Annotation
): Record<string, unknown> {
// Todo: this function can be used to set different styles for different toolMode
// for the textBox.
return {
visibility: this.getStyle('textBoxVisibility', specifications, annotation),
fontFamily: this.getStyle('textBoxFontFamily', specifications, annotation),
fontSize: this.getStyle('textBoxFontSize', specifications, annotation),
color: this.getStyle('textBoxColor', specifications, annotation),
shadow: this.getStyle('textBoxShadow', specifications, annotation),
background: this.getStyle('textBoxBackground', specifications, annotation),
lineWidth: this.getStyle('textBoxLinkLineWidth', specifications, annotation),
lineDash: this.getStyle('textBoxLinkLineDash', specifications, annotation),
};
}
}
@@ -1,5 +1,7 @@
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
const toolNames = {
DICOMSRDisplay: 'DICOMSRDisplay',
DICOMSRDisplay: DICOMSRDisplayTool.toolName,
SRLength: 'SRLength',
SRBidirectional: 'SRBidirectional',
SREllipticalROI: 'SREllipticalROI',
@@ -9,7 +11,6 @@ const toolNames = {
SRCobbAngle: 'SRCobbAngle',
SRRectangleROI: 'SRRectangleROI',
SRPlanarFreehandROI: 'SRPlanarFreehandROI',
SRSCOORD3DPoint: 'SRSCOORD3DPoint',
};
export default toolNames;
@@ -1,62 +0,0 @@
const SRSCOOR3DProbe = {
toAnnotation: measurement => {},
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType,
customizationService
) => {
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('Probe tool: Missing metadata or data');
return null;
}
const { toolName } = metadata;
const { points } = data.handles;
const displayText = getDisplayText(annotation);
return {
uid: annotationUID,
points,
metadata,
toolName: metadata.toolName,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType?.(toolName) ?? null,
};
},
};
function getDisplayText(annotation) {
const { data } = annotation;
if (!data) {
return [''];
}
const { labels } = data;
const displayText = [];
for (const label of labels) {
// make this generic
if (label.label === '33636980076') {
displayText.push(`Finding Site: ${label.value}`);
}
}
return displayText;
}
export default SRSCOOR3DProbe;
@@ -1,49 +1,106 @@
import { vec3 } from 'gl-matrix';
import { Types, annotation } from '@cornerstonejs/tools';
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
import { SCOORDTypes } from '../enums';
import toolNames from '../tools/toolNames';
import SCOORD_TYPES from '../constants/scoordTypes';
const EPSILON = 1e-4;
const getRenderableCoords = ({ GraphicData, ValueType, imageId }) => {
const renderableData = [];
if (ValueType === 'SCOORD3D') {
for (let i = 0; i < GraphicData.length; i += 3) {
renderableData.push([GraphicData[i], GraphicData[i + 1], GraphicData[i + 2]]);
}
} else {
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [GraphicData[i], GraphicData[i + 1]]);
renderableData.push(worldPos);
}
}
return renderableData;
};
export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) {
const toolName = toolNames.DICOMSRDisplay;
function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
let renderableData = [];
const measurementData = {
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
renderableData: {},
labels: measurement.labels,
imageId,
};
measurement.coords.forEach(coord => {
const { GraphicType, GraphicData } = coord;
if (measurementData.renderableData[GraphicType] === undefined) {
measurementData.renderableData[GraphicType] = [];
}
measurementData.renderableData[GraphicType].push(
_getRenderableData(GraphicType, GraphicData, imageId)
);
});
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
/**
* This annotation (DICOMSRDisplay) is only used by the SR viewport.
* This is used before the annotation is hydrated. If hydrated the measurement will be added
* to the measurement service and will be available for the other viewports.
*/
const SRAnnotation: Types.Annotation = {
annotationUID: measurement.TrackingUniqueIdentifier,
highlighted: false,
isLocked: false,
invalidated: false,
metadata: {
toolName: toolName,
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
referencedImageId: imageId,
},
data: {
label: measurement.labels,
handles: {
textBox: measurement.textBox ?? {},
},
cachedStats: {},
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
frameNumber,
},
};
const annotationManager = annotation.state.getAnnotationManager();
annotationManager.addAnnotation(SRAnnotation);
}
function _getRenderableData(GraphicType, GraphicData, imageId) {
let renderableData: csTypes.Point3[];
switch (GraphicType) {
case SCOORDTypes.POINT:
case SCOORDTypes.MULTIPOINT:
case SCOORDTypes.POLYLINE: {
renderableData = getRenderableCoords({ GraphicData, ValueType, imageId });
case SCOORD_TYPES.POINT:
case SCOORD_TYPES.MULTIPOINT:
case SCOORD_TYPES.POLYLINE:
renderableData = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
renderableData.push(worldPos);
}
break;
}
case SCOORDTypes.CIRCLE: {
const pointsWorld: csTypes.Point3[] = getRenderableCoords({
GraphicData,
ValueType,
imageId,
});
case SCOORD_TYPES.CIRCLE: {
const pointsWorld = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
pointsWorld.push(worldPos);
}
// We do not have an explicit draw circle svg helper in Cornerstone3D at
// this time, but we can use the ellipse svg helper to draw a circle, so
// here we reshape the data for that purpose.
const center = pointsWorld[0];
const onPerimeter = pointsWorld[1];
const radius = vec3.distance(center, onPerimeter);
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
if (!imagePlaneModule) {
throw new Error('No imagePlaneModule found');
}
@@ -58,12 +115,14 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
// we need to get major/minor axis (which are both the same size major = minor)
// first axisStart
const firstAxisStart = vec3.create();
vec3.scaleAndAdd(firstAxisStart, center, columnCosines, radius);
const firstAxisEnd = vec3.create();
vec3.scaleAndAdd(firstAxisEnd, center, columnCosines, -radius);
// second axisStart
const secondAxisStart = vec3.create();
vec3.scaleAndAdd(secondAxisStart, center, rowCosines, radius);
@@ -79,16 +138,21 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
break;
}
case SCOORDTypes.ELLIPSE: {
case SCOORD_TYPES.ELLIPSE: {
// GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY]
// But Cornerstone3D points are ordered as top, bottom, left, right for the
// ellipse so we need to identify if the majorAxis is horizontal or vertical
// and then choose the correct points to use for the ellipse.
const pointsWorld: csTypes.Point3[] = getRenderableCoords({
GraphicData,
ValueType,
imageId,
});
const pointsWorld: csTypes.Point3[] = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
pointsWorld.push(worldPos);
}
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
@@ -138,5 +202,3 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
return renderableData;
}
export default getRenderableData;
@@ -1,67 +0,0 @@
import { Types, annotation } from '@cornerstonejs/tools';
import { metaData } from '@cornerstonejs/core';
import getRenderableData from './getRenderableData';
import toolNames from '../tools/toolNames';
export default function addSRAnnotation(measurement, imageId, frameNumber) {
let toolName = toolNames.DICOMSRDisplay;
const renderableData = measurement.coords.reduce((acc, coordProps) => {
acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || [];
acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId }));
return acc;
}, {});
const { TrackingUniqueIdentifier } = measurement;
const { ValueType: valueType, GraphicType: graphicType } = measurement.coords[0];
const graphicTypePoints = renderableData[graphicType];
/** TODO: Read the tool name from the DICOM SR identification type in the future. */
let frameOfReferenceUID = null;
if (imageId) {
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
frameOfReferenceUID = imagePlaneModule?.frameOfReferenceUID;
}
if (valueType === 'SCOORD3D') {
toolName = toolNames.SRSCOORD3DPoint;
// get the ReferencedFrameOfReferenceUID from the measurement
frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence;
}
const SRAnnotation: Types.Annotation = {
annotationUID: TrackingUniqueIdentifier,
highlighted: false,
isLocked: false,
invalidated: false,
metadata: {
toolName,
valueType,
graphicType,
FrameOfReferenceUID: frameOfReferenceUID,
referencedImageId: imageId,
},
data: {
label: measurement.labels?.[0]?.value || undefined,
displayText: measurement.displayText || undefined,
handles: {
textBox: measurement.textBox ?? {},
points: graphicTypePoints[0],
},
cachedStats: {},
frameNumber,
renderableData,
TrackingUniqueIdentifier,
labels: measurement.labels,
},
};
/**
* const annotationManager = annotation.annotationState.getAnnotationManager();
* was not triggering annotation_added events.
*/
annotation.state.addAnnotation(SRAnnotation);
console.debug('Adding SR annotation:', SRAnnotation);
}
@@ -1,14 +1,8 @@
import { addTool } from '@cornerstonejs/tools';
export default function addToolInstance(name: string, toolClass, configuration = {}): void {
export default function addToolInstance(name: string, toolClass, configuration?): void {
class InstanceClass extends toolClass {
static toolName = name;
constructor(toolProps, defaultToolProps) {
toolProps.configuration = toolProps.configuration
? { ...toolProps.configuration, ...configuration }
: configuration;
super(toolProps, defaultToolProps);
}
}
addTool(InstanceClass);
}
@@ -61,11 +61,6 @@ const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
const imageSet = new ImageSet(instances);
const instance = instances[0];
if (!instance) {
return;
}
imageSet.setAttributes({
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
SeriesDate: instance.SeriesDate,
@@ -1,67 +0,0 @@
import { utils } from '@ohif/core';
/**
* Formatters used to format each of the content items (SR "nodes") which can be
* text, code, UID ref, number, person name, date, time and date time. Each
* formatter must be a function with the following signature:
*
* [VALUE_TYPE]: (contentItem) => string
*
*/
const contentItemFormatters = {
TEXT: contentItem => contentItem.TextValue,
CODE: contentItem => contentItem.ConceptCodeSequence?.[0]?.CodeMeaning,
UIDREF: contentItem => contentItem.UID,
NUM: contentItem => {
const measuredValue = contentItem.MeasuredValueSequence?.[0];
if (!measuredValue) {
return;
}
const { NumericValue, MeasurementUnitsCodeSequence } = measuredValue;
const { CodeValue } = MeasurementUnitsCodeSequence;
return `${NumericValue} ${CodeValue}`;
},
PNAME: contentItem => {
const personName = contentItem.PersonName?.[0]?.Alphabetic;
return personName ? utils.formatPN(personName) : undefined;
},
DATE: contentItem => {
const { Date } = contentItem;
return Date ? utils.formatDate(Date) : undefined;
},
TIME: contentItem => {
const { Time } = contentItem;
return Time ? utils.formatTime(Time) : undefined;
},
DATETIME: contentItem => {
const { DateTime } = contentItem;
if (typeof DateTime !== 'string') {
return;
}
// 14 characters because it should be something like 20180614113714
if (DateTime.length < 14) {
return DateTime;
}
const dicomDate = DateTime.substring(0, 8);
const dicomTime = DateTime.substring(8, 14);
const formattedDate = utils.formatDate(dicomDate);
const formattedTime = utils.formatTime(dicomTime);
return `${formattedDate} ${formattedTime}`;
},
};
function formatContentItemValue(contentItem) {
const { ValueType } = contentItem;
const fnFormat = contentItemFormatters[ValueType];
return fnFormat ? fnFormat(contentItem) : `[${ValueType} is not supported]`;
}
export { formatContentItemValue as default, formatContentItemValue };
@@ -1,7 +1,3 @@
import { adaptersSR } from '@cornerstonejs/adapters';
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
/**
* Extracts the label from the toolData imported from dcmjs. We need to do this
* as dcmjs does not depeend on OHIF/the measurementService, it just produces data for cornestoneTools.
@@ -13,15 +9,13 @@ const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
export default function getLabelFromDCMJSImportedToolData(toolData) {
const { findingSites = [], finding } = toolData;
let freeTextLabel = findingSites.find(
fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
);
let freeTextLabel = findingSites.find(fs => fs.CodeValue === 'CORNERSTONEFREETEXT');
if (freeTextLabel) {
return freeTextLabel.CodeMeaning;
}
if (finding && finding.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT) {
if (finding && finding.CodeValue === 'CORNERSTONEFREETEXT') {
return finding.CodeMeaning;
}
}
@@ -3,12 +3,14 @@ import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adaptersSR } from '@cornerstonejs/adapters';
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
const { locking } = CsAnnotation;
const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const convertCode = (codingValues, code) => {
@@ -41,23 +43,15 @@ const convertSites = (codingValues, sites) => {
*
*/
export default function hydrateStructuredReport(
{ servicesManager, extensionManager, appConfig }: withAppTypes,
{ servicesManager, extensionManager, appConfig },
displaySetInstanceUID
) {
const annotationManager = CsAnnotation.state.getAnnotationManager();
const disableEditing = appConfig?.disableEditing;
const dataSource = extensionManager.getActiveDataSource()[0];
const { measurementService, displaySetService, customizationService } = servicesManager.services;
const codingValues = customizationService.getCustomization('codingValues', {});
const { disableEditing } = customizationService.getCustomization(
'PanelMeasurement.disableEditing',
{
id: 'default.disableEditing',
disableEditing: false,
}
);
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
// TODO -> We should define a strict versioning somewhere.
@@ -4,7 +4,7 @@ const cornerstoneAdapters =
adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const CORNERSTONE_3D_TAG = adaptersSR.Cornerstone3D.CORNERSTONE_3D_TAG;
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
/**
* Checks if the given `displaySet`can be rehydrated into the `measurementService`.
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ExtensionManager } from '@ohif/core';
import { ServicesManager, ExtensionManager } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
@@ -14,16 +14,9 @@ const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const {
commandsManager,
children,
dataSource,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
} = props;
function OHIFCornerstoneSRViewport(props) {
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
const [appConfig] = useAppConfig();
@@ -70,7 +63,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
trackedMeasurements = tracked?.[0];
sendTrackedMeasurementsEvent = tracked?.[1];
}
if (!sendTrackedMeasurementsEvent) {
// if no panels from measurement-tracking extension is used, this code will run
trackedMeasurements = null;
@@ -80,7 +72,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
{ servicesManager, extensionManager, appConfig },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([
@@ -93,11 +84,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
};
}
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
/**
* Store the tracking identifiers per viewport in order to be able to
* show the SR measurements on the referenced image on the correct viewport,
@@ -149,10 +135,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
newMeasurementSelected,
displaySetService
).then(({ referencedDisplaySet, referencedDisplaySetMetadata }) => {
if (!referencedDisplaySet || !referencedDisplaySetMetadata) {
return;
}
setMeasurementSelected(newMeasurementSelected);
setActiveImageDisplaySetData(referencedDisplaySet);
setReferencedDisplaySetMetadata(referencedDisplaySetMetadata);
@@ -169,10 +151,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
// new measurement
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!csViewport) {
return;
}
const imageIds = csViewport.getImageIds();
const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId);
@@ -292,31 +270,30 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
updateViewport(measurementSelected);
};
loadSR();
}, [srDisplaySet]);
}, [dataSource, srDisplaySet]);
/**
* Hook to update the tracking identifiers when the selected measurement changes or
* the element changes
*/
useEffect(() => {
const updateSR = async () => {
if (!srDisplaySet.isLoaded) {
await srDisplaySet.load();
}
if (!element || !srDisplaySet.isLoaded) {
return;
}
setTrackingIdentifiers(measurementSelected);
};
updateSR();
if (!element || !srDisplaySet.isLoaded) {
return;
}
setTrackingIdentifiers(measurementSelected);
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
useEffect(() => {
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
}, [trackedMeasurements]);
useEffect(() => {
viewportActionCornersService.addComponents([
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -384,17 +361,22 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
);
}
OHIFCornerstoneSRMeasurementViewport.propTypes = {
OHIFCornerstoneSRViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
viewportLabel: PropTypes.string,
customProps: PropTypes.object,
viewportOptions: PropTypes.object,
servicesManager: PropTypes.object.isRequired,
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
OHIFCornerstoneSRViewport.defaultProps = {
customProps: {},
};
async function _getViewportReferencedDisplaySetData(
displaySet,
measurementSelected,
@@ -412,10 +394,6 @@ async function _getViewportReferencedDisplaySetData(
displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(displaySetService, displaySet);
}
if (!displaySetInstanceUID) {
return { referencedDisplaySetMetadata: null, referencedDisplaySet: null };
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const image0 = referencedDisplaySet.images[0];
@@ -529,4 +507,4 @@ function _getStatusComponent({
);
}
export default OHIFCornerstoneSRMeasurementViewport;
export default OHIFCornerstoneSRViewport;
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.10.0-beta.6",
"version": "3.8.0",
"description": "OHIF extension for 4D volumes data",
"author": "OHIF",
"license": "MIT",
@@ -29,21 +29,22 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.6",
"@ohif/extension-cornerstone": "3.10.0-beta.6",
"@ohif/extension-default": "3.10.0-beta.6",
"@ohif/i18n": "3.10.0-beta.6",
"@ohif/ui": "3.10.0-beta.6",
"dcmjs": "*",
"@ohif/core": "3.8.0",
"@ohif/extension-cornerstone": "3.8.0",
"@ohif/extension-default": "3.8.0",
"@ohif/i18n": "3.8.0",
"@ohif/ui": "3.8.0",
"dcmjs": "^0.29.5",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^18.3.1"
"react": "^17.0.2"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^2.2.3",
"@cornerstonejs/tools": "^2.2.3",
"@cornerstonejs/core": "^1.70.14",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.14",
"@cornerstonejs/tools": "^1.70.14",
"classnames": "^2.3.2"
}
}
@@ -110,12 +110,7 @@ function _getTimePointsData(volume) {
return { timePoints, timePointsUnit };
}
function _getSegmentationData(
segmentation,
volumesTimePointsCache,
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
) {
const { displaySetService, segmentationService, viewportGridService } = servicesManager.services;
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
const displaySets = displaySetService.getActiveDisplaySets();
const dynamic4DDisplaySet = displaySets.find(displaySet => {
@@ -150,11 +145,8 @@ function _getSegmentationData(
referencedDynamicVolume.imageIds[0]
);
const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
const maskVolumeId = segmentationVolume?.volumeId;
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
maskVolumeId,
maskVolumeId: segmentation.id,
}) as number[][];
const pixelCount = timeData.length;
@@ -163,15 +155,27 @@ function _getSegmentationData(
return [];
}
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
// const color = csTools.segmentation.config.color.getSegmentIndexColor(
// segmentationRepresentationUID,
// 1 // segmentIndex
// );
const viewportId = viewportGridService.getActiveViewportId();
const color = segmentationService.getSegmentColor(viewportId, segmentation.segmentationId, 1);
// since we only use one segmentation representation per segmentationId
// it is fine to pick the first one
const segmentationRepresentations = csTools.segmentation.state.getSegmentationIdRepresentations(
segmentation.id
);
const hexColor = cs.utilities.color.rgbToHex(color[0], color[1], color[2]);
const segmentationRepresentationUID =
segmentationRepresentations[0].segmentationRepresentationUID;
const toolGroupId = csTools.segmentation.state.getToolGroupIdFromSegmentationRepresentationUID(
segmentationRepresentationUID
);
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
const color = csTools.segmentation.config.color.getColorForSegmentIndex(
toolGroupId,
segmentationRepresentationUID,
1 // segmentIndex
);
const hexColor = cs.utilities.color.rgbToHex(...color);
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
if (!timePointsData) {
@@ -215,14 +219,14 @@ function _getSegmentationData(
};
}
function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
function _getInstanceFromSegmentations(segmentations, displaySetService) {
if (!segmentations.length) {
return;
}
const volumesTimePointsCache = new WeakMap();
const segmentationsData = segmentations.map(segmentation =>
_getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager })
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
);
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
@@ -261,12 +265,11 @@ function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
return { seriesMetadata, instance };
}
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
debugger;
const { segmentationService } = servicesManager.services;
function updateSegmentationsChartDisplaySet({ servicesManager }): void {
const { segmentationService, displaySetService } = servicesManager.services;
const segmentations = segmentationService.getSegmentations();
const { seriesMetadata, instance } =
_getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {};
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
if (seriesMetadata && instance) {
// An event is triggered after adding the instance and the displaySet is created
@@ -4,7 +4,7 @@ import { cache } from '@cornerstonejs/core';
const LABELMAP = Enums.SegmentationRepresentations.Labelmap;
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
const commandsModule = ({ commandsManager, servicesManager }) => {
const services = servicesManager.services;
const { displaySetService, viewportGridService, segmentationService } = services;
@@ -84,11 +84,11 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
const stdDeviation = Math.sqrt(variance);
return stdDeviation;
}
// Iterate through each segmentation to get the timeData and ijkCoords
segmentations.forEach(segmentation => {
const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
segmentations.forEach((segmentation, segmentationIndex) => {
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
maskVolumeId: volume.volumeId,
maskVolumeId: segmentation.id,
}) as number[][];
if (summaryStats) {
@@ -113,33 +113,17 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
// for each voxel in the ROI, get the value at the current time point
const voxelValues = [];
let sum = 0;
let minValue = Infinity;
let maxValue = -Infinity;
let minIndex = 0;
let maxIndex = 0;
// Single pass through the data to collect all needed values
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
const value = timeData[voxelIndex][timeIndex];
voxelValues.push(value);
sum += value;
if (value < minValue) {
minValue = value;
minIndex = voxelIndex;
}
if (value > maxValue) {
maxValue = value;
maxIndex = voxelIndex;
}
voxelValues.push(timeData[voxelIndex][timeIndex]);
}
mean.push(sum / numVoxels);
min.push(minValue);
minIJK.push(ijkCoords[minIndex]);
max.push(maxValue);
maxIJK.push(ijkCoords[maxIndex]);
mean.push(voxelValues.reduce((acc, value) => acc + value, 0) / numVoxels);
const minimum = Math.min(...voxelValues);
min.push(minimum);
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]);
const maximum = Math.max(...voxelValues);
max.push(maximum);
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
std.push(calculateStandardDeviation(voxelValues));
}
@@ -356,19 +340,30 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
throw new Error('No reference display set found based on the dynamic data');
}
const displaySet = displaySetService.getDisplaySetByUID(
referenceDisplaySet.displaySetInstanceUID
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
referenceDisplaySet.displaySetInstanceUID,
{ label }
);
const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
label,
});
// Add Segmentation to all toolGroupIds in the viewer
const toolGroupIds = Array.from(
viewports.values(),
viewport => viewport.viewportOptions.toolGroupId
);
const firstViewport = viewports.values().next().value;
const representationType = LABELMAP;
await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
segmentationId,
});
for (const toolGroupId of toolGroupIds) {
const hydrateSegmentation = true;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId,
hydrateSegmentation,
representationType
);
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
}
return segmentationId;
},
@@ -147,8 +147,8 @@ function getFusionViewports() {
name: DEFAULT_COLORMAP,
opacity: [
{ value: 0, opacity: 0 },
{ value: 0.1, opacity: 0.8 },
{ value: 1, opacity: 0.8 },
{ value: 0.1, opacity: 0.3 },
{ value: 1, opacity: 0.3 },
],
},
};
@@ -191,15 +191,6 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -249,15 +240,6 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -307,15 +289,6 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -1,10 +1,9 @@
import React from 'react';
import { DynamicDataPanel } from './panels';
import { Toolbox } from '@ohif/ui-next';
import { PanelSegmentation } from '@ohif/extension-cornerstone';
import { Toolbox } from '@ohif/ui';
import DynamicExport from './panels/DynamicExport';
function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) {
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
const wrappedDynamicDataPanel = () => {
return (
<DynamicDataPanel
@@ -15,7 +14,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager, co
);
};
const wrappedDynamicSegmentation = () => {
const wrappedDynamicToolbox = () => {
return (
<>
<Toolbox
@@ -25,17 +24,18 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager, co
buttonSectionId="dynamic-toolbox"
title="Threshold Tools"
/>
<PanelSegmentation
servicesManager={servicesManager}
</>
);
};
const wrappedDynamicExport = () => {
return (
<>
<DynamicExport
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={configuration}
>
<DynamicExport
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
</PanelSegmentation>
/>
</>
);
};
@@ -49,11 +49,18 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager, co
component: wrappedDynamicDataPanel,
},
{
name: 'dynamic-segmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedDynamicSegmentation,
name: 'dynamic-toolbox',
iconName: 'tab-4d',
iconLabel: '4D Workflow',
label: 'Dynamic Toolbox',
component: wrappedDynamicToolbox,
},
{
name: 'dynamic-export',
iconName: 'tab-4d',
iconLabel: '4D Workflow',
label: '4D Workflow',
component: wrappedDynamicExport,
},
];
}
@@ -1,19 +1,17 @@
import React from 'react';
import PanelGenerateImage from './PanelGenerateImage';
function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) {
function DynamicDataPanel({ servicesManager, commandsManager }) {
return (
<>
<div
className="flex flex-col text-white"
data-cy={'dynamic-volume-panel'}
>
<PanelGenerateImage
commandsManager={commandsManager}
servicesManager={servicesManager}
></PanelGenerateImage>
</div>
</>
<div
className="flex flex-col text-white"
data-cy={'dynamic-volume-panel'}
>
<PanelGenerateImage
commandsManager={commandsManager}
servicesManager={servicesManager}
></PanelGenerateImage>
</div>
);
}
@@ -1,52 +1,73 @@
import React from 'react';
import { Button, Icons } from '@ohif/ui-next';
import { useSegmentations } from '@ohif/extension-cornerstone';
import React, { useState, useEffect } from 'react';
import { ActionButtons } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
function DynamicExport({ commandsManager, servicesManager }: withAppTypes) {
const segmentations = useSegmentations({ servicesManager });
function DynamicExport({ commandsManager, servicesManager, extensionManager }) {
const { segmentationService } = servicesManager.services;
const { t } = useTranslation('dynamicExport');
if (!segmentations?.length) {
return null;
}
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
const actions = [
{
label: 'Export Time Data',
onClick: () => {
commandsManager.runCommand('exportTimeReportCSV', {
segmentations,
options: {
filename: 'TimeData.csv',
},
});
},
disabled: !segmentations?.length,
},
{
label: 'Export ROI Stats',
onClick: () => {
commandsManager.runCommand('exportTimeReportCSV', {
segmentations,
summaryStats: true,
options: {
filename: 'ROIStats.csv',
},
});
},
disabled: !segmentations?.length,
},
];
/**
* Update UI based on segmentation changes (added, removed, updated)
*/
useEffect(() => {
// ~~ Subscription
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
const subscriptions = [];
[added, updated, removed].forEach(evt => {
const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = segmentationService.getSegmentations();
setSegmentations(segmentations);
});
subscriptions.push(unsubscribe);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, []);
return (
<div className="flex gap-2">
<div className="flex h-8 w-full items-center rounded pr-0.5">
<Button
size="sm"
variant="ghost"
className="pl-1.5"
onClick={() => {
commandsManager.runCommand('exportTimeReportCSV', {
segmentations,
options: {
filename: 'TimeData.csv',
},
});
}}
>
<Icons.Export />
<span className="pl-1">Time Data</span>
</Button>
</div>
<div className="flex h-8 w-full items-center rounded pr-0.5">
<Button
size="sm"
variant="ghost"
className="pl-1.5"
onClick={() => {
commandsManager.runCommand('exportTimeReportCSV', {
segmentations,
summaryStats: true,
options: {
filename: 'ROIStats.csv',
},
});
}}
>
<Icons.Export />
<span className="pl-1">ROI Stats</span>
</Button>
<div>
<div className="mt-3 flex justify-center px-2">
<ActionButtons
actions={actions}
t={t}
/>
</div>
</div>
);
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import {
InputDoubleRange,
Button,
PanelSection,
ButtonGroup,
@@ -9,8 +10,6 @@ import {
Tooltip,
} from '@ohif/ui';
import { DoubleSlider } from '@ohif/ui-next';
import { Enums } from '@cornerstonejs/core';
const controlClassNames = {
@@ -56,15 +55,21 @@ const DynamicVolumeControls = ({
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]);
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
useEffect(() => {
setSliderRangeValues([framesLength / 4, framesLength / 2]);
}, [framesLength]);
const handleSliderChange = newValues => {
onDoubleRangeChange(newValues);
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
return;
}
setSliderRangeValues(newValues);
};
const formatLabel = value => Math.round(value);
return (
<div className="flex select-none flex-col">
<PanelSection
@@ -129,7 +134,7 @@ const DynamicVolumeControls = ({
}
/>
<ButtonGroup
className={`mt-2 w-full`}
className={`mt-2 w-full `}
separated={true}
>
<button
@@ -151,15 +156,15 @@ const DynamicVolumeControls = ({
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
</button>
</ButtonGroup>
<div className="mt-2 w-full">
<DoubleSlider
min={0}
max={framesLength - 1}
<div className="w-ful mt-2">
<InputDoubleRange
values={sliderRangeValues}
onChange={handleSliderChange}
minValue={0}
showLabel={true}
allowNumberEdit={true}
maxValue={framesLength}
step={1}
defaultValue={sliderRangeValues}
onValueChange={handleSliderChange}
formatLabel={formatLabel}
className="w-full"
/>
</div>
<Button
@@ -211,14 +216,14 @@ function FrameControls({
>
<Icon
name={getPlayPauseIconName()}
className="active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
className=" active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
/>
</IconButton>
<InputNumber
value={currentFrameIndex}
onChange={onFrameChange}
minValue={0}
maxValue={framesLength - 1}
maxValue={framesLength}
label="Frame"
{...controlClassNames}
/>
@@ -1,12 +1,14 @@
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { useCine, useViewportGrid } from '@ohif/ui';
import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
import { cache, utilities as csUtils, volumeLoader, eventTarget } from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/streaming-image-volume-loader';
import { utilities as cstUtils } from '@cornerstonejs/tools';
import DynamicVolumeControls from './DynamicVolumeControls';
const SOPClassHandlerId = '@ohif/extension-default.sopClassHandlerModule.stack';
export default function PanelGenerateImage({ servicesManager, commandsManager }: withAppTypes) {
export default function PanelGenerateImage({ servicesManager, commandsManager }) {
const { cornerstoneViewportService, viewportGridService, displaySetService } =
servicesManager.services;
@@ -14,8 +16,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
const [{ activeViewportId }] = useViewportGrid();
//
const [timePointsRange, setTimePointsRange] = useState([0, 0]);
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([0, 0]);
const [timePointsRange, setTimePointsRange] = useState([]);
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
const [dynamicVolume, setDynamicVolume] = useState(null);
const [frameRate, setFrameRate] = useState(20);
@@ -29,62 +31,47 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
useEffect(() => {
const viewportDataChangedEvt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const cineStateChangedEvt = servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED;
const evt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const viewportDataChangedCallback = evtDetails => {
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, evtDetails => {
evtDetails.viewportData.data.forEach(volumeData => {
if (volumeData.volume?.isDynamicVolume()) {
if (volumeData.volume.isDynamicVolume()) {
setDynamicVolume(volumeData.volume);
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
const newRange = [1, volumeData.volume.numTimePoints];
setTimePointsRange(newRange);
setTimePointsRangeToUseForGenerate(newRange);
setTimePointsRange([1, volumeData.volume.numTimePoints]);
}
});
};
const cineStateChangedCallback = evt => {
setIsPlaying(evt.isPlaying);
};
const { unsubscribe: unsubscribeViewportData } = cornerstoneViewportService.subscribe(
viewportDataChangedEvt,
viewportDataChangedCallback
);
const { unsubscribe: unsubscribeCineState } = servicesManager.services.cineService.subscribe(
cineStateChangedEvt,
cineStateChangedCallback
);
});
return () => {
unsubscribeViewportData();
unsubscribeCineState();
};
}, [cornerstoneViewportService, cineService, servicesManager.services.cineService]);
useEffect(() => {
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
const callback = evt => {
setTimePointRendered(evt.detail.timePointIndex);
};
eventTarget.addEventListener(evt, callback);
return () => {
eventTarget.removeEventListener(evt, callback);
unsubscribe();
};
}, [cornerstoneViewportService]);
useEffect(() => {
const { unsubscribe } = servicesManager.services.cineService.subscribe(
servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED,
evt => {
setIsPlaying(evt.isPlaying);
}
);
return () => {
unsubscribe();
};
}, [cineService]);
useEffect(() => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
if (!displaySetUIDs?.length) {
if (!displaySetUIDs || displaySetUIDs.length === 0) {
return;
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const displaySets = displaySetUIDs.map(displaySetUID =>
displaySetService.getDisplaySetByUID(displaySetUID)
);
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
if (!dynamicVolumeDisplaySet) {
@@ -101,16 +88,23 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
setDynamicVolume(dynamicVolume);
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
const newRange = [1, dynamicVolume.numTimePoints];
setTimePointsRange(newRange);
setTimePointsRangeToUseForGenerate(newRange);
}, [
activeViewportId,
viewportGridService,
displaySetService,
cornerstoneViewportService,
cineService,
]);
setTimePointsRange([1, dynamicVolume.numTimePoints]);
}, [activeViewportId, cornerstoneViewportService]);
useEffect(() => {
// ~~ Subscription
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
const callback = evt => {
setTimePointRendered(evt.detail.timePointIndex);
};
eventTarget.addEventListener(evt, callback);
return () => {
eventTarget.removeEventListener(evt, callback);
};
}, [cornerstoneViewportService]);
function renderGeneratedImage(displaySet) {
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
@@ -136,19 +130,26 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
let computedVolume = cache.getVolume(computedVolumeId);
if (!computedVolume) {
computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
volumeId: computedVolumeId,
});
await createComputedVolume(dynamicVolumeId, computedVolumeId);
computedVolume = cache.getVolume(computedVolumeId);
}
const [start, end] = timePointsRangeToUseForGenerate;
const frameNumbers = Array.from({ length: end - start + 1 }, (_, i) => i + start - 1);
const options = {
frameNumbers: operationName === 'SUBTRACT' ? [start, end - 1] : frameNumbers,
targetVolume: computedVolume,
};
const vals = timePointsRangeToUseForGenerate;
cstUtils.dynamicVolume.updateVolumeFromTimeData(dynamicVolume, operationName, options);
const targets = Array.from({ length: vals[1] - vals[0] + 1 }, (_, i) => i + vals[0]);
const dataInTime = cstUtils.dynamicVolume.generateImageFromTimeData(
dynamicVolume,
operationName,
operationName === 'SUBTRACT' ? vals : targets
);
// Add loadStatus.loaded to computed volume and set to true
computedVolume.loadStatus = {};
computedVolume.loadStatus.loaded = true;
// Set computed scalar data to volume
const scalarData = computedVolume.getScalarData();
scalarData.set(dataInTime);
// If computed display set does not exist, create an object to be used as
// the displaySet. If it does exist, update the image data and vtkTexture
@@ -165,7 +166,6 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
madeInClient: true,
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
isDerived: true,
imageIds: computedVolume.imageIds,
};
setComputedDisplaySet(displaySet);
renderGeneratedImage(displaySet);
@@ -173,8 +173,19 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
commandsManager.runCommand('updateVolumeData', {
volume: computedVolume,
});
// Check if viewport is currently displaying the computed volume, if so,
// call render on the viewports to update the image, if not, call
// renderGeneratedImage
// if (!cache.getVolume(dynamicVolumeId)) {
// for (const viewportId of viewports.keys()) {
// const viewportForRendering =
// cornerstoneViewportService.getCornerstoneViewport(viewportId);
// viewportForRendering.render();
// }
// } else {
cornerstoneViewportService.getRenderingEngine().render();
renderGeneratedImage(computedDisplaySet);
// }
}
}
@@ -237,7 +248,28 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
onGenerate={onGenerateImage}
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
onDoubleRangeChange={handleSliderChange}
initialRangeValues={timePointsRangeToUseForGenerate}
/>
);
}
async function createComputedVolume(dynamicVolumeId, computedVolumeId) {
if (!cache.getVolume(computedVolumeId)) {
const computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
volumeId: computedVolumeId,
});
return computedVolume;
}
}
PanelGenerateImage.propTypes = {
servicesManager: PropTypes.shape({
services: PropTypes.shape({
measurementService: PropTypes.shape({
getMeasurements: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
EVENTS: PropTypes.object.isRequired,
VALUE_TYPES: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
};
@@ -1,11 +1,15 @@
import React from 'react';
import type { ServicesManager } from '@ohif/core';
import React, { useMemo } from 'react';
function WorkflowPanel({ servicesManager }: { servicesManager: ServicesManager }) {
const ProgressDropdownWithService =
servicesManager.services.customizationService.getCustomization(
'progressDropdownWithServiceComponent'
function WorkflowPanel({ servicesManager, extensionManager }) {
const ProgressDropdownWithService = useMemo(() => {
const defaultComponents = extensionManager.getModuleEntry(
'@ohif/extension-default.customizationModule.default'
).value;
return defaultComponents.find(
component => component.id === 'progressDropdownWithServiceComponent'
).component;
}, [extensionManager]);
return (
<div
File diff suppressed because it is too large. Load diff
+1
View File
@@ -3,6 +3,7 @@ const pkg = require('./package');
module.exports = {
...base,
name: pkg.name,
displayName: pkg.name,
// rootDir: "../.."
// testMatch: [
+16 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.10.0-beta.6",
"version": "3.8.0",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -36,32 +36,33 @@
"peerDependencies": {
"@cornerstonejs/codec-charls": "^1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^2.2.3",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.70.14",
"@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.10.0-beta.6",
"@ohif/ui": "3.10.0-beta.6",
"dcmjs": "*",
"@ohif/core": "3.8.0",
"@ohif/ui": "3.8.0",
"dcmjs": "^0.29.12",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-resize-detector": "^10.0.1"
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-resize-detector": "^6.7.6"
},
"devDependencies": {
"lodash": "^4.17.21"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^2.2.3",
"@cornerstonejs/core": "^2.2.3",
"@cornerstonejs/tools": "^2.2.3",
"@cornerstonejs/adapters": "^1.70.14",
"@cornerstonejs/core": "^1.70.14",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.14",
"@cornerstonejs/tools": "^1.70.14",
"@icr/polyseg-wasm": "^0.4.0",
"@kitware/vtk.js": "32.1.0",
"@kitware/vtk.js": "30.4.1",
"html2canvas": "^1.4.1",
"lodash.debounce": "^4.0.8",
"lodash.debounce": "4.0.8",
"lodash.merge": "^4.6.2",
"shader-loader": "^1.3.1",
"worker-loader": "^3.0.8"
File diff suppressed because it is too large. Load diff
@@ -5,7 +5,7 @@ import CustomizableViewportOverlay from './CustomizableViewportOverlay';
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
function CornerstoneOverlays(props: withAppTypes) {
function CornerstoneOverlays(props) {
const { viewportId, element, scrollbarHeight, servicesManager } = props;
const { cornerstoneViewportService } = servicesManager.services;
const [imageSliceData, setImageSliceData] = useState({
@@ -2,9 +2,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
import { ViewportOverlay } from '@ohif/ui';
import type { InstanceMetadata } from '@ohif/core/src/types';
import { ServicesManager } from '@ohif/core';
import { InstanceMetadata } from '@ohif/core/src/types';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
@@ -18,7 +19,7 @@ interface OverlayItemProps {
element: HTMLElement;
viewportData: ViewportData;
imageSliceData: ImageSliceData;
servicesManager: AppTypes.ServicesManager;
servicesManager: ServicesManager;
viewportId: string;
instance: InstanceMetadata;
customization: any;
@@ -39,86 +40,11 @@ interface OverlayItemProps {
}
const OverlayItemComponents = {
'ohif.overlayItem': OverlayItem,
'ohif.overlayItem.windowLevel': VOIOverlayItem,
'ohif.overlayItem.zoomLevel': ZoomOverlayItem,
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
};
const studyDateItem = {
id: 'StudyDate',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Study date',
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
formatDate(referenceInstance.StudyDate),
};
const seriesDescriptionItem = {
id: 'SeriesDescription',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Series description',
condition: ({ referenceInstance }) => {
return referenceInstance && referenceInstance.SeriesDescription;
},
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
};
const topLeftItems = {
id: 'cornerstoneOverlayTopLeft',
items: [studyDateItem, seriesDescriptionItem],
};
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
const bottomLeftItems = {
id: 'cornerstoneOverlayBottomLeft',
items: [
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: props => {
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
return activeToolName === 'Zoom';
},
},
],
};
const bottomRightItems = {
id: 'cornerstoneOverlayBottomRight',
items: [
{
id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
],
};
/**
* The @ohif/cornerstoneOverlay is a default value for a customization
* for the cornerstone overlays. The intent is to allow it to be extended
* without needing to re-write the individual overlays by using the append
* mechanism. Individual attributes can be modified individually without
* affecting the other items by using the append as well, with position
* based replacement.
* This is used as a default in the getCustomizationModule so that it
* is available early for additional customization extensions.
*/
const CornerstoneOverlay = {
id: '@ohif/cornerstoneOverlay',
topLeftItems,
topRightItems,
bottomLeftItems,
bottomRightItems,
};
/**
* Customizable Viewport Overlay
*/
@@ -133,34 +59,34 @@ function CustomizableViewportOverlay({
viewportData: ViewportData;
imageSliceData: ImageSliceData;
viewportId: string;
servicesManager: AppTypes.ServicesManager;
servicesManager: ServicesManager;
}) {
const { cornerstoneViewportService, customizationService, toolGroupService, displaySetService } =
const { cornerstoneViewportService, customizationService, toolGroupService } =
servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const { imageIndex } = imageSliceData;
// The new customization is 'cornerstoneOverlay', with an append or replace
// on the individual items rather than defining individual items.
const cornerstoneOverlay = customizationService.getCustomization('@ohif/cornerstoneOverlay');
const topLeftCustomization = customizationService.getModeCustomization(
'cornerstoneOverlayTopLeft'
);
const topRightCustomization = customizationService.getModeCustomization(
'cornerstoneOverlayTopRight'
);
const bottomLeftCustomization = customizationService.getModeCustomization(
'cornerstoneOverlayBottomLeft'
);
const bottomRightCustomization = customizationService.getModeCustomization(
'cornerstoneOverlayBottomRight'
);
// Historical usage defined the overlays as separate items due to lack of
// append functionality. This code enables the historical usage, but
// the recommended functionality is to append to the default values in
// cornerstoneOverlay rather than defining individual items.
const topLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
cornerstoneOverlay?.topLeftItems;
const topRightCustomization =
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
cornerstoneOverlay?.topRightItems;
const bottomLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
cornerstoneOverlay?.bottomLeftItems;
const bottomRightCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
cornerstoneOverlay?.bottomRightItems;
const instances = useMemo(() => {
if (viewportData != null) {
return _getViewportInstances(viewportData);
} else {
return null;
}
}, [viewportData, imageIndex]);
const instanceNumber = useMemo(
() =>
@@ -170,22 +96,6 @@ function CustomizableViewportOverlay({
[viewportData, viewportId, imageIndex, cornerstoneViewportService]
);
const displaySetProps = useMemo(() => {
const displaySets = getDisplaySets(viewportData, displaySetService);
if (!displaySets) {
return null;
}
const [displaySet] = displaySets;
const { instances, instance: referenceInstance } = displaySet;
return {
displaySets,
displaySet,
instance: instances?.[imageIndex],
instances,
referenceInstance,
};
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
/**
* Updating the VOI when the viewport changes its voi
*/
@@ -227,8 +137,20 @@ function CustomizableViewportOverlay({
return;
}
const scale = viewport.getZoom();
const imageData = viewport.getImageData();
if (!imageData) {
return;
}
if (camera.scale) {
setScale(camera.scale);
return;
}
const { spacing } = imageData;
// convert parallel scale to scale
const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
setScale(scale);
}
};
@@ -241,9 +163,8 @@ function CustomizableViewportOverlay({
}, [viewportId, viewportData, cornerstoneViewportService, element]);
const _renderOverlayItem = useCallback(
(item, props) => {
item => {
const overlayItemProps = {
...props,
element,
viewportData,
imageSliceData,
@@ -256,6 +177,10 @@ function CustomizableViewportOverlay({
formatTime: formatDICOMTime,
formatNumberPrecision,
},
instance: instances ? instances[item?.instanceIndex] : null,
voi,
scale,
instanceNumber,
};
if (!item) {
@@ -270,8 +195,8 @@ function CustomizableViewportOverlay({
} else {
const renderItem = customizationService.transform(item);
if (typeof renderItem.contentF === 'function') {
return renderItem.contentF(overlayItemProps);
if (typeof renderItem.content === 'function') {
return renderItem.content(overlayItemProps);
}
}
},
@@ -282,7 +207,7 @@ function CustomizableViewportOverlay({
viewportId,
servicesManager,
customizationService,
displaySetProps,
instances,
voi,
scale,
instanceNumber,
@@ -290,27 +215,20 @@ function CustomizableViewportOverlay({
);
const getContent = useCallback(
(customization, keyPrefix) => {
if (!customization?.items) {
return null;
}
const { items } = customization;
const props = {
...displaySetProps,
formatters: { formatDate: formatDICOMDate },
voi,
scale,
instanceNumber,
viewportId,
toolGroupService,
};
(customization, defaultItems, keyPrefix) => {
const items = customization?.items ?? defaultItems;
return (
<>
{items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}>
{((!item?.condition || item.condition(props)) && _renderOverlayItem(item, props)) ||
null}
{item?.condition
? item.condition({
instance: instances ? instances[item?.instanceIndex] : null,
formatters: { formatDate: formatDICOMDate },
})
? _renderOverlayItem(item)
: null
: _renderOverlayItem(item)}
</div>
))}
</>
@@ -319,31 +237,104 @@ function CustomizableViewportOverlay({
[_renderOverlayItem]
);
const studyDateItem = {
id: 'StudyDate',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Study date',
condition: ({ instance }) => instance && instance.StudyDate,
contentF: ({ instance, formatters: { formatDate } }) => formatDate(instance.StudyDate),
};
const seriesDescriptionItem = {
id: 'SeriesDescription',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Series description',
attribute: 'SeriesDescription',
condition: ({ instance }) => {
return instance && instance.SeriesDescription;
},
};
const topLeftItems = instances
? instances
.map((instance, index) => {
return [
{
...studyDateItem,
instanceIndex: index,
},
{
...seriesDescriptionItem,
instanceIndex: index,
},
];
})
.flat()
: [];
return (
<ViewportOverlay
topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
topRight={getContent(topRightCustomization, 'topRightOverlayItem')}
bottomLeft={getContent(bottomLeftCustomization, 'bottomLeftOverlayItem')}
bottomRight={getContent(bottomRightCustomization, 'bottomRightOverlayItem')}
topLeft={
/**
* Inline default overlay items for a more standard expansion
*/
getContent(topLeftCustomization, [...topLeftItems], 'topLeftOverlayItem')
}
topRight={getContent(topRightCustomization, [], 'topRightOverlayItem')}
bottomLeft={getContent(
bottomLeftCustomization,
[
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: () => {
const activeToolName = toolGroupService.getActiveToolForViewport(viewportId);
return activeToolName === 'Zoom';
},
},
],
'bottomLeftOverlayItem'
)}
bottomRight={getContent(
bottomRightCustomization,
[
{
id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
],
'bottomRightOverlayItem'
)}
/>
);
}
/**
* Gets an array of display sets for the given viewport, based on the viewport data.
* Returns null if none found.
*/
function getDisplaySets(viewportData, displaySetService) {
if (!viewportData?.data?.length) {
return null;
function _getViewportInstances(viewportData) {
const imageIds = [];
if (viewportData.viewportType === Enums.ViewportType.STACK) {
imageIds.push(viewportData.data.imageIds[0]);
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
const volumes = viewportData.data;
volumes.forEach(volume => {
if (!volume?.imageIds || volume.imageIds.length === 0) {
return;
}
imageIds.push(volume.imageIds[0]);
});
}
const displaySets = viewportData.data
.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID))
.filter(it => !!it);
if (!displaySets.length) {
return null;
}
return displaySets;
const instances = [];
imageIds.forEach(imageId => {
const instance = metaData.get('instance', imageId) || {};
instances.push(instance);
});
return instances;
}
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
@@ -367,7 +358,7 @@ const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneView
};
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.data[0].imageIds;
const imageIds = viewportData.data.imageIds;
const imageId = imageIds[imageIndex];
if (!imageId) {
@@ -403,11 +394,6 @@ function _getInstanceNumberFromVolume(
// Todo: support fusion of acquisition plane which has instanceNumber
const { volume } = volumes[0];
if (!volume) {
return;
}
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
@@ -438,25 +424,6 @@ function _getInstanceNumberFromVolume(
}
}
function OverlayItem(props) {
const { instance, customization = {} } = props;
const { color, attribute, title, label, background } = customization;
const value = customization.contentF?.(props, customization) ?? instance?.[attribute];
if (value === undefined || value === null) {
return null;
}
return (
<div
className="overlay-item flex flex-row"
style={{ color, background }}
title={title}
>
{label ? <span className="mr-1 shrink-0">{label}</span> : null}
<span className="ml-1 mr-2 shrink-0">{value}</span>
</div>
);
}
/**
* Window Level / Center Overlay item
*/
@@ -469,7 +436,7 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
return (
<div
className="overlay-item flex flex-row"
style={{ color: customization?.color }}
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 shrink-0">{windowWidth.toFixed(0)}</span>
@@ -530,5 +497,3 @@ CustomizableViewportOverlay.propTypes = {
};
export default CustomizableViewportOverlay;
export { CustomizableViewportOverlay, CornerstoneOverlay };
Loaded 100 of 2023 files, more files were not shown because too many files have changed in this diff. Show more