feat(v3p8): advanced 3D layouts and 4d data support (#4071)

This commit is contained in:
Alireza 2024-04-29 12:36:12 -04:00 committed by GitHub
commit 1dcdc9dabe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
860 changed files with 56278 additions and 13801 deletions

View File

@ -13,7 +13,7 @@ version: 2.1
##
orbs:
codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@3.1.4
cypress: cypress-io/cypress@3.3.1
executors:
cypress-custom:

View File

@ -1,25 +0,0 @@
# ABOUT:
# https://docs.codecov.io/docs/codecov-yaml
#
#
# COMMIT STATUS:
# https://docs.codecov.io/docs/commit-status
coverage:
status:
project:
default:
threshold: 0.5%
core:
flags: core
threshold: 0.5%
viewer:
flags: viewer
threshold: 0.5%
patch: off
flags:
core:
paths:
- platform/core
viewer:
paths:
- platform/app

View File

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

1
.gitignore vendored
View File

@ -24,6 +24,7 @@ yarn-error.log
.DS_Store
.env
*.code-workspace
.directory
# Common Example Data Directories
sampledata/

View File

@ -0,0 +1,265 @@
/*
* This script uses nodejs to generate a JSON file from a DICOM study folder.
* You need to have dcmjs installed in your project.
* The JSON file can be used to load the study into the OHIF Viewer. You can get more detail
* in the DICOM JSON Data source on docs.ohif.org
*
* Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>
*
* params:
* - 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
* - outputJSONPath: path to the output JSON file
*/
const dcmjs = require('dcmjs');
const path = require('path');
const fs = require('fs').promises;
const args = process.argv.slice(2);
const [studyDirectory, urlPrefix, outputPath] = args;
if (args.length !== 3) {
console.error('Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>');
process.exit(1);
}
const model = {
studies: [],
};
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath) {
try {
const files = await recursiveReadDir(studyDirectory);
console.debug('Processing...');
for (const file of files) {
if (!file.includes('.DS_Store') && !file.includes('.xml')) {
const arrayBuffer = await fs.readFile(file);
const dicomDict = dcmjs.data.DicomMessage.readFile(arrayBuffer.buffer);
const instance = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomDict.dict);
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory);
processInstance(instance);
}
}
console.log('Successfully loaded data');
model.studies.forEach(study => {
study.NumInstances = findInstancesNumber(study);
study.Modalities = findModalities(study).join('/');
});
await fs.writeFile(outputPath, JSON.stringify(model, null, 2));
console.log('JSON saved');
} catch (error) {
console.error(error);
}
}
async function recursiveReadDir(dir) {
let results = [];
const list = await fs.readdir(dir);
for (const file of list) {
const filePath = path.resolve(dir, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
const res = await recursiveReadDir(filePath);
results = results.concat(res);
} else {
results.push(filePath);
}
}
return results;
}
function createImageId(fileLocation, urlPrefix, studyDirectory) {
const relativePath = path.relative(studyDirectory, fileLocation);
const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/');
return `dicomweb:${urlPrefix}${normalizedPath}`;
}
function processInstance(instance) {
const { StudyInstanceUID, SeriesInstanceUID } = instance;
let study = getStudy(StudyInstanceUID);
if (!study) {
study = createStudyMetadata(StudyInstanceUID, instance);
model.studies.push(study);
}
let series = getSeries(StudyInstanceUID, SeriesInstanceUID);
if (!series) {
series = createSeriesMetadata(instance);
study.series.push(series);
}
const instanceMetaData =
instance.NumberOfFrames > 1
? createInstanceMetaDataMultiFrame(instance)
: createInstanceMetaData(instance);
series.instances.push(...[].concat(instanceMetaData));
}
function getStudy(StudyInstanceUID) {
return model.studies.find(study => study.StudyInstanceUID === StudyInstanceUID);
}
function getSeries(StudyInstanceUID, SeriesInstanceUID) {
const study = getStudy(StudyInstanceUID);
return study
? study.series.find(series => series.SeriesInstanceUID === SeriesInstanceUID)
: undefined;
}
const findInstancesNumber = study => {
let numInstances = 0;
study.series.forEach(aSeries => {
numInstances = numInstances + aSeries.instances.length;
});
return numInstances;
};
const findModalities = study => {
let modalities = new Set();
study.series.forEach(aSeries => {
modalities.add(aSeries.Modality);
});
return Array.from(modalities);
};
function createStudyMetadata(StudyInstanceUID, instance) {
return {
StudyInstanceUID,
StudyDescription: instance.StudyDescription,
StudyDate: instance.StudyDate,
StudyTime: instance.StudyTime,
PatientName: instance.PatientName,
PatientID: instance.PatientID || '1234', // this is critical to have
AccessionNumber: instance.AccessionNumber,
PatientAge: instance.PatientAge,
PatientSex: instance.PatientSex,
PatientWeight: instance.PatientWeight,
series: [],
};
}
function createSeriesMetadata(instance) {
return {
SeriesInstanceUID: instance.SeriesInstanceUID,
SeriesDescription: instance.SeriesDescription,
SeriesNumber: instance.SeriesNumber,
SeriesTime: instance.SeriesTime,
Modality: instance.Modality,
SliceThickness: instance.SliceThickness,
instances: [],
};
}
function commonMetaData(instance) {
return {
Columns: instance.Columns,
Rows: instance.Rows,
InstanceNumber: instance.InstanceNumber,
SOPClassUID: instance.SOPClassUID,
AcquisitionNumber: instance.AcquisitionNumber,
PhotometricInterpretation: instance.PhotometricInterpretation,
BitsAllocated: instance.BitsAllocated,
BitsStored: instance.BitsStored,
PixelRepresentation: instance.PixelRepresentation,
SamplesPerPixel: instance.SamplesPerPixel,
PixelSpacing: instance.PixelSpacing,
HighBit: instance.HighBit,
ImageOrientationPatient: instance.ImageOrientationPatient,
ImagePositionPatient: instance.ImagePositionPatient,
FrameOfReferenceUID: instance.FrameOfReferenceUID,
ImageType: instance.ImageType,
Modality: instance.Modality,
SOPInstanceUID: instance.SOPInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
StudyInstanceUID: instance.StudyInstanceUID,
WindowCenter: instance.WindowCenter,
WindowWidth: instance.WindowWidth,
RescaleIntercept: instance.RescaleIntercept,
RescaleSlope: instance.RescaleSlope,
};
}
function conditionalMetaData(instance) {
return {
...(instance.ConceptNameCodeSequence && {
ConceptNameCodeSequence: instance.ConceptNameCodeSequence,
}),
...(instance.SeriesDate && { SeriesDate: instance.SeriesDate }),
...(instance.ReferencedSeriesSequence && {
ReferencedSeriesSequence: instance.ReferencedSeriesSequence,
}),
...(instance.SharedFunctionalGroupsSequence && {
SharedFunctionalGroupsSequence: instance.SharedFunctionalGroupsSequence,
}),
...(instance.PerFrameFunctionalGroupsSequence && {
PerFrameFunctionalGroupsSequence: instance.PerFrameFunctionalGroupsSequence,
}),
...(instance.ContentSequence && { ContentSequence: instance.ContentSequence }),
...(instance.ContentTemplateSequence && {
ContentTemplateSequence: instance.ContentTemplateSequence,
}),
...(instance.CurrentRequestedProcedureEvidenceSequence && {
CurrentRequestedProcedureEvidenceSequence: instance.CurrentRequestedProcedureEvidenceSequence,
}),
...(instance.CodingSchemeIdentificationSequence && {
CodingSchemeIdentificationSequence: instance.CodingSchemeIdentificationSequence,
}),
...(instance.RadiopharmaceuticalInformationSequence && {
RadiopharmaceuticalInformationSequence: instance.RadiopharmaceuticalInformationSequence,
}),
...(instance.ROIContourSequence && {
ROIContourSequence: instance.ROIContourSequence,
}),
...(instance.StructureSetROISequence && {
StructureSetROISequence: instance.StructureSetROISequence,
}),
...(instance.ReferencedFrameOfReferenceSequence && {
ReferencedFrameOfReferenceSequence: instance.ReferencedFrameOfReferenceSequence,
}),
...(instance.CorrectedImage && { CorrectedImage: instance.CorrectedImage }),
...(instance.Units && { Units: instance.Units }),
...(instance.DecayCorrection && { DecayCorrection: instance.DecayCorrection }),
...(instance.AcquisitionDate && { AcquisitionDate: instance.AcquisitionDate }),
...(instance.AcquisitionTime && { AcquisitionTime: instance.AcquisitionTime }),
...(instance.PatientWeight && { PatientWeight: instance.PatientWeight }),
...(instance.NumberOfFrames && { NumberOfFrames: instance.NumberOfFrames }),
...(instance.FrameTime && { FrameTime: instance.FrameTime }),
...(instance.EncapsulatedDocument && { EncapsulatedDocument: instance.EncapsulatedDocument }),
...(instance.SequenceOfUltrasoundRegions && {
SequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions,
}),
};
}
function createInstanceMetaData(instance) {
const metadata = {
...commonMetaData(instance),
...conditionalMetaData(instance),
};
return { metadata, url: instance.fileLocation };
}
function createInstanceMetaDataMultiFrame(instance) {
const instances = [];
const commonData = commonMetaData(instance);
const conditionalData = conditionalMetaData(instance);
for (let i = 1; i <= instance.NumberOfFrames; i++) {
const metadata = { ...commonData, ...conditionalData };
const result = { metadata, url: instance.fileLocation + `?frame=${i}` };
instances.push(result);
}
return instances;
}
convertDICOMToJSON(studyDirectory, urlPrefix, outputPath);

View File

@ -31,6 +31,6 @@
"prettier.endOfLine": "lf",
"workbench.colorCustomizations": {},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
}
}

View File

@ -120,6 +120,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
test: /\.wasm/,
type: 'asset/resource',
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'assets/images/[name].[ext]',
},
},
],
},
], //.concat(vtkRules),
},
resolve: {

File diff suppressed because it is too large Load Diff

View File

@ -41,12 +41,15 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
| | | |
| :-: | :--- | :--- |
| <img src="platform/docs/docs/assets/img/demo-measurements.jpg" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
| <img src="platform/docs/docs/assets/img/demo-segmentation.png" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
| <img src="platform/docs/docs/assets/img/demo-ptct.png" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
| <img src="platform/docs/docs/assets/img/demo-microscopy.png" alt="Microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.275741864483510678566144889372061815320) |
| <img src="platform/docs/docs/assets/img/demo-volumeRendering.png" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-measurements.webp?raw=true" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-segmentation.webp?raw=true" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-ptct.webp?raw=true" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-volume-rendering.webp?raw=true" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-pdf.webp?raw=true" alt="PDF" width="350"/> | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-rtstruct.webp?raw=true" alt="RTSTRUCT" width="350"/> | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4d.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) |
| <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

View File

@ -26,6 +26,7 @@ module.exports = {
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
'@babel/plugin-transform-class-static-block',
],
env: {
test: {
@ -45,6 +46,7 @@ module.exports = {
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/transform-destructuring',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
],

View File

@ -1 +1 @@
5ddf8a16027255d28dc01c1740099cf85bbcf458
f1a736d1934733a434cb87b2c284907a3122403f

View File

@ -3,7 +3,769 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.7.0) (2023-10-11)
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
### Bug Fixes
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
### Bug Fixes
* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637))
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02)
### Features
* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1))
# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21)
### Features
* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6))
# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22)
### Bug Fixes
* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db))
# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09)
### Bug Fixes
* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6))
# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt

View File

@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.7.0",
"version": "3.8.0-beta.93",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@ -24,6 +24,8 @@
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-seg": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
@ -31,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.7.0",
"@ohif/extension-cornerstone": "3.7.0",
"@ohif/extension-default": "3.7.0",
"@ohif/i18n": "3.7.0",
"@ohif/core": "3.8.0-beta.93",
"@ohif/extension-cornerstone": "3.8.0-beta.93",
"@ohif/extension-default": "3.8.0-beta.93",
"@ohif/i18n": "3.8.0-beta.93",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",

View File

@ -66,6 +66,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
onKeyPress: event => {
if (event.key === 'Enter') {
onSubmit(RESPONSE.HYDRATE_SEG);
}
},
});
});
}

View File

@ -1,13 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import OHIF, { utils } from '@ohif/core';
import { ViewportActionBar, useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
const { formatDate } = utils;
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
function OHIFCornerstoneRTViewport(props) {
@ -15,7 +13,6 @@ function OHIFCornerstoneRTViewport(props) {
children,
displaySets,
viewportOptions,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
@ -27,6 +24,7 @@ function OHIFCornerstoneRTViewport(props) {
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@ -118,7 +116,10 @@ function OHIFCornerstoneRTViewport(props) {
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
}}
onElementEnabled={onElementEnabled}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
onElementDisabled={onElementDisabled}
></Component>
);
@ -126,7 +127,6 @@ function OHIFCornerstoneRTViewport(props) {
const onSegmentChange = useCallback(
direction => {
direction = direction === 'left' ? -1 : 1;
const segmentationId = rtDisplaySet.displaySetInstanceUID;
const segmentation = segmentationService.getSegmentation(segmentationId);
@ -168,6 +168,8 @@ function OHIFCornerstoneRTViewport(props) {
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
useEffect(() => {
// I'm not sure what is this, since in RT we support Overlapping segments
// via contours
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
@ -280,20 +282,7 @@ function OHIFCornerstoneRTViewport(props) {
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
SeriesNumber,
} = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => {
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
@ -307,41 +296,47 @@ function OHIFCornerstoneRTViewport(props) {
});
setIsHydrated(isHydrated);
};
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onSegmentChange}
getStatusComponent={() => {
return _getStatusComponent({
isHydrated,
onStatusClick,
});
}}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: `RT Viewport ${SeriesDescription}`,
patientInformation: {
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
spacing:
SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{rtIsLoading && (
<LoadingIndicatorTotalPercent

View File

@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
switch (isHydrated) {
case true:
StatusIcon = () => <Icon name="status-alert" />;
@ -26,23 +23,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
}
const StatusArea = () => (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">RTSTRUCT</span>
</div>
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">RTSTRUCT</span>
</div>
)}
</div>
);
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
</div>
)}
</div>
);
};
return (
<>

View File

@ -3,7 +3,820 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.7.0) (2023-10-11)
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
### Bug Fixes
* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f))
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
### Bug Fixes
* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0))
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
### Bug Fixes
* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951))
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
### Features
* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb))
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
### Bug Fixes
* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af))
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
### Bug Fixes
* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02))
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
### Bug Fixes
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
### Bug Fixes
* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637))
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
### Features
* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13))
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
### Features
* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2))
# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05)
### Bug Fixes
* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e))
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03)
### Features
* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d))
# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02)
### Features
* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1))
# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28)
### Bug Fixes
* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43))
# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28)
### Features
* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6))
# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27)
### Features
* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5))
# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18)
### Bug Fixes
* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5))
# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22)
### Bug Fixes
* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30))
# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21)
### Features
* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6))
# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12)
### Bug Fixes
* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412))
# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09)
### Bug Fixes
* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6))
# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06)
### Bug Fixes
* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df))
# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24)
### Bug Fixes
* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682))
# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg

View File

@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.7.0",
"version": "3.8.0-beta.93",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@ -24,6 +24,8 @@
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-seg": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
@ -31,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.7.0",
"@ohif/extension-cornerstone": "3.7.0",
"@ohif/extension-default": "3.7.0",
"@ohif/i18n": "3.7.0",
"@ohif/core": "3.8.0-beta.93",
"@ohif/extension-cornerstone": "3.8.0-beta.93",
"@ohif/extension-default": "3.8.0-beta.93",
"@ohif/i18n": "3.8.0-beta.93",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@ -44,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.20.3",
"@cornerstonejs/tools": "^1.20.3",
"@kitware/vtk.js": "27.3.1",
"@cornerstonejs/adapters": "^1.70.14",
"@cornerstonejs/core": "^1.70.14",
"@kitware/vtk.js": "30.4.1",
"react-color": "^2.19.3"
}
}

View File

@ -5,6 +5,7 @@ import { cache, metaData } from '@cornerstonejs/core';
import {
segmentation as cornerstoneToolsSegmentation,
Enums as cornerstoneToolsEnums,
utilities,
} from '@cornerstonejs/tools';
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
import { classes, DicomMetadataStore } from '@ohif/core';
@ -18,6 +19,7 @@ import {
getUpdatedViewportsForSegmentation,
getTargetViewport,
} from './utils/hydrationUtils';
const { segmentation: segmentationUtils } = utilities;
const { datasetToBlob } = dcmjs.data;
@ -45,6 +47,7 @@ const commandsModule = ({
uiDialogService,
displaySetService,
viewportGridService,
toolGroupService,
} = (servicesManager as ServicesManager).services;
const actions = {
@ -203,6 +206,9 @@ const commandsModule = ({
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
// Todo: handle adding more than one segmentation
const displaySet = displaySets[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
updateViewportsForSegmentationRendering({
viewportId,
@ -218,7 +224,8 @@ const commandsModule = ({
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;
},
});
@ -397,6 +404,36 @@ const commandsModule = ({
console.warn(e);
}
},
setBrushSize: ({ value, toolNames }) => {
const brushSize = Number(value);
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
if (toolNames?.length === 0) {
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize);
} else {
toolNames?.forEach(toolName => {
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
});
}
});
},
setThresholdRange: ({
value,
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
}) => {
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
toolNames?.forEach(toolName => {
toolGroup.setToolConfiguration(toolName, {
strategySpecificConfiguration: {
THRESHOLD: {
threshold: value,
},
},
});
});
});
},
};
const definitions = {
@ -424,11 +461,18 @@ const commandsModule = ({
downloadRTSS: {
commandFn: actions.downloadRTSS,
},
setBrushSize: {
commandFn: actions.setBrushSize,
},
setThresholdRange: {
commandFn: actions.setThresholdRange,
},
};
return {
actions,
definitions,
defaultContext: 'SEGMENTATION',
};
};

View File

@ -1,17 +1,21 @@
import React from 'react';
import { useAppConfig } from '@state';
import { Toolbox } from '@ohif/ui';
import PanelSegmentation from './panels/PanelSegmentation';
import SegmentationToolbox from './panels/SegmentationToolbox';
const getPanelModule = ({ commandsManager, servicesManager, extensionManager, configuration }) => {
const getPanelModule = ({
commandsManager,
servicesManager,
extensionManager,
configuration,
title,
}) => {
const { customizationService } = servicesManager.services;
const wrappedPanelSegmentation = configuration => {
const [appConfig] = useAppConfig();
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
return (
<PanelSegmentation
commandsManager={commandsManager}
@ -19,7 +23,8 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing || disableEditingForMode?.value,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
/>
);
@ -27,12 +32,15 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
const wrappedPanelSegmentationWithTools = configuration => {
const [appConfig] = useAppConfig();
return (
<>
<SegmentationToolbox
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="segmentationToolbox"
title="Segmentation Tools"
configuration={{
...configuration,
}}
@ -43,6 +51,8 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
/>
</>

View File

@ -1,5 +1,6 @@
import { utils } from '@ohif/core';
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
@ -141,7 +142,7 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
'@ohif/extension-cornerstone.utilityModule.common'
);
const { segmentationService } = servicesManager.services;
const { segmentationService, uiNotificationService } = servicesManager.services;
const { dicomLoaderService } = utilityModule.exports;
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
@ -174,12 +175,40 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
);
let usedRecommendedDisplayCIELabValue = true;
results.segMetadata.data.forEach((data, i) => {
if (i > 0) {
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
data.rgba = data.RecommendedDisplayCIELabValue;
if (data.rgba) {
data.rgba = dicomlabToRGB(data.rgba);
} else {
usedRecommendedDisplayCIELabValue = false;
data.rgba = CONSTANTS.COLOR_LUT[i % CONSTANTS.COLOR_LUT.length];
}
}
});
if (results.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
message:
'Unsupported overlapping segments detected, segmentation rendering results may be incorrect.',
type: 'warning',
});
}
if (!usedRecommendedDisplayCIELabValue) {
// Display a notification about the non-utilization of RecommendedDisplayCIELabValue
uiNotificationService.show({
title: 'DICOM SEG import',
message:
'RecommendedDisplayCIELabValue not found for one or more segments. The default color was used instead.',
type: 'warning',
duration: 5000,
});
}
Object.assign(segDisplaySet, results);
}

View File

@ -0,0 +1,57 @@
export function getToolbarModule({ servicesManager }) {
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
return [
{
name: 'evaluate.cornerstone.segmentation',
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
// Todo: we need to pass in the button section Id since we are kind of
// forcing the button to have black background since initially
// it is designed for the toolbox not the toolbar on top
// we should then branch the buttonSectionId to have different styles
const segmentations = segmentationService.getSegmentations();
if (!segmentations?.length) {
return {
disabled: true,
className: '!text-common-bright !bg-black opacity-50',
disabledText: disabledText ?? 'No segmentations available',
};
}
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup.hasTool(toolName) && !toolNames) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isPrimaryActive = toolNames
? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool())
: toolGroup.getActivePrimaryMouseButtonTool() === toolName;
return {
disabled: false,
className: isPrimaryActive
? '!text-black !bg-primary-light hover:bg-primary-light hover-text-black hover:cursor-pointer'
: '!text-common-bright !bg-black hover:bg-primary-light hover:cursor-pointer hover:text-black',
// Todo: isActive right now is used for nested buttons where the primary
// button needs to be fully rounded (vs partial rounded) when active
// otherwise it does not have any other use
isActive: isPrimaryActive,
};
},
},
];
}

View File

@ -5,7 +5,7 @@ import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import getPanelModule from './getPanelModule';
import getCommandsModule from './commandsModule';
import preRegistration from './init';
import { getToolbarModule } from './getToolbarModule';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
@ -28,8 +28,6 @@ const extension = {
* You ID can be anything you want, but it should be unique.
*/
id,
preRegistration,
/**
* 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,
@ -38,8 +36,8 @@ const extension = {
*/
getPanelModule,
getCommandsModule,
getViewportModule({ servicesManager, extensionManager }) {
getToolbarModule,
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
const ExtendedOHIFCornerstoneSEGViewport = props => {
return (
<OHIFCornerstoneSEGViewport

View File

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

View File

@ -1,23 +1,35 @@
import { createReportAsync } from '@ohif/extension-default';
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTable, LegacyButtonGroup, LegacyButton } from '@ohif/ui';
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 } = servicesManager.services;
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()
);
@ -47,6 +59,64 @@ export default function PanelSegmentation({
};
}, []);
// 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);
@ -54,7 +124,9 @@ export default function PanelSegmentation({
};
const onSegmentationAdd = async () => {
commandsManager.runCommand('createEmptySegmentationForViewport');
commandsManager.runCommand('createEmptySegmentationForViewport', {
viewportId: viewportGridService.getActiveViewportId(),
});
};
const onSegmentationClick = (segmentationId: string) => {
@ -147,6 +219,7 @@ export default function PanelSegmentation({
segmentationService.removeSegment(segmentationId, segmentIndex);
};
// segment hide
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segmentInfo = segmentation.segments[segmentIndex];
@ -170,6 +243,22 @@ export default function PanelSegmentation({
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(
@ -221,59 +310,61 @@ export default function PanelSegmentation({
});
};
const SegmentationGroupTableComponent =
components[configuration?.segmentationPanelMode] || SegmentationGroupTable;
const allowAddSegment = configuration?.addSegment;
const onSegmentationAddWrapper =
configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function'
? configuration?.onSegmentationAdd
: onSegmentationAdd;
return (
<>
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
<SegmentationGroupTable
title={t('Segmentations')}
segmentations={segmentations}
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAdd}
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)
}
/>
</div>
</>
<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)
}
/>
);
}

View File

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

View File

@ -0,0 +1,4 @@
export enum SegmentationPanelMode {
Expanded = 'expanded',
Dropdown = 'dropdown',
}

View File

@ -138,7 +138,7 @@ function getUpdatedViewportsForSegmentation({
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
servicesManager.services;
const { viewports } = viewportGridService.getState();
const { viewports, isHangingProtocolLayout } = viewportGridService.getState();
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
@ -153,7 +153,8 @@ function getUpdatedViewportsForSegmentation({
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
targetViewportId,
referenceDisplaySetInstanceUID
referenceDisplaySetInstanceUID,
isHangingProtocolLayout
);
viewports.forEach((viewport, viewportId) => {
@ -180,7 +181,8 @@ function getUpdatedViewportsForSegmentation({
});
}
});
return updatedViewports;
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
}
export {

View File

@ -63,6 +63,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
onKeyPress: event => {
if (event.key === 'Enter') {
onSubmit(RESPONSE.HYDRATE_SEG);
}
},
});
});
}

View File

@ -1,13 +1,11 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils } from '@ohif/core';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent';
const { formatDate } = utils;
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
function OHIFCornerstoneSEGViewport(props) {
@ -15,7 +13,6 @@ function OHIFCornerstoneSEGViewport(props) {
children,
displaySets,
viewportOptions,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
@ -28,8 +25,8 @@ function OHIFCornerstoneSEGViewport(props) {
displaySetService,
toolGroupService,
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
@ -114,16 +111,17 @@ function OHIFCornerstoneSEGViewport(props) {
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
}}
onElementEnabled={onElementEnabled}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
onElementDisabled={onElementDisabled}
// initialImageIndex={initialImageIndex}
></Component>
);
}, [viewportId, segDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
direction = direction === 'left' ? -1 : 1;
const segmentationId = segDisplaySet.displaySetInstanceUID;
const segmentation = segmentationService.getSegmentation(segmentationId);
@ -172,14 +170,6 @@ function OHIFCornerstoneSEGViewport(props) {
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
setSegIsLoading(false);
}
if (evt.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
message: 'Overlapping segments detected which is not currently supported',
type: 'warning',
});
}
}
);
@ -256,6 +246,69 @@ function OHIFCornerstoneSEGViewport(props) {
};
}, [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
// presentation state, so that we can restore it after hydrating the SEG. This is
// required if the user has changed the viewport (other viewport than SEG viewport)
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@ -291,61 +344,8 @@ function OHIFCornerstoneSEGViewport(props) {
SpacingBetweenSlices,
} = referencedDisplaySetRef.current.metadata;
const hydrateSEGDisplaySet = ({ segDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [segDisplaySet],
viewportId,
});
};
const onStatusClick = async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the SEG. This is
// required if the user has changed the viewport (other viewport than SEG viewport)
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
};
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onSegmentChange}
getStatusComponent={() => {
return _getStatusComponent({
isHydrated,
onStatusClick,
});
}}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
seriesDescription: `SEG Viewport ${SeriesDescription}`,
patientInformation: {
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? utils.roundNumber(SliceThickness, 2) : '',
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
spacing:
SpacingBetweenSlices !== undefined ? utils.roundNumber(SpacingBetweenSlices, 2) : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{segIsLoading && (
<LoadingIndicatorTotalPercent

View File

@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
switch (isHydrated) {
case true:
StatusIcon = () => <Icon name="status-alert" />;
@ -26,23 +23,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
}
const StatusArea = () => (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SEG</span>
</div>
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SEG</span>
</div>
)}
</div>
);
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
</div>
)}
</div>
);
};
return (
<>

View File

@ -3,7 +3,820 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.7.0) (2023-10-11)
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
### Bug Fixes
* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f))
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
### Bug Fixes
* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0))
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
### Bug Fixes
* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951))
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
### Features
* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb))
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
### Bug Fixes
* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af))
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
### Bug Fixes
* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02))
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
### Bug Fixes
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
### Features
* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13))
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
### Features
* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2))
# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05)
### Bug Fixes
* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e))
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02)
### Features
* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1))
# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28)
### Bug Fixes
* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43))
# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27)
### Features
* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5))
# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18)
### Bug Fixes
* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5))
# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22)
### Bug Fixes
* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30))
# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21)
### Features
* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6))
# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12)
### Bug Fixes
* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412))
# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09)
### Bug Fixes
* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6))
# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13)
### Features
* **customizationService:** Enable saving and loading of private tags in SRs ([#3842](https://github.com/OHIF/Viewers/issues/3842)) ([e1f55e6](https://github.com/OHIF/Viewers/commit/e1f55e65f2d2a34136ad5d0b1ada77d337a0ea23))
# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06)
### Bug Fixes
* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df))
# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24)
### Bug Fixes
* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682))
# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10)
### Features
* **dicomJSON:** Add Loading Other Display Sets and JSON Metadata Generation script ([#3777](https://github.com/OHIF/Viewers/issues/3777)) ([43b1c17](https://github.com/OHIF/Viewers/commit/43b1c17209502e4876ad59bae09ed9442eda8024))
# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24)
### Bug Fixes
* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3))
# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr

View File

@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.7.0",
"version": "3.8.0-beta.93",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@ -23,6 +23,8 @@
"ohif-extension"
],
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
@ -32,11 +34,11 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.7.0",
"@ohif/extension-cornerstone": "3.7.0",
"@ohif/extension-measurement-tracking": "3.7.0",
"@ohif/ui": "3.7.0",
"dcmjs": "^0.29.5",
"@ohif/core": "3.8.0-beta.93",
"@ohif/extension-cornerstone": "3.8.0-beta.93",
"@ohif/extension-measurement-tracking": "3.8.0-beta.93",
"@ohif/ui": "3.8.0-beta.93",
"dcmjs": "^0.29.12",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@ -44,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.20.3",
"@cornerstonejs/core": "^1.20.3",
"@cornerstonejs/tools": "^1.20.3",
"@cornerstonejs/adapters": "^1.70.14",
"@cornerstonejs/core": "^1.70.14",
"@cornerstonejs/tools": "^1.70.14",
"classnames": "^2.3.2"
}
}

View File

@ -40,7 +40,9 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
return dataset;
};
const commandsModule = ({}) => {
const commandsModule = props => {
const { servicesManager } = props;
const { customizationService } = servicesManager.services;
const actions = {
/**
*
@ -95,7 +97,15 @@ const commandsModule = ({}) => {
throw new Error('Invalid report, no content');
}
await dataSource.store.dicom(naturalizedReport);
const onBeforeDicomStore =
customizationService.getModeCustomization('onBeforeDicomStore')?.value;
let dicomDict;
if (typeof onBeforeDicomStore === 'function') {
dicomDict = onBeforeDicomStore({ measurementData, naturalizedReport });
}
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
if (StudyInstanceUID) {
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
@ -118,13 +128,9 @@ const commandsModule = ({}) => {
const definitions = {
downloadReport: {
commandFn: actions.downloadReport,
storeContexts: [],
options: {},
},
storeMeasurements: {
commandFn: actions.storeMeasurements,
storeContexts: [],
options: {},
},
};

View File

@ -1,6 +1,6 @@
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
import addMeasurement from './utils/addMeasurement';
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
import isRehydratable from './utils/isRehydratable';
import { adaptersSR } from '@cornerstonejs/adapters';
@ -150,13 +150,37 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
return [displaySet];
}
function _load(displaySet, servicesManager, extensionManager) {
async function _load(displaySet, servicesManager, extensionManager) {
const { displaySetService, measurementService } = servicesManager.services;
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const { ContentSequence } = displaySet.instance;
async function retrieveBulkData(obj, parentObj = null, key = null) {
for (const prop in obj) {
if (typeof obj[prop] === 'object' && obj[prop] !== null) {
await retrieveBulkData(obj[prop], obj, prop);
} else if (Array.isArray(obj[prop])) {
await Promise.all(obj[prop].map(item => retrieveBulkData(item, obj, prop)));
} else if (prop === 'BulkDataURI') {
const value = await dataSource.retrieve.bulkDataURI({
BulkDataURI: obj[prop],
StudyInstanceUID: displaySet.instance.StudyInstanceUID,
SeriesInstanceUID: displaySet.instance.SeriesInstanceUID,
SOPInstanceUID: displaySet.instance.SOPInstanceUID,
});
if (parentObj && key) {
parentObj[key] = new Float32Array(value);
}
}
}
}
if (displaySet.isLoaded !== true) {
await retrieveBulkData(ContentSequence);
}
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
displaySet.measurements = _getMeasurements(ContentSequence);
@ -171,7 +195,12 @@ function _load(displaySet, servicesManager, extensionManager) {
// Check currently added displaySets and add measurements if the sources exist.
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(displaySet, activeDisplaySet, dataSource);
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
activeDisplaySet,
dataSource,
servicesManager
);
});
// Subscribe to new displaySets as the source may come in after.
@ -180,12 +209,23 @@ function _load(displaySet, servicesManager, extensionManager) {
// 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(displaySet, newDisplaySet, dataSource);
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
newDisplaySet,
dataSource,
servicesManager
);
});
});
}
function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dataSource) {
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
dataSource,
servicesManager
) {
const { customizationService } = servicesManager.services;
let unloadedMeasurements = srDisplaySet.measurements.filter(
measurement => measurement.loaded === false
);
@ -195,12 +235,16 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat
return;
}
if (!newDisplaySet instanceof ImageSet) {
if ((!newDisplaySet) instanceof ImageSet) {
// This also filters out _this_ displaySet, as it is not an ImageSet.
return;
}
const { sopClassUids, images } = newDisplaySet;
if (newDisplaySet.unsupported) {
return;
}
const { sopClassUids } = newDisplaySet;
// Check if any have the newDisplaySet is the correct SOPClass.
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
@ -240,9 +284,37 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
const measurement = unloadedMeasurements[j];
let measurement = unloadedMeasurements[j];
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
'onBeforeSRAddMeasurement'
)?.value;
if (typeof onBeforeSRAddMeasurement === 'function') {
measurement = onBeforeSRAddMeasurement({
measurement,
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
});
}
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
const frame =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
1;
/** Add DICOMSRDisplay annotation for the SR viewport (only) */
addDICOMSRDisplayAnnotation(measurement, imageId, frame);
/** 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;
unloadedMeasurements.splice(j, 1);
}
@ -258,7 +330,7 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
// Standard. But for now, we will support only one ReferenceFrameNumber.
const ReferencedFrameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
1;
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {

View File

@ -1,6 +1,6 @@
import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule, { srProtocol } from './getHangingProtocolModule';
import { srProtocol } from './getHangingProtocolModule';
import onModeEnter from './onModeEnter';
import getCommandsModule from './commandsModule';
import preRegistration from './init';

View File

@ -1,5 +1,4 @@
import {
addTool,
AngleTool,
annotation,
ArrowAnnotateTool,
@ -9,6 +8,7 @@ import {
CircleROITool,
LengthTool,
PlanarFreehandROITool,
RectangleROITool,
} from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import addToolInstance from './utils/addToolInstance';
@ -19,24 +19,25 @@ import toolNames from './tools/toolNames';
* @param {object} configuration
*/
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
addTool(DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool, {});
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool);
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
addToolInstance(toolNames.SRCircleROI, CircleROITool);
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
addToolInstance(toolNames.SRAngle, AngleTool);
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
// TODO - fix the rehydration of Freehand, as it throws an exception
// on a missing polyline. The fix is probably in CS3D
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
};
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
[toolNames.DICOMSRDisplay]: dashedLine,
SRLength: dashedLine,
SRBidirectional: dashedLine,
SREllipticalROI: dashedLine,
@ -45,6 +46,7 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
SRCobbAngle: dashedLine,
SRAngle: dashedLine,
SRPlanarFreehandROI: dashedLine,
SRRectangleROI: dashedLine,
global: {},
});
}

View File

@ -22,14 +22,14 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
}
_getTextBoxLinesFromLabels(labels) {
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
const labelLength = Math.min(labels.length, 3);
const labelLength = Math.min(labels.length, 5);
const lines = [];
for (let i = 0; i < labelLength; i++) {
const labelEntry = labels[i];
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`);
}
return lines;
@ -65,7 +65,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
// Filter toolData to only render the data for the active SR.
const filteredAnnotations = annotations.filter(annotation =>
trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier)
trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier)
);
if (!viewport._actors?.size) {
@ -77,20 +77,24 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
toolName: this.getToolName(),
viewportId: enabledElement.viewport.id,
};
const { style: annotationStyle } = annotation.config;
for (let i = 0; i < filteredAnnotations.length; i++) {
const annotation = filteredAnnotations[i];
const annotationUID = annotation.annotationUID;
const { renderableData } = annotation.data.cachedStats;
const { cachedStats } = annotation.data;
const { renderableData, TrackingUniqueIdentifier } = annotation.data;
const { referencedImageId } = annotation.metadata;
styleSpecifier.annotationUID = annotationUID;
const groupStyle = annotationStyle.getToolGroupToolStyles(this.toolGroupId)[
this.getToolName()
];
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
const color =
cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
? 'rgb(0, 255, 0)'
: this.getStyle('color', styleSpecifier, annotation);
@ -98,6 +102,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
color,
lineDash,
lineWidth,
...groupStyle,
};
Object.keys(renderableData).forEach(GraphicType => {
@ -160,6 +165,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
const drawingOptions = {
color: options.color,
width: options.lineWidth,
lineDash: options.lineDash,
};
let allCanvasCoordinates = [];
renderableData.map((data, index) => {
@ -307,6 +313,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
{
color: options.color,
width: options.lineWidth,
lineDash: options.lineDash,
}
);
});
@ -339,7 +346,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
const textLines = this._getTextBoxLinesFromLabels(label);
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
if (!annotation.data?.handles?.textBox?.worldPosition) {
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
}
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);

View File

@ -1,15 +1,13 @@
import { vec3 } from 'gl-matrix';
import { Types, annotation } from '@cornerstonejs/tools';
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
import toolNames from '../tools/toolNames';
import SCOORD_TYPES from '../constants/scoordTypes';
const EPSILON = 1e-4;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
export default function addMeasurement(measurement, imageId, displaySetInstanceUID) {
// TODO -> Render rotated ellipse .
export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) {
const toolName = toolNames.DICOMSRDisplay;
const measurementData = {
@ -27,59 +25,43 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
}
measurementData.renderableData[GraphicType].push(
_getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier)
_getRenderableData(GraphicType, GraphicData, imageId)
);
});
// Use the metadata provider to grab its imagePlaneModule metadata
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
const annotationManager = annotation.state.getAnnotationManager();
// Create Cornerstone3D Annotation from measurement
const frameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
1;
/**
* 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: {
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
toolName: toolName,
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
referencedImageId: imageId,
},
data: {
label: measurement.labels,
handles: {
textBox: {},
textBox: measurement.textBox ?? {},
},
cachedStats: {
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
},
frameNumber: frameNumber,
cachedStats: {},
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
frameNumber,
},
};
const annotationManager = annotation.state.getAnnotationManager();
annotationManager.addAnnotation(SRAnnotation);
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = displaySetInstanceUID;
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
// It'd be super weird if it didn't anyway as a SCOORD.
measurement.ReferencedSOPInstanceUID =
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
measurement.frameNumber = frameNumber;
delete measurement.coords;
}
function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifier) {
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
function _getRenderableData(GraphicType, GraphicData, imageId) {
let renderableData: csTypes.Point3[];
switch (GraphicType) {

View File

@ -90,7 +90,7 @@ export default function hydrateStructuredReport(
const datasetToUse = _mapLegacyDataSet(instance);
// Use dcmjs to generate toolState.
const storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
datasetToUse,
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
// for the imageToWorld transformation. The following assumes that the order
@ -101,6 +101,16 @@ export default function hydrateStructuredReport(
metaData
);
const onBeforeSRHydration =
customizationService.getModeCustomization('onBeforeSRHydration')?.value;
if (typeof onBeforeSRHydration === 'function') {
storedMeasurementByAnnotationType = onBeforeSRHydration({
storedMeasurementByAnnotationType,
displaySet,
});
}
// Filter what is found by DICOM SR to measurements we support.
const mappingDefinitions = mappings.map(m => m.annotationType);
const hydratableMeasurementsInSR = {};

View File

@ -1,35 +1,31 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
import { ServicesManager, ExtensionManager } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import { useAppConfig } from '@state';
const { formatDate } = utils;
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRViewport(props) {
const {
children,
dataSource,
displaySets,
viewportLabel,
viewportOptions,
servicesManager,
extensionManager,
} = props;
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
const [appConfig] = useAppConfig();
const { displaySetService, cornerstoneViewportService, measurementService } =
servicesManager.services;
const {
displaySetService,
cornerstoneViewportService,
measurementService,
viewportActionCornersService,
} = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@ -48,6 +44,8 @@ function OHIFCornerstoneSRViewport(props) {
const [element, setElement] = useState(null);
const { viewports, activeViewportId } = viewportGrid;
const { t } = useTranslation('Common');
// Optional hook into tracking extension, if present.
let trackedMeasurements;
let sendTrackedMeasurementsEvent;
@ -128,6 +126,10 @@ function OHIFCornerstoneSRViewport(props) {
console.warn('More than one SOPClassUID in the same series is not yet supported.');
}
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
// return;
// }
_getViewportReferencedDisplaySetData(
srDisplaySet,
newMeasurementSelected,
@ -202,7 +204,10 @@ function OHIFCornerstoneSRViewport(props) {
// The positionIds for the viewport aren't meaningful for the child display sets
positionIds: null,
}}
onElementEnabled={onElementEnabled}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
initialImageIndex={initialImageIndex}
isJumpToMeasurementDisabled={true}
></Component>
@ -213,18 +218,11 @@ function OHIFCornerstoneSRViewport(props) {
direction => {
let newMeasurementSelected = measurementSelected;
if (direction === 'right') {
newMeasurementSelected++;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
}
} else {
newMeasurementSelected--;
if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
newMeasurementSelected += direction;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
} else if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
setTrackingIdentifiers(newMeasurementSelected);
@ -263,12 +261,16 @@ function OHIFCornerstoneSRViewport(props) {
* if it is hydrated we don't even use the SR viewport.
*/
useEffect(() => {
if (!srDisplaySet.isLoaded) {
srDisplaySet.load();
}
const numMeasurements = srDisplaySet.measurements.length;
setMeasurementCount(numMeasurements);
}, [srDisplaySet]);
const loadSR = async () => {
if (!srDisplaySet.isLoaded) {
await srDisplaySet.load();
}
const numMeasurements = srDisplaySet.measurements.length;
setMeasurementCount(numMeasurements);
updateViewport(measurementSelected);
};
loadSR();
}, [dataSource, srDisplaySet]);
/**
* Hook to update the tracking identifiers when the selected measurement changes or
@ -285,18 +287,50 @@ function OHIFCornerstoneSRViewport(props) {
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
useEffect(() => {
isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
}, [trackedMeasurements]);
/**
* Data fetching for the SR displaySet, which updates the measurements and
* also gets the referenced image displaySet that SR is based on.
*/
useEffect(() => {
updateViewport(measurementSelected);
}, [dataSource, srDisplaySet]);
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
srDisplaySet,
viewportId,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
index: 0,
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onMeasurementChange}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
isLocked,
onMeasurementChange,
sendTrackedMeasurementsEvent,
srDisplaySet,
t,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@ -317,57 +351,8 @@ function OHIFCornerstoneSRViewport(props) {
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
SeriesNumber,
} = referencedDisplaySetMetadata;
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onMeasurementChange}
getStatusComponent={() =>
_getStatusComponent({
srDisplaySet,
viewportId,
isTracked: false,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
})
}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: SeriesDescription || '',
patientInformation: {
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
spacing:
SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{getCornerstoneViewport()}
{childrenWithProps}
@ -384,7 +369,6 @@ OHIFCornerstoneSRViewport.propTypes = {
viewportLabel: PropTypes.string,
customProps: PropTypes.object,
viewportOptions: PropTypes.object,
viewportLabel: PropTypes.string,
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
@ -402,6 +386,13 @@ async function _getViewportReferencedDisplaySetData(
const measurement = measurements[measurementSelected];
const { displaySetInstanceUID } = measurement;
if (!displaySet.keyImageDisplaySet) {
// Create a new display set, and preserve a reference to it here,
// so that it can be re-displayed and shown inside the SR viewport.
// This is only for ease of redisplay - the display set is stored in the
// usual manner in the display set service.
displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(displaySetService, displaySet);
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
@ -429,6 +420,7 @@ function _getStatusComponent({
isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
}) {
const handleMouseUp = () => {
sendTrackedMeasurementsEvent('HYDRATE_SR', {
@ -437,7 +429,6 @@ function _getStatusComponent({
});
};
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
// 1 - Incompatible

View File

@ -0,0 +1,8 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
};

View File

@ -0,0 +1,54 @@
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const pkg = require('./../package.json');
const ROOT_DIR = path.join(__dirname, '../');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
const ENTRY = {
app: `${SRC_DIR}/index.ts`,
};
const outputName = `ohif-${pkg.name.split('/').pop()}`;
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
return merge(commonConfig, {
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: ROOT_DIR,
library: 'ohif-extension-cornerstone',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
new MiniCssExtractPlugin({
filename: `./dist/${outputName}.css`,
chunkFilename: `./dist/${outputName}.css`,
}),
],
});
};

View File

@ -0,0 +1,196 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
### Bug Fixes
* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f))
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
### Bug Fixes
* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0))
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
### Bug Fixes
* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951))
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
### Features
* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb))
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
### Bug Fixes
* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af))
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
### Bug Fixes
* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d))
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
### Bug Fixes
* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02))
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
### Bug Fixes
* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8))
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
### Bug Fixes
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
### Bug Fixes
* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637))
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
### Features
* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13))
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
### Features
* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2))

View File

@ -0,0 +1,20 @@
MIT License
Copyright (c) 2023 cornerstone-dynamic-volume ()
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,8 @@
# cornerstone-dynamic-volume
## Description
## Author
OHIF
## License
MIT

View File

@ -0,0 +1 @@
module.exports = require('../../babel.config.js');

View File

@ -0,0 +1,50 @@
{
"name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.8.0-beta.93",
"description": "OHIF extension for 4D volumes data",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-extension-cornerstone-dynamic-volume.umd.js",
"module": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./types": "./src/types/index.ts"
},
"files": [
"dist",
"README.md"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"start": "yarn run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.8.0-beta.93",
"@ohif/extension-cornerstone": "3.8.0-beta.93",
"@ohif/extension-default": "3.8.0-beta.93",
"@ohif/i18n": "3.8.0-beta.93",
"@ohif/ui": "3.8.0-beta.93",
"dcmjs": "^0.29.5",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^17.0.2"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^1.70.14",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.14",
"@cornerstonejs/tools": "^1.70.14",
"classnames": "^2.3.2"
}
}

View File

@ -0,0 +1,3 @@
import updateSegmentationsChartDisplaySet from './updateSegmentationsChartDisplaySet';
export { updateSegmentationsChartDisplaySet };

View File

@ -0,0 +1,281 @@
import { DicomMetadataStore, utils } from '@ohif/core';
import * as cs from '@cornerstonejs/core';
import * as csTools from '@cornerstonejs/tools';
const CHART_MODALITY = 'CHT';
const SEG_CHART_INSTANCE_UID = utils.guid();
// Private SOPClassUid for chart data
const ChartDataSOPClassUid = '1.9.451.13215.7.3.2.7.6.1';
const { utilities: csToolsUtils } = csTools;
function _getDateTimeStr() {
const now = new Date();
const date =
now.getFullYear() + ('0' + now.getUTCMonth()).slice(-2) + ('0' + now.getUTCDate()).slice(-2);
const time =
('0' + now.getUTCHours()).slice(-2) +
('0' + now.getUTCMinutes()).slice(-2) +
('0' + now.getUTCSeconds()).slice(-2);
return { date, time };
}
function _getTimePointsDataByTagName(volume, timePointsTag) {
const uniqueTimePoints = volume.imageIds.reduce((timePoints, imageId) => {
const instance = DicomMetadataStore.getInstanceByImageId(imageId);
const timePointValue = instance[timePointsTag];
if (timePointValue !== undefined) {
timePoints.add(timePointValue);
}
return timePoints;
}, new Set());
return Array.from(uniqueTimePoints).sort((a: number, b: number) => a - b);
}
function _convertTimePointsUnit(timePoints, timePointsUnit) {
const validUnits = ['ms', 's', 'm', 'h'];
const divisors = [1000, 60, 60];
const currentUnitIndex = validUnits.indexOf(timePointsUnit);
let divisor = 1;
if (currentUnitIndex !== -1) {
for (let i = currentUnitIndex; i < validUnits.length - 1; i++) {
const newDivisor = divisor * divisors[i];
const greaterThanDivisorCount = timePoints.filter(timePoint => timePoint > newDivisor).length;
// Change the scale only if more than 50% of the time points are
// greater than the new divisor.
if (greaterThanDivisorCount <= timePoints.length / 2) {
break;
}
divisor = newDivisor;
timePointsUnit = validUnits[i + 1];
}
if (divisor > 1) {
timePoints = timePoints.map(timePoint => timePoint / divisor);
}
}
return { timePoints, timePointsUnit };
}
// It currently supports only one tag but a few other will be added soon
// Supported 4D Tags
// (0018,1060) Trigger Time [NOK]
// (0018,0081) Echo Time [NOK]
// (0018,0086) Echo Number [NOK]
// (0020,0100) Temporal Position Identifier [NOK]
// (0054,1300) FrameReferenceTime [OK]
function _getTimePointsData(volume) {
const timePointsTags = {
FrameReferenceTime: {
unit: 'ms',
},
};
const timePointsTagNames = Object.keys(timePointsTags);
let timePoints;
let timePointsUnit;
for (let i = 0; i < timePointsTagNames.length; i++) {
const tagName = timePointsTagNames[i];
const curTimePoints = _getTimePointsDataByTagName(volume, tagName);
if (curTimePoints.length) {
timePoints = curTimePoints;
timePointsUnit = timePointsTags[tagName].unit;
break;
}
}
if (!timePoints.length) {
const concatTagNames = timePointsTagNames.join(', ');
throw new Error(`Could not extract time points data for the following tags: ${concatTagNames}`);
}
const convertedTimePoints = _convertTimePointsUnit(timePoints, timePointsUnit);
timePoints = convertedTimePoints.timePoints;
timePointsUnit = convertedTimePoints.timePointsUnit;
return { timePoints, timePointsUnit };
}
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
const displaySets = displaySetService.getActiveDisplaySets();
const dynamic4DDisplaySet = displaySets.find(displaySet => {
const anInstance = displaySet.instances?.[0];
if (anInstance) {
return (
anInstance.FrameReferenceTime !== undefined || anInstance.NumberOfTimeSlices !== undefined
);
}
return false;
});
// const referencedDynamicVolume = cs.cache.getVolume(dynamic4DDisplaySet.displaySetInstanceUID);
let volumeCacheKey: string | undefined;
const volumeId = dynamic4DDisplaySet.displaySetInstanceUID;
for (const [key] of cs.cache._volumeCache) {
if (key.includes(volumeId)) {
volumeCacheKey = key;
break;
}
}
let referencedDynamicVolume;
if (volumeCacheKey) {
referencedDynamicVolume = cs.cache.getVolume(volumeCacheKey);
}
const { StudyInstanceUID, StudyDescription } = DicomMetadataStore.getInstanceByImageId(
referencedDynamicVolume.imageIds[0]
);
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
maskVolumeId: segmentation.id,
}) as number[][];
const pixelCount = timeData.length;
if (pixelCount === 0) {
return [];
}
// 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 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) {
timePointsData = _getTimePointsData(referencedDynamicVolume);
volumesTimePointsCache.set(referencedDynamicVolume, timePointsData);
}
const { timePoints, timePointsUnit } = timePointsData;
if (timePoints.length !== timeData[0].length) {
throw new Error('Invalid number of time points returned');
}
const timepointsCount = timePoints.length;
const chartSeriesData = new Array(timepointsCount);
for (let i = 0; i < timepointsCount; i++) {
const average = timeData.reduce((acc, cur) => acc + cur[i] / pixelCount, 0);
chartSeriesData[i] = [timePoints[i], average];
}
return {
StudyInstanceUID,
StudyDescription,
chartData: {
series: {
label: segmentation.label,
points: chartSeriesData,
color: hexColor,
},
axis: {
x: {
label: `Time (${timePointsUnit})`,
},
y: {
label: `Vl (Bq/ml)`,
},
},
},
};
}
function _getInstanceFromSegmentations(segmentations, displaySetService) {
if (!segmentations.length) {
return;
}
const volumesTimePointsCache = new WeakMap();
const segmentationsData = segmentations.map(segmentation =>
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
);
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
const series = segmentationsData.reduce((allSeries, curSegData) => {
return [...allSeries, curSegData.chartData.series];
}, []);
const instance = {
SOPClassUID: ChartDataSOPClassUid,
Modality: CHART_MODALITY,
SOPInstanceUID: utils.guid(),
SeriesDate: seriesDate,
SeriesTime: seriesTime,
SeriesInstanceUID: SEG_CHART_INSTANCE_UID,
StudyInstanceUID: segmentationsData[0].StudyInstanceUID,
StudyDescription: segmentationsData[0].StudyDescription,
SeriesNumber: 100,
SeriesDescription: 'Segmentation chart series data',
chartData: {
series,
axis: { ...segmentationsData[0].chartData.axis },
},
};
const seriesMetadata = {
StudyInstanceUID: instance.StudyInstanceUID,
StudyDescription: instance.StudyDescription,
SeriesInstanceUID: instance.SeriesInstanceUID,
SeriesDescription: instance.SeriesDescription,
SeriesNumber: instance.SeriesNumber,
SeriesTime: instance.SeriesTime,
SOPClassUID: instance.SOPClassUID,
Modality: instance.Modality,
};
return { seriesMetadata, instance };
}
function updateSegmentationsChartDisplaySet({ servicesManager }): void {
const { segmentationService, displaySetService } = servicesManager.services;
const segmentations = segmentationService.getSegmentations();
const { seriesMetadata, instance } =
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
if (seriesMetadata && instance) {
// An event is triggered after adding the instance and the displaySet is created
DicomMetadataStore.addSeriesMetadata([seriesMetadata], true);
DicomMetadataStore.addInstances([instance], true);
}
}
export { updateSegmentationsChartDisplaySet as default };

View File

@ -0,0 +1,407 @@
import * as importedActions from './actions';
import { utilities, Enums } from '@cornerstonejs/tools';
import { cache } from '@cornerstonejs/core';
const LABELMAP = Enums.SegmentationRepresentations.Labelmap;
const commandsModule = ({ commandsManager, servicesManager }) => {
const services = servicesManager.services;
const { displaySetService, viewportGridService, segmentationService } = services;
const actions = {
...importedActions,
getDynamic4DDisplaySet: () => {
const displaySets = displaySetService.getActiveDisplaySets();
const dynamic4DDisplaySet = displaySets.find(displaySet => {
const anInstance = displaySet.instances?.[0];
if (anInstance) {
return (
anInstance.FrameReferenceTime !== undefined ||
anInstance.NumberOfTimeSlices !== undefined ||
anInstance.TemporalPositionIdentifier !== undefined
);
}
return false;
});
return dynamic4DDisplaySet;
},
getComputedDisplaySets: () => {
const displaySetCache = displaySetService.getDisplaySetCache();
const cachedDisplaySets = [...displaySetCache.values()];
const computedDisplaySets = cachedDisplaySets.filter(displaySet => {
return displaySet.isDerived;
});
return computedDisplaySets;
},
exportTimeReportCSV: ({ segmentations, config, options, summaryStats }) => {
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
const volumeId = dynamic4DDisplaySet?.displaySetInstanceUID;
// cache._volumeCache is a map that has a key that includes the volumeId
// it is not exactly the volumeId, but it is the key that includes the volumeId
// so we can't do cache._volumeCache.get(volumeId) we should iterate
// over the keys and find the one that includes the volumeId
let volumeCacheKey: string | undefined;
for (const [key] of cache._volumeCache) {
if (key.includes(volumeId)) {
volumeCacheKey = key;
break;
}
}
let dynamicVolume;
if (volumeCacheKey) {
dynamicVolume = cache.getVolume(volumeCacheKey);
}
const instance = dynamic4DDisplaySet.instances[0];
const csv = [];
// CSV header information with placeholder empty values for the metadata lines
csv.push(`Patient ID,${instance.PatientID},`);
csv.push(`Study Date,${instance.StudyDate},`);
csv.push(`StudyInstanceUID,${instance.StudyInstanceUID},`);
csv.push(`StudyDescription,${instance.StudyDescription},`);
csv.push(`SeriesInstanceUID,${instance.SeriesInstanceUID},`);
// empty line
csv.push('');
csv.push('');
// Helper function to calculate standard deviation
function calculateStandardDeviation(data) {
const n = data.length;
const mean = data.reduce((acc, value) => acc + value, 0) / n;
const squaredDifferences = data.map(value => (value - mean) ** 2);
const variance = squaredDifferences.reduce((acc, value) => acc + value, 0) / n;
const stdDeviation = Math.sqrt(variance);
return stdDeviation;
}
// Iterate through each segmentation to get the timeData and ijkCoords
segmentations.forEach((segmentation, segmentationIndex) => {
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
maskVolumeId: segmentation.id,
}) as number[][];
if (summaryStats) {
// Adding column headers for pixel identifier and segmentation label ids
let headers = 'Operation,Segmentation Label ID';
const maxLength = dynamicVolume.numTimePoints;
for (let t = 0; t < maxLength; t++) {
headers += `,Time Point ${t}`;
}
csv.push(headers);
// // perform summary statistics on the timeData including for each time point, mean, median, min, max, and standard deviation for
// // all the voxels in the ROI
const mean = [];
const min = [];
const minIJK = [];
const max = [];
const maxIJK = [];
const std = [];
const numVoxels = timeData.length;
// Helper function to calculate standard deviation
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
// for each voxel in the ROI, get the value at the current time point
const voxelValues = [];
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
voxelValues.push(timeData[voxelIndex][timeIndex]);
}
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));
}
let row = `Mean,${segmentation.label}`;
// Generate separate rows for each statistic
for (let t = 0; t < maxLength; t++) {
row += `,${mean[t]}`;
}
csv.push(row);
row = `Standard Deviation,${segmentation.label}`;
for (let t = 0; t < maxLength; t++) {
row += `,${std[t]}`;
}
csv.push(row);
row = `Min,${segmentation.label}`;
for (let t = 0; t < maxLength; t++) {
row += `,${min[t]}`;
}
csv.push(row);
row = `Max,${segmentation.label}`;
for (let t = 0; t < maxLength; t++) {
row += `,${max[t]}`;
}
csv.push(row);
} else {
// Adding column headers for pixel identifier and segmentation label ids
let headers = 'Pixel Identifier (IJK),Segmentation Label ID';
const maxLength = dynamicVolume.numTimePoints;
for (let t = 0; t < maxLength; t++) {
headers += `,Time Point ${t}`;
}
csv.push(headers);
// Assuming timeData and ijkCoords are of the same length
for (let i = 0; i < timeData.length; i++) {
// Generate the pixel identifier
const pixelIdentifier = `${ijkCoords[i][0]}_${ijkCoords[i][1]}_${ijkCoords[i][2]}`;
// Start a new row for the current pixel
let row = `${pixelIdentifier},${segmentation.label}`;
// Add time data points for this pixel
for (let t = 0; t < timeData[i].length; t++) {
row += `,${timeData[i][t]}`;
}
// Append the row to the CSV array
csv.push(row);
}
}
});
// Convert to CSV string
const csvContent = csv.join('\n');
// Generate filename and trigger download
const filename = `${instance.PatientID}.csv`;
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
swapDynamicWithComputedDisplaySet: ({ displaySet }) => {
const computedDisplaySet = displaySet;
const displaySetCache = displaySetService.getDisplaySetCache();
const cachedDisplaySetKeys = [displaySetCache.keys()];
const { displaySetInstanceUID } = computedDisplaySet;
// Check to see if computed display set is already in cache
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
displaySetCache.set(displaySetInstanceUID, computedDisplaySet);
}
// Get all viewports and their corresponding indices
const { viewports } = viewportGridService.getState();
// get the viewports in the grid
// iterate over them and find the ones that are showing a dynamic
// volume (displaySet), and replace that exact displaySet with the
// computed displaySet
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
const viewportsToUpdate = [];
for (const [key, value] of viewports) {
const viewport = value;
const viewportOptions = viewport.viewportOptions;
const { displaySetInstanceUIDs } = viewport;
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
dynamic4DDisplaySet.displaySetInstanceUID
);
if (displaySetInstanceUIDIndex !== -1) {
const newViewport = {
viewportId: viewport.viewportId,
// merge the other displaySetInstanceUIDs with the new one
displaySetInstanceUIDs: [
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
displaySetInstanceUID,
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
],
viewportOptions: {
initialImageOptions: viewportOptions.initialImageOptions,
viewportType: 'volume',
orientation: viewportOptions.orientation,
background: viewportOptions.background,
},
};
viewportsToUpdate.push(newViewport);
}
}
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
},
swapComputedWithDynamicDisplaySet: () => {
// Todo: this assumes there is only one dynamic display set in the viewer
const dynamicDisplaySet = actions.getDynamic4DDisplaySet();
const displaySetCache = displaySetService.getDisplaySetCache();
const cachedDisplaySetKeys = [...displaySetCache.keys()]; // Fix: Spread to get the array
const { displaySetInstanceUID } = dynamicDisplaySet;
// Check to see if dynamic display set is already in cache
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
displaySetCache.set(displaySetInstanceUID, dynamicDisplaySet);
}
// Get all viewports and their corresponding indices
const { viewports } = viewportGridService.getState();
// Get the computed 4D display set
const computed4DDisplaySet = actions.getComputedDisplaySets()[0];
const viewportsToUpdate = [];
for (const [key, value] of viewports) {
const viewport = value;
const viewportOptions = viewport.viewportOptions;
const { displaySetInstanceUIDs } = viewport;
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
computed4DDisplaySet.displaySetInstanceUID
);
if (displaySetInstanceUIDIndex !== -1) {
const newViewport = {
viewportId: viewport.viewportId,
// merge the other displaySetInstanceUIDs with the new one
displaySetInstanceUIDs: [
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
displaySetInstanceUID,
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
],
viewportOptions: {
initialImageOptions: viewportOptions.initialImageOptions,
viewportType: 'volume',
orientation: viewportOptions.orientation,
background: viewportOptions.background,
},
};
viewportsToUpdate.push(newViewport);
}
}
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
},
createNewLabelMapForDynamicVolume: async ({ label }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
// get the dynamic 4D display set
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
const dynamic4DDisplaySetInstanceUID = dynamic4DDisplaySet.displaySetInstanceUID;
// check if the dynamic 4D display set is in the display, if not we might have
// the computed volumes and we should choose them for the segmentation
// creation
let referenceDisplaySet;
const activeViewport = viewports.get(activeViewportId);
const activeDisplaySetInstanceUIDs = activeViewport.displaySetInstanceUIDs;
const dynamicIsInActiveViewport = activeDisplaySetInstanceUIDs.includes(
dynamic4DDisplaySetInstanceUID
);
if (dynamicIsInActiveViewport) {
referenceDisplaySet = dynamic4DDisplaySet;
}
if (!referenceDisplaySet) {
// try to see if there is any derived displaySet in the active viewport
// which is referencing the dynamic 4D display set
// Todo: this is wrong but I don't have time to fix it now
const cachedDisplaySets = displaySetService.getDisplaySetCache();
for (const [key, displaySet] of cachedDisplaySets) {
if (displaySet.referenceDisplaySetUID === dynamic4DDisplaySetInstanceUID) {
referenceDisplaySet = displaySet;
break;
}
}
}
if (!referenceDisplaySet) {
throw new Error('No reference display set found based on the dynamic data');
}
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
referenceDisplaySet.displaySetInstanceUID,
{ label }
);
// Add Segmentation to all toolGroupIds in the viewer
const toolGroupIds = Array.from(
viewports.values(),
viewport => viewport.viewportOptions.toolGroupId
);
const representationType = LABELMAP;
for (const toolGroupId of toolGroupIds) {
const hydrateSegmentation = true;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId,
hydrateSegmentation,
representationType
);
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
}
return segmentationId;
},
};
const definitions = {
updateSegmentationsChartDisplaySet: {
commandFn: actions.updateSegmentationsChartDisplaySet,
storeContexts: [],
options: {},
},
exportTimeReportCSV: {
commandFn: actions.exportTimeReportCSV,
storeContexts: [],
options: {},
},
swapDynamicWithComputedDisplaySet: {
commandFn: actions.swapDynamicWithComputedDisplaySet,
storeContexts: [],
options: {},
},
createNewLabelMapForDynamicVolume: {
commandFn: actions.createNewLabelMapForDynamicVolume,
storeContexts: [],
options: {},
},
swapComputedWithDynamicDisplaySet: {
commandFn: actions.swapComputedWithDynamicDisplaySet,
storeContexts: [],
options: {},
},
};
return {
actions,
definitions,
defaultContext: 'DYNAMIC-VOLUME:CORNERSTONE',
};
};
export default commandsModule;

View File

@ -0,0 +1,654 @@
const DEFAULT_COLORMAP = '2hot';
const toolGroupIds = {
pt: 'dynamic4D-pt',
fusion: 'dynamic4D-fusion',
ct: 'dynamic4D-ct',
};
function getPTOptions({
colormap,
voiInverted,
}: {
colormap?: {
name: string;
opacity:
| number
| {
value: number;
opacity: number;
}[];
};
voiInverted?: boolean;
} = {}) {
return {
blendMode: 'MIP',
colormap,
voi: {
windowWidth: 5,
windowCenter: 2.5,
},
voiInverted,
};
}
function getPTViewports() {
const ptOptionsParams = {
colormap: {
name: DEFAULT_COLORMAP,
opacity: [
{ value: 0, opacity: 0 },
{ value: 0.1, opacity: 1 },
{ value: 1, opacity: 1 },
],
},
voiInverted: false,
};
return [
{
viewportOptions: {
viewportId: 'ptAxial',
viewportType: 'volume',
orientation: 'axial',
toolGroupId: toolGroupIds.pt,
initialImageOptions: {
preset: 'middle', // 'first', 'last', 'middle'
},
syncGroups: [
{
type: 'cameraPosition',
id: 'axialSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ptWLSync',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'ptDisplaySet',
options: { ...getPTOptions(ptOptionsParams) },
},
],
},
{
viewportOptions: {
viewportId: 'ptSagittal',
viewportType: 'volume',
orientation: 'sagittal',
toolGroupId: toolGroupIds.pt,
initialImageOptions: {
preset: 'middle', // 'first', 'last', 'middle'
},
syncGroups: [
{
type: 'cameraPosition',
id: 'sagittalSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ptWLSync',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'ptDisplaySet',
options: { ...getPTOptions(ptOptionsParams) },
},
],
},
{
viewportOptions: {
viewportId: 'ptCoronal',
viewportType: 'volume',
orientation: 'coronal',
toolGroupId: toolGroupIds.pt,
initialImageOptions: {
preset: 'middle', // 'first', 'last', 'middle'
},
syncGroups: [
{
type: 'cameraPosition',
id: 'coronalSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ptWLSync',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'ptDisplaySet',
options: { ...getPTOptions(ptOptionsParams) },
},
],
},
];
}
function getFusionViewports() {
const ptOptionsParams = {
colormap: {
name: DEFAULT_COLORMAP,
opacity: [
{ value: 0, opacity: 0 },
{ value: 0.1, opacity: 0.3 },
{ value: 1, opacity: 0.3 },
],
},
};
return [
{
viewportOptions: {
viewportId: 'fusionAxial',
viewportType: 'volume',
orientation: 'axial',
toolGroupId: toolGroupIds.fusion,
initialImageOptions: {
preset: 'middle', // 'first', 'last', 'middle'
},
syncGroups: [
{
type: 'cameraPosition',
id: 'axialSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ctWLSync',
source: false,
target: true,
},
{
type: 'voi',
id: 'fusionWLSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ptFusionWLSync',
source: false,
target: true,
options: {
syncInvertState: false,
},
},
],
},
displaySets: [
{
id: 'ctDisplaySet',
},
{
options: { ...getPTOptions(ptOptionsParams) },
id: 'ptDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'fusionSagittal',
viewportType: 'volume',
orientation: 'sagittal',
toolGroupId: toolGroupIds.fusion,
initialImageOptions: {
preset: 'middle', // 'first', 'last', 'middle'
},
syncGroups: [
{
type: 'cameraPosition',
id: 'sagittalSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ctWLSync',
source: false,
target: true,
},
{
type: 'voi',
id: 'fusionWLSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ptFusionWLSync',
source: false,
target: true,
options: {
syncInvertState: false,
},
},
],
},
displaySets: [
{
id: 'ctDisplaySet',
},
{
options: { ...getPTOptions(ptOptionsParams) },
id: 'ptDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'fusionCoronal',
viewportType: 'volume',
orientation: 'coronal',
toolGroupId: toolGroupIds.fusion,
initialImageOptions: {
preset: 'middle', // 'first', 'last', 'middle'
},
syncGroups: [
{
type: 'cameraPosition',
id: 'coronalSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ctWLSync',
source: false,
target: true,
},
{
type: 'voi',
id: 'fusionWLSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ptFusionWLSync',
source: false,
target: true,
options: {
syncInvertState: false,
},
},
],
},
displaySets: [
{
id: 'ctDisplaySet',
},
{
options: { ...getPTOptions(ptOptionsParams) },
id: 'ptDisplaySet',
},
],
},
];
}
function getSeriesChartViewport() {
return {
viewportOptions: {
viewportId: 'seriesChart',
},
displaySets: [
{
id: 'chartDisplaySet',
options: {
// This dataset does not require the download of any instance since it is pre-computed locally,
// but interleaveTopToBottom.ts was not loading any series because it consider that all viewports
// are a Cornerstone viewport which is not true in this case and it waits for all viewports to
// have called interleaveTopToBottom(...).
skipLoading: true,
},
},
],
};
}
function getCTViewports() {
return [
{
viewportOptions: {
viewportId: 'ctAxial',
viewportType: 'volume',
orientation: 'axial',
toolGroupId: toolGroupIds.ct,
initialImageOptions: {
preset: 'middle', // 'first', 'last', 'middle'
},
syncGroups: [
{
type: 'cameraPosition',
id: 'axialSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ctWLSync',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'ctDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'ctSagittal',
viewportType: 'volume',
orientation: 'sagittal',
toolGroupId: toolGroupIds.ct,
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'cameraPosition',
id: 'sagittalSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ctWLSync',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'ctDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'ctCoronal',
viewportType: 'volume',
orientation: 'coronal',
toolGroupId: toolGroupIds.ct,
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'cameraPosition',
id: 'coronalSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ctWLSync',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'ctDisplaySet',
},
],
},
];
}
const defaultProtocol = {
id: 'default4D',
locked: true,
// Don't store this hanging protocol as it applies to the currently active
// display set by default
// cacheId: null,
hasUpdatedPriorsInformation: false,
name: 'Default',
createdDate: '2023-01-01T00:00:00.000Z',
modifiedDate: '2023-01-01T00:00:00.000Z',
availableTo: {},
editableBy: {},
imageLoadStrategy: 'default', // "default" , "interleaveTopToBottom", "interleaveCenter"
protocolMatchingRules: [
{
attribute: 'ModalitiesInStudy',
constraint: {
contains: ['CT', 'PT'],
},
},
],
// -1 would be used to indicate active only, whereas other values are
// the number of required priors referenced - so 0 means active with
// 0 or more priors.
numberOfPriorsReferenced: -1,
displaySetSelectors: {
defaultDisplaySetId: {
// Unused currently
imageMatchingRules: [],
// Matches displaysets, NOT series
seriesMatchingRules: [
// Try to match series with images by default, to prevent weird display
// on SEG/SR containing studies
{
attribute: 'numImageFrames',
constraint: {
greaterThan: { value: 0 },
},
},
],
// Can be used to select matching studies
// studyMatchingRules: [],
},
ctDisplaySet: {
// Unused currently
imageMatchingRules: [],
// Matches displaysets, NOT series
seriesMatchingRules: [
{
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
},
required: true,
},
{
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
// Can be used to select matching studies
// studyMatchingRules: [],
},
ptDisplaySet: {
// Unused currently
imageMatchingRules: [],
// Matches displaysets, NOT series
seriesMatchingRules: [
{
attribute: 'Modality',
constraint: {
equals: 'PT',
},
required: true,
},
{
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
{
attribute: 'SeriesDescription',
constraint: {
contains: 'Corrected',
},
},
{
weight: 2,
attribute: 'SeriesDescription',
constraint: {
doesNotContain: {
value: 'Uncorrected',
},
},
},
// Should we check if CorrectedImage contains ATTN?
// (0028,0051) (CorrectedImage): NORM\DTIM\ATTN\SCAT\RADL\DECY
],
// Can be used to select matching studies
// studyMatchingRules: [],
},
chartDisplaySet: {
// Unused currently
imageMatchingRules: [],
// Matches displaysets, NOT series
seriesMatchingRules: [
{
attribute: 'Modality',
constraint: {
equals: {
value: 'CHT',
},
},
required: true,
},
],
},
},
stages: [
{
id: 'dataPreparation',
name: 'Data Preparation',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 3,
},
},
viewports: [...getPTViewports()],
createdDate: '2023-01-01T00:00:00.000Z',
},
{
id: 'registration',
name: 'Registration',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 3,
columns: 3,
},
},
viewports: [...getFusionViewports(), ...getCTViewports(), ...getPTViewports()],
createdDate: '2023-01-01T00:00:00.000Z',
},
{
id: 'roiQuantification',
name: 'ROI Quantification',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 3,
},
},
viewports: [...getFusionViewports()],
createdDate: '2023-01-01T00:00:00.000Z',
},
{
id: 'kineticAnalysis',
name: 'Kinetic Analysis',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 3,
layoutOptions: [
{
x: 0,
y: 0,
width: 1 / 3,
height: 1 / 2,
},
{
x: 1 / 3,
y: 0,
width: 1 / 3,
height: 1 / 2,
},
{
x: 2 / 3,
y: 0,
width: 1 / 3,
height: 1 / 2,
},
{
x: 0,
y: 1 / 2,
width: 1,
height: 1 / 2,
},
],
},
},
viewports: [...getFusionViewports(), getSeriesChartViewport()],
createdDate: '2023-01-01T00:00:00.000Z',
},
],
};
/**
* HangingProtocolModule should provide a list of hanging protocols that will be
* available in OHIF for Modes to use to decide on the structure of the viewports
* and also the series that hung in the viewports. Each hanging protocol is defined by
* { name, protocols}. Examples include the default hanging protocol provided by
* the default extension that shows 2x2 viewports.
*/
function getHangingProtocolModule() {
return [
{
name: defaultProtocol.id,
protocol: defaultProtocol,
},
];
}
export default getHangingProtocolModule;

View File

@ -0,0 +1,68 @@
import React from 'react';
import { DynamicDataPanel } from './panels';
import { Toolbox } from '@ohif/ui';
import DynamicExport from './panels/DynamicExport';
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
const wrappedDynamicDataPanel = () => {
return (
<DynamicDataPanel
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
/>
);
};
const wrappedDynamicToolbox = () => {
return (
<>
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="dynamic-toolbox"
title="Threshold Tools"
/>
</>
);
};
const wrappedDynamicExport = () => {
return (
<>
<DynamicExport
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
/>
</>
);
};
return [
{
name: 'dynamic-volume',
iconName: 'tab-4d',
iconLabel: '4D Workflow',
label: '4D Workflow',
component: wrappedDynamicDataPanel,
},
{
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,
},
];
}
export default getPanelModule;

View File

@ -0,0 +1,6 @@
import packageJson from '../package.json';
const id = packageJson.name;
const SOPClassHandlerName = 'dynamic-volume';
export { id, SOPClassHandlerName };

View File

@ -0,0 +1,57 @@
import { id } from './id';
import commandsModule from './commandsModule';
import getPanelModule from './getPanelModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import { cache } from '@cornerstonejs/core';
/**
* You can remove any of the following modules if you don't need them.
*/
const dynamicVolumeExtension = {
/**
* Only required property. Should be a unique value across all extensions.
* You ID can be anything you want, but it should be unique.
*/
id,
/**
* Perform any pre-registration tasks here. This is called before the extension
* is registered. Usually we run tasks such as: configuring the libraries
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
* this extension is providing.
*/
preRegistration: ({ servicesManager, commandsManager, configuration = {} }) => {
// TODO: look for the right fix
cache.setMaxCacheSize(5 * 1024 * 1024 * 1024);
},
/**
* PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name,
* iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/
getPanelModule,
/**
* ViewportModule should provide a list of viewports that will be available in OHIF
* for Modes to consume and use in the viewports. Each viewport is defined by
* {name, component} object. Example of a viewport module is the CornerstoneViewport
* that is provided by the Cornerstone extension in OHIF.
*/
getHangingProtocolModule,
/**
* CommandsModule should provide a list of commands that will be available in OHIF
* for Modes to consume and use in the viewports. Each command is defined by
* an object of { actions, definitions, defaultContext } where actions is an
* object of functions, definitions is an object of available commands, their
* options, and defaultContext is the default context for the command to run against.
*/
getCommandsModule: ({ servicesManager, commandsManager, extensionManager }) => {
return commandsModule({
servicesManager,
commandsManager,
extensionManager,
});
},
};
export { dynamicVolumeExtension as default };

View File

@ -0,0 +1,18 @@
import React from 'react';
import PanelGenerateImage from './PanelGenerateImage';
function DynamicDataPanel({ servicesManager, commandsManager }) {
return (
<div
className="flex flex-col text-white"
data-cy={'dynamic-volume-panel'}
>
<PanelGenerateImage
commandsManager={commandsManager}
servicesManager={servicesManager}
></PanelGenerateImage>
</div>
);
}
export default DynamicDataPanel;

View File

@ -0,0 +1,76 @@
import React, { useState, useEffect } from 'react';
import { ActionButtons } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
function DynamicExport({ commandsManager, servicesManager, extensionManager }) {
const { segmentationService } = servicesManager.services;
const { t } = useTranslation('dynamicExport');
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>
<div className="mt-3 flex justify-center px-2">
<ActionButtons
actions={actions}
t={t}
/>
</div>
</div>
);
}
export default DynamicExport;

View File

@ -0,0 +1,241 @@
import React, { useEffect, useState } from 'react';
import {
InputDoubleRange,
Button,
PanelSection,
ButtonGroup,
IconButton,
InputNumber,
Icon,
Tooltip,
} from '@ohif/ui';
import { Enums } from '@cornerstonejs/core';
const controlClassNames = {
sizeClassName: 'w-[58px] h-[28px]',
arrowsDirection: 'horizontal',
labelPosition: 'bottom',
};
const Header = ({ title, tooltip }) => (
<div className="flex items-center space-x-1">
<Tooltip
content={<div className="text-white">{tooltip}</div>}
position="bottom-left"
tight={true}
tooltipBoxClassName="max-w-xs p-2"
>
<Icon
name="info-link"
className="text-primary-active h-[14px] w-[14px]"
/>
</Tooltip>
<span className="text-aqua-pale text-[11px] uppercase">{title}</span>
</div>
);
const DynamicVolumeControls = ({
isPlaying,
onPlayPauseChange,
// fps
fps,
onFpsChange,
minFps,
maxFps,
// Frames
currentFrameIndex,
onFrameChange,
framesLength,
onGenerate,
onDoubleRangeChange,
onDynamicClick,
}) => {
const [computedView, setComputedView] = useState(false);
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
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);
};
return (
<div className="flex select-none flex-col">
<PanelSection
title="Controls"
childrenClassName="space-y-4 pb-5 px-5"
>
<div className="mt-2">
<Header
title="View"
tooltip={
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
}
/>
<ButtonGroup className="mt-2 w-full">
<button
className="w-1/2"
onClick={() => {
setComputedView(false);
onDynamicClick?.();
}}
>
4D
</button>
<button
className="w-1/2"
onClick={() => {
setComputedView(true);
}}
>
Computed
</button>
</ButtonGroup>
</div>
<div>
<FrameControls
onPlayPauseChange={onPlayPauseChange}
isPlaying={isPlaying}
computedView={computedView}
// fps
fps={fps}
onFpsChange={onFpsChange}
minFps={minFps}
maxFps={maxFps}
//
framesLength={framesLength}
onFrameChange={onFrameChange}
currentFrameIndex={currentFrameIndex}
/>
</div>
<div className={`mt-6 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
<Header
title="Computed Operation"
tooltip={
<div>
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be
applied to the data set.
<br></br> Range Slider: Choose the numeric range within which the operation will be
performed.
<br></br>Generate Button: Execute the chosen operation on the specified range of
data.{' '}
</div>
}
/>
<ButtonGroup
className={`mt-2 w-full `}
separated={true}
>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUM)}
>
{Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
</button>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
>
{Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
</button>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
>
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
</button>
</ButtonGroup>
<div className="w-ful mt-2">
<InputDoubleRange
values={sliderRangeValues}
onChange={handleSliderChange}
minValue={0}
showLabel={true}
allowNumberEdit={true}
maxValue={framesLength}
step={1}
/>
</div>
<Button
className="mt-2 !h-[26px] !w-[115px] self-start !p-0"
onClick={() => {
onGenerate(computeViewMode);
}}
>
Generate
</Button>
</div>
</PanelSection>
</div>
);
};
export default DynamicVolumeControls;
function FrameControls({
isPlaying,
onPlayPauseChange,
fps,
minFps,
maxFps,
onFpsChange,
framesLength,
onFrameChange,
currentFrameIndex,
computedView,
}) {
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
return (
<div className={computedView && 'ohif-disabled'}>
<Header
title="4D Controls"
tooltip={
<div>
Play/Pause Button: Begin or pause the animation of the 4D visualization. <br></br> Frame
Selector: Navigate through individual frames of the 4D data. <br></br> FPS (Frames Per
Second) Selector: Adjust the playback speed of the animation.
</div>
}
/>
<div className="mt-3 flex justify-between">
<IconButton
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]"
onClick={() => onPlayPauseChange(!isPlaying)}
>
<Icon
name={getPlayPauseIconName()}
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}
label="Frame"
{...controlClassNames}
/>
<InputNumber
value={fps}
onChange={onFpsChange}
minValue={minFps}
maxValue={maxFps}
{...controlClassNames}
label="FPS"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,71 @@
import React from 'react';
import { InputDoubleRange } from '@ohif/ui';
import { Select } from '@ohif/ui';
import { Button } from '@ohif/ui';
import PropTypes from 'prop-types';
const GenerateVolume = ({
rangeValues,
handleSliderChange,
operationsUI,
options,
handleGenerateOptionsChange,
onGenerateImage,
returnTo4D,
displayingComputedVolume,
}) => {
return (
<>
<div>
<div className="mb-2 text-white">Computed Image</div>
<Select
closeMenuOnSelect={true}
className="border-primary-main mr-2 bg-black text-white "
options={operationsUI}
placeholder={operationsUI.find(option => option.value === options.Operation).placeHolder}
value={options.Operation}
onChange={({ value }) => {
handleGenerateOptionsChange({
Operation: value,
});
}}
/>
<InputDoubleRange
values={rangeValues}
onChange={handleSliderChange}
minValue={rangeValues[0] || 1}
maxValue={rangeValues[1] || 2}
showLabel={true}
step={1}
/>
<div className="flex space-x-2">
<Button
onClick={onGenerateImage}
className="w-1/2"
>
Generate
</Button>
<Button
onClick={returnTo4D}
disabled={!displayingComputedVolume}
className="w-1/2"
>
Return To 4D
</Button>
</div>
</div>
</>
);
};
GenerateVolume.propTypes = {
rangeValues: PropTypes.array.isRequired,
handleSliderChange: PropTypes.func.isRequired,
operationsUI: PropTypes.array.isRequired,
options: PropTypes.object.isRequired,
handleGenerateOptionsChange: PropTypes.func.isRequired,
onGenerateImage: PropTypes.func.isRequired,
returnTo4D: PropTypes.func.isRequired,
};
export default GenerateVolume;

View File

@ -0,0 +1,275 @@
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { useCine, useViewportGrid } from '@ohif/ui';
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 }) {
const { cornerstoneViewportService, viewportGridService, displaySetService } =
servicesManager.services;
const [{ isCineEnabled }, cineService] = useCine();
const [{ activeViewportId }] = useViewportGrid();
//
const [timePointsRange, setTimePointsRange] = useState([]);
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
const [dynamicVolume, setDynamicVolume] = useState(null);
const [frameRate, setFrameRate] = useState(20);
const [isPlaying, setIsPlaying] = useState(isCineEnabled);
const [timePointRendered, setTimePointRendered] = useState(null);
const [displayingComputed, setDisplayingComputed] = useState(false);
//
const uuidComputedVolume = useRef(csUtils.uuidv4());
const uuidDynamicVolume = useRef(null);
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
useEffect(() => {
const evt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, evtDetails => {
evtDetails.viewportData.data.forEach(volumeData => {
if (volumeData.volume.isDynamicVolume()) {
setDynamicVolume(volumeData.volume);
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
setTimePointsRange([1, volumeData.volume.numTimePoints]);
}
});
});
return () => {
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 || displaySetUIDs.length === 0) {
return;
}
const displaySets = displaySetUIDs.map(displaySetUID =>
displaySetService.getDisplaySetByUID(displaySetUID)
);
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
if (!dynamicVolumeDisplaySet) {
return;
}
const dynamicVolume = cache
.getVolumes()
.find(volume => volume.volumeId.includes(dynamicVolumeDisplaySet.displaySetInstanceUID));
if (!dynamicVolume) {
return;
}
setDynamicVolume(dynamicVolume);
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
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', {
displaySet,
});
setDisplayingComputed(true);
}
function renderDynamicImage(displaySet) {
commandsManager.runCommand('swapComputedWithDynamicDisplaySet');
}
// Get computed volume from cache, calculate the data across the time frames,
// set the scalar data to the computedVolume, and create displaySet
async function onGenerateImage(operationName) {
const dynamicVolumeId = dynamicVolume.volumeId;
if (!dynamicVolumeId) {
return;
}
let computedVolume = cache.getVolume(computedVolumeId);
if (!computedVolume) {
await createComputedVolume(dynamicVolumeId, computedVolumeId);
computedVolume = cache.getVolume(computedVolumeId);
}
const vals = timePointsRangeToUseForGenerate;
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
if (!computedDisplaySet) {
const displaySet = {
volumeLoaderSchema: computedVolume.volumeId.split(':')[0],
displaySetInstanceUID: uuidComputedVolume.current,
SOPClassHandlerId: SOPClassHandlerId,
Modality: dynamicVolume.metadata.Modality,
isMultiFrame: false,
numImageFrames: 1,
uid: uuidComputedVolume.current,
referenceDisplaySetUID: dynamicVolume.volumeId.split(':')[1],
madeInClient: true,
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
isDerived: true,
};
setComputedDisplaySet(displaySet);
renderGeneratedImage(displaySet);
} else {
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);
// }
}
}
const onPlayPauseChange = isPlaying => {
isPlaying ? handlePlay() : handleStop();
};
const handlePlay = () => {
setIsPlaying(true);
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId);
if (!viewportInfo) {
return;
}
const { element } = viewportInfo;
cineService.playClip(element, { framesPerSecond: frameRate, viewportId: activeViewportId });
};
const handleStop = () => {
setIsPlaying(false);
const { element } = cornerstoneViewportService.getViewportInfo(activeViewportId);
cineService.stopClip(element);
};
const handleSetFrameRate = newFrameRate => {
setFrameRate(newFrameRate);
handleStop();
handlePlay();
};
function handleSliderChange(newValues) {
if (
newValues[0] === timePointsRangeToUseForGenerate[0] &&
newValues[1] === timePointsRangeToUseForGenerate[1]
) {
return;
}
setTimePointsRangeToUseForGenerate(newValues);
}
if (!dynamicVolume || timePointsRange.length === 0) {
return null;
}
return (
<DynamicVolumeControls
fps={frameRate}
isPlaying={isPlaying}
onPlayPauseChange={onPlayPauseChange}
minFps={1}
maxFps={50}
currentFrameIndex={timePointRendered}
onFpsChange={handleSetFrameRate}
framesLength={timePointsRange[1]}
onFrameChange={timePointIndex => {
dynamicVolume.timePointIndex = timePointIndex;
}}
onGenerate={onGenerateImage}
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
onDoubleRangeChange={handleSliderChange}
/>
);
}
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,
};

View File

@ -0,0 +1,27 @@
import React, { useMemo } from 'react';
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
data-cy={'workflow-panel'}
className="bg-secondary-dark mb-1 px-3 py-4"
>
<div className="mb-1">Workflow</div>
<div>
<ProgressDropdownWithService servicesManager={servicesManager} />
</div>
</div>
);
}
export default WorkflowPanel;

View File

@ -0,0 +1,5 @@
import DynamicDataPanel from './DynamicDataPanel';
import WorkflowPanel from './WorkflowPanel';
import PanelGenerateImage from './PanelGenerateImage';
export { DynamicDataPanel, WorkflowPanel, PanelGenerateImage };

View File

@ -3,7 +3,893 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.7.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.7.0) (2023-10-11)
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
### Bug Fixes
* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f))
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
### Bug Fixes
* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0))
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
### Bug Fixes
* **viewport-sync:** Enable re-sync image slices in a different position when needed ([#3984](https://github.com/OHIF/Viewers/issues/3984)) ([6ebd2cc](https://github.com/OHIF/Viewers/commit/6ebd2cc7cb70cd88fd01dc1e516077f27b201943))
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
### Bug Fixes
* **vewport:** Add missing blendmodes from cornerstonejs ([#4055](https://github.com/OHIF/Viewers/issues/4055)) ([3ec7e51](https://github.com/OHIF/Viewers/commit/3ec7e512169a07506388902acb5b2c118093fa50))
* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951))
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
### Bug Fixes
* **hp:** Fails to display any layouts in the layout selector if first layout has multiple stages ([#4058](https://github.com/OHIF/Viewers/issues/4058)) ([f0ed3fd](https://github.com/OHIF/Viewers/commit/f0ed3fd7b99b0e4e00b261ceb9888ba94726719c))
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
### Features
* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb))
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
### Bug Fixes
* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af))
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
### Bug Fixes
* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d))
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
### Bug Fixes
* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02))
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
### Bug Fixes
* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8))
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
### Bug Fixes
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
### Bug Fixes
* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637))
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
### Features
* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13))
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
### Bug Fixes
* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90))
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
### Features
* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2))
# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05)
### Bug Fixes
* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e))
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
### Features
* **advanced-roi-tools:** new tools and icon updates and overlay bug fixes ([#4014](https://github.com/OHIF/Viewers/issues/4014)) ([cea27d4](https://github.com/OHIF/Viewers/commit/cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1))
# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05)
### Features
* **measurement:** Add support measurement label autocompletion ([#3855](https://github.com/OHIF/Viewers/issues/3855)) ([56b1eae](https://github.com/OHIF/Viewers/commit/56b1eae6356a6534960df1196bdd1e95b0a9a470))
# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03)
### Bug Fixes
* **presentation-state:** Iterate over map properly to restore the presentation state ([#4013](https://github.com/OHIF/Viewers/issues/4013)) ([fa38e6a](https://github.com/OHIF/Viewers/commit/fa38e6a07a259d8cb33277922884e722414ac548))
# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03)
### Features
* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d))
# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02)
### Features
* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1))
# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28)
### Bug Fixes
* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43))
# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28)
### Features
* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6))
# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27)
### Features
* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5))
# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18)
### Bug Fixes
* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5))
# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22)
### Bug Fixes
* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30))
# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21)
### Features
* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6))
# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22)
### Bug Fixes
* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db))
# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12)
### Bug Fixes
* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412))
# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09)
### Bug Fixes
* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6))
# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08)
### Bug Fixes
* colormap for stack viewports via HangingProtocol ([#3866](https://github.com/OHIF/Viewers/issues/3866)) ([e8858f3](https://github.com/OHIF/Viewers/commit/e8858f3eb55552f695af4a55980f9ae2e9af7291))
# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13)
### Features
* **overlay:** add inline binary overlays ([#3852](https://github.com/OHIF/Viewers/issues/3852)) ([0177b62](https://github.com/OHIF/Viewers/commit/0177b625ba86760168bc4db58c8a109aa9ee83cb))
# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13)
### Features
* **customizationService:** Enable saving and loading of private tags in SRs ([#3842](https://github.com/OHIF/Viewers/issues/3842)) ([e1f55e6](https://github.com/OHIF/Viewers/commit/e1f55e65f2d2a34136ad5d0b1ada77d337a0ea23))
# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08)
### Features
* **HP:** Added new 3D hanging protocols to be used in the new layout selector ([#3844](https://github.com/OHIF/Viewers/issues/3844)) ([59576d6](https://github.com/OHIF/Viewers/commit/59576d695d4d26601d35c43f73d602f0b12d72bf))
# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06)
### Bug Fixes
* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df))
# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24)
### Bug Fixes
* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682))
# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21)
### Bug Fixes
* **DICOM Overlay:** The overlay data wasn't being refreshed on change ([#3793](https://github.com/OHIF/Viewers/issues/3793)) ([00e7519](https://github.com/OHIF/Viewers/commit/00e751933ac6d611a34773fa69594243f1b99082))
# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13)
### Bug Fixes
* 🐛 Run error handler for failed image requests ([#3773](https://github.com/OHIF/Viewers/issues/3773)) ([3234014](https://github.com/OHIF/Viewers/commit/323401418e7ccab74655ba02f990bbe0ed4e523b))
# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13)
### Bug Fixes
* **overlay:** Overlays aren't shown on undefined origin ([#3781](https://github.com/OHIF/Viewers/issues/3781)) ([fd1251f](https://github.com/OHIF/Viewers/commit/fd1251f751d8147b8a78c7f4d81c67ba69769afa))
# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10)
### Features
* **dicomJSON:** Add Loading Other Display Sets and JSON Metadata Generation script ([#3777](https://github.com/OHIF/Viewers/issues/3777)) ([43b1c17](https://github.com/OHIF/Viewers/commit/43b1c17209502e4876ad59bae09ed9442eda8024))
# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09)
### Bug Fixes
* **arrow:** ArrowAnnotate text key cause validation error ([#3771](https://github.com/OHIF/Viewers/issues/3771)) ([8af1046](https://github.com/OHIF/Viewers/commit/8af10468035f1f59e0a21e579d50ad63c8cbf7ad))
# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08)
### Features
* add VolumeViewport rotation ([#3776](https://github.com/OHIF/Viewers/issues/3776)) ([442f99d](https://github.com/OHIF/Viewers/commit/442f99d5eb2ceece7def20e14da59af1dd7d8442))
# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08)
### Features
* **hp callback:** Add viewport ready callback ([#3772](https://github.com/OHIF/Viewers/issues/3772)) ([bf252bc](https://github.com/OHIF/Viewers/commit/bf252bcec2aae3a00479fdcb732110b344bcf2c0))
# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25)
### Bug Fixes
* **toolbar:** allow customizable toolbar for active viewport and allow active tool to be deactivated via a click ([#3608](https://github.com/OHIF/Viewers/issues/3608)) ([dd6d976](https://github.com/OHIF/Viewers/commit/dd6d9768bbca1d3cc472e8c1e6d85822500b96ef))
# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24)
### Bug Fixes
* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3))
# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19)
### Bug Fixes
* **cine:** Use the frame rate specified in DICOM and optionally auto play cine ([#3735](https://github.com/OHIF/Viewers/issues/3735)) ([d9258ec](https://github.com/OHIF/Viewers/commit/d9258eca70587cf4dc18be4e56c79b16bae73d6d))
# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19)
### Bug Fixes
* **calibration:** No calibration popup caused by perhaps an unused code optimization for production builds ([#3736](https://github.com/OHIF/Viewers/issues/3736)) ([93d798d](https://github.com/OHIF/Viewers/commit/93d798db99c0dee53ef73c376f8a74ac3049cf3f))
# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12)
**Note:** Version bump only for package @ohif/extension-cornerstone

View File

@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.7.0",
"version": "3.8.0-beta.93",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@ -25,6 +25,8 @@
"access": "public"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --progress --config .webpack/webpack.prod.js",
@ -36,10 +38,11 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.20.3",
"@ohif/core": "3.7.0",
"@ohif/ui": "3.7.0",
"dcmjs": "^0.29.6",
"@cornerstonejs/dicom-image-loader": "^1.70.14",
"@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.8.0-beta.93",
"@ohif/ui": "3.8.0-beta.93",
"dcmjs": "^0.29.12",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@ -52,11 +55,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.20.3",
"@cornerstonejs/core": "^1.20.3",
"@cornerstonejs/streaming-image-volume-loader": "^1.20.3",
"@cornerstonejs/tools": "^1.20.3",
"@kitware/vtk.js": "27.3.1",
"@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": "30.4.1",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",
"lodash.merge": "^4.6.2",

View File

@ -10,7 +10,7 @@ import {
utilities as csUtils,
} from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import { Notification, useViewportDialog } from '@ohif/ui';
import { Notification, useViewportDialog, AllInOneMenu } from '@ohif/ui';
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
import { setEnabledElement } from '../state';
@ -22,6 +22,12 @@ import CornerstoneServices from '../types/CornerstoneServices';
import CinePlayer from '../components/CinePlayer';
import { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
import { useAppConfig } from '@state';
import { LutPresentation, PositionPresentation } from '../types/Presentation';
const STACK = 'stack';
/**
@ -101,7 +107,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
onElementDisabled,
isJumpToMeasurementDisabled,
@ -109,12 +114,32 @@ const OHIFCornerstoneViewport = React.memo(props => {
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIndex,
// if the viewport is part of a hanging protocol layout
// we should not really rely on the old synchronizers and
// you see below we only rehydrate the synchronizers if the viewport
// is not part of the hanging protocol layout. HPs should
// define their own synchronizers. Since the synchronizers are
// viewportId dependent and
isHangingProtocolLayout,
} = props;
const viewportId = viewportOptions.viewportId;
if (!viewportId) {
throw new Error('Viewport ID is required');
}
// Since we only have support for dynamic data in volume viewports, we should
// handle this case here and set the viewportType to volume if any of the
// displaySets are dynamic volumes
viewportOptions.viewportType = displaySets.some(ds => ds.isDynamicVolume && ds.isReconstructable)
? 'volume'
: viewportOptions.viewportType;
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef();
const [appConfig] = useAppConfig();
const {
measurementService,
@ -126,12 +151,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
cornerstoneCacheService,
viewportGridService,
stateSyncService,
viewportActionCornersService,
} = servicesManager.services as CornerstoneServices;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
@ -151,6 +177,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
viewportActionCornersService.clear(viewportId);
},
[viewportId]
);
@ -175,6 +203,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
const synchronizersStore = stateSyncService.getState().synchronizersStore;
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
// If the viewport used to have a synchronizer, re apply it again
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
}
if (onElementEnabled) {
onElementEnabled(evt);
}
@ -197,13 +232,18 @@ const OHIFCornerstoneViewport = React.memo(props => {
return;
}
cleanUpServices(viewportInfo);
cornerstoneViewportService.storePresentation({ viewportId });
// This should be done after the store presentation since synchronizers
// will get cleaned up and they need the viewportInfo to be present
cleanUpServices(viewportInfo);
if (onElementDisabled) {
onElementDisabled(viewportInfo);
}
cornerstoneViewportService.disableElement(viewportId);
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
}, []);
@ -265,7 +305,10 @@ const OHIFCornerstoneViewport = React.memo(props => {
// The presentation state will have been stored previously by closing
// a viewport. Otherwise, this viewport will be unchanged and the
// presentation information will be directly carried over.
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
const state = stateSyncService.getState();
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
const { presentationIds } = viewportOptions;
const presentations = {
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
@ -344,12 +387,38 @@ const OHIFCornerstoneViewport = React.memo(props => {
};
}, [displaySets, elementRef, viewportId]);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
// Doing an === check here because the default config value when not set is true
if (appConfig.addWindowLevelActionMenu === false) {
return;
}
// TODO: In the future we should consider using the customization service
// to determine if and in which corner various action components should go.
const wlActionMenu = getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
});
viewportActionCornersService.setComponent({
viewportId,
id: 'windowLevelActionMenu',
component: wlActionMenu,
location: viewportActionCornersService.LOCATIONS.topRight,
indexPriority: -100,
});
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
return (
<React.Fragment>
<div className="viewport-wrapper">
<ReactResizeDetector
refreshMode="debounce"
refreshRate={50} // Wait 50 ms after last move to render
onResize={onResize}
targetRef={elementRef.current}
/>
@ -373,7 +442,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
servicesManager={servicesManager}
/>
</div>
<div className="absolute w-full">
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
@ -382,9 +452,12 @@ const OHIFCornerstoneViewport = React.memo(props => {
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
onKeyPress={viewportDialogState.onKeyPress}
/>
)}
</div>
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
<OHIFViewportActionCorners viewportId={viewportId} />
</React.Fragment>
);
}, areEqual);
@ -412,7 +485,10 @@ function _subscribeToJumpToMeasurementEvents(
cornerstoneViewportService.getViewportIdToJump(
jumpId,
measurement.displaySetInstanceUID,
{ referencedImageId: measurement.referencedImageId }
{
referencedImageId:
measurement.referencedImageId || measurement.metadata?.referencedImageId,
}
);
}
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
@ -521,6 +597,9 @@ function _jumpToMeasurement(
i => i.SOPInstanceUID === SOPInstanceUID
);
// the index is reversed in the volume viewport
// imageIdIndex = referencedDisplaySet.images.length - 1 - imageIdIndex;
const { viewPlaneNormal: viewportViewPlane } = viewport.getCamera();
// should compare abs for both planes since the direction can be flipped
@ -547,6 +626,58 @@ function _jumpToMeasurement(
}
}
function _rehydrateSynchronizers(
synchronizersStore: { [key: string]: unknown },
viewportId: string,
syncGroupService: any
) {
synchronizersStore[viewportId].forEach(synchronizerObj => {
if (!synchronizerObj.id) {
return;
}
const { id, sourceViewports, targetViewports } = synchronizerObj;
const synchronizer = syncGroupService.getSynchronizer(id);
if (!synchronizer) {
return;
}
const sourceViewportInfo = sourceViewports.find(
sourceViewport => sourceViewport.viewportId === viewportId
);
const targetViewportInfo = targetViewports.find(
targetViewport => targetViewport.viewportId === viewportId
);
const isSourceViewportInSynchronizer = synchronizer
.getSourceViewports()
.find(sourceViewport => sourceViewport.viewportId === viewportId);
const isTargetViewportInSynchronizer = synchronizer
.getTargetViewports()
.find(targetViewport => targetViewport.viewportId === viewportId);
// if the viewport was previously a source viewport, add it again
if (sourceViewportInfo && !isSourceViewportInSynchronizer) {
synchronizer.addSource({
viewportId: sourceViewportInfo.viewportId,
renderingEngineId: sourceViewportInfo.renderingEngineId,
});
}
// if the viewport was previously a target viewport, add it again
if (targetViewportInfo && !isTargetViewportInSynchronizer) {
synchronizer.addTarget({
viewportId: targetViewportInfo.viewportId,
renderingEngineId: targetViewportInfo.renderingEngineId,
});
}
});
}
// Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';

View File

@ -2,21 +2,25 @@ 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 { ViewportOverlay } from '@ohif/ui';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { InstanceMetadata } from 'platform/core/src/types';
import { ServicesManager } from '@ohif/core';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
import { ViewportOverlay } from '@ohif/ui';
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';
import './CustomizableViewportOverlay.css';
const EPSILON = 1e-4;
type ViewportData = StackViewportData | VolumeViewportData;
interface OverlayItemProps {
element: any;
viewportData: any;
element: HTMLElement;
viewportData: ViewportData;
imageSliceData: ImageSliceData;
servicesManager: ServicesManager;
viewportId: string;
instance: InstanceMetadata;
customization: any;
formatters: {
@ -35,67 +39,11 @@ interface OverlayItemProps {
scale?: number;
}
/**
* Window Level / Center Overlay item
*/
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi;
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 shrink-0 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1 shrink-0">L:</span>
<span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}
/**
* Zoom Level Overlay item
*/
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
/**
* Instance Number Overlay Item
*/
function InstanceNumberOverlayItem({
instanceNumber,
imageSliceData,
customization,
}: OverlayItemProps) {
const { imageIndex, numberOfSlices } = imageSliceData;
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">I:</span>
<span className="font-light">
{instanceNumber !== undefined && instanceNumber !== null
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}
const OverlayItemComponents = {
'ohif.overlayItem.windowLevel': VOIOverlayItem,
'ohif.overlayItem.zoomLevel': ZoomOverlayItem,
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
};
/**
* Customizable Viewport Overlay
@ -106,12 +54,17 @@ function CustomizableViewportOverlay({
imageSliceData,
viewportId,
servicesManager,
}: {
element: HTMLElement;
viewportData: ViewportData;
imageSliceData: ImageSliceData;
viewportId: string;
servicesManager: ServicesManager;
}) {
const { toolbarService, cornerstoneViewportService, customizationService } =
const { cornerstoneViewportService, customizationService, toolGroupService } =
servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
const { imageIndex } = imageSliceData;
const topLeftCustomization = customizationService.getModeCustomization(
@ -127,27 +80,21 @@ function CustomizableViewportOverlay({
'cornerstoneOverlayBottomRight'
);
const instance = useMemo(() => {
const instances = useMemo(() => {
if (viewportData != null) {
return _getViewportInstance(viewportData, imageIndex);
return _getViewportInstances(viewportData);
} else {
return null;
}
}, [viewportData, imageIndex]);
const instanceNumber = useMemo(() => {
if (viewportData != null) {
return _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService);
}
return null;
}, [viewportData, viewportId, imageIndex, cornerstoneViewportService]);
/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(toolbarService.getActiveTools());
}, []);
const instanceNumber = useMemo(
() =>
viewportData
? getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService)
: null,
[viewportData, viewportId, imageIndex, cornerstoneViewportService]
);
/**
* Updating the VOI when the viewport changes its voi
@ -215,26 +162,9 @@ function CustomizableViewportOverlay({
};
}, [viewportId, viewportData, cornerstoneViewportService, element]);
/**
* Updating the active tools when the toolbar changes
*/
// Todo: this should act on the toolGroups instead of the toolbar state
useEffect(() => {
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
setActiveTools(toolbarService.getActiveTools());
}
);
return () => {
unsubscribe();
};
}, [toolbarService]);
const _renderOverlayItem = useCallback(
item => {
const overlayItemProps: OverlayItemProps = {
const overlayItemProps = {
element,
viewportData,
imageSliceData,
@ -242,24 +172,26 @@ function CustomizableViewportOverlay({
servicesManager,
customization: item,
formatters: {
formatPN: formatPN,
formatPN,
formatDate: formatDICOMDate,
formatTime: formatDICOMTime,
formatNumberPrecision: formatNumberPrecision,
formatNumberPrecision,
},
instance,
// calculated
instance: instances ? instances[item?.instanceIndex] : null,
voi,
scale,
instanceNumber,
};
if (item.customizationType === 'ohif.overlayItem.windowLevel') {
return <VOIOverlayItem {...overlayItemProps} />;
} else if (item.customizationType === 'ohif.overlayItem.zoomLevel') {
return <ZoomOverlayItem {...overlayItemProps} />;
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
return <InstanceNumberOverlayItem {...overlayItemProps} />;
if (!item) {
return null;
}
const { customizationType } = item;
const OverlayItemComponent = OverlayItemComponents[customizationType];
if (OverlayItemComponent) {
return <OverlayItemComponent {...overlayItemProps} />;
} else {
const renderItem = customizationService.transform(item);
@ -275,110 +207,155 @@ function CustomizableViewportOverlay({
viewportId,
servicesManager,
customizationService,
instance,
instances,
voi,
scale,
instanceNumber,
]
);
const getTopLeftContent = useCallback(() => {
const items = topLeftCustomization?.items || [
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
];
return (
<>
{items.map((item, i) => (
<div key={`topLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [topLeftCustomization, _renderOverlayItem]);
const getContent = useCallback(
(customization, defaultItems, keyPrefix) => {
const items = customization?.items ?? defaultItems;
return (
<>
{items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}>
{item?.condition
? item.condition({
instance: instances ? instances[item?.instanceIndex] : null,
formatters: { formatDate: formatDICOMDate },
})
? _renderOverlayItem(item)
: null
: _renderOverlayItem(item)}
</div>
))}
</>
);
},
[_renderOverlayItem]
);
const getTopRightContent = useCallback(() => {
const items = topRightCustomization?.items || [
{
id: 'InstanceNmber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
];
return (
<>
{items.map((item, i) => (
<div key={`topRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [topRightCustomization, _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 getBottomLeftContent = useCallback(() => {
const items = bottomLeftCustomization?.items || [];
return (
<>
{items.map((item, i) => (
<div key={`bottomLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [bottomLeftCustomization, _renderOverlayItem]);
const seriesDescriptionItem = {
id: 'SeriesDescription',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Series description',
attribute: 'SeriesDescription',
condition: ({ instance }) => {
return instance && instance.SeriesDescription;
},
};
const getBottomRightContent = useCallback(() => {
const items = bottomRightCustomization?.items || [];
return (
<>
{items.map((item, i) => (
<div key={`bottomRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [bottomRightCustomization, _renderOverlayItem]);
const topLeftItems = instances
? instances
.map((instance, index) => {
return [
{
...studyDateItem,
instanceIndex: index,
},
{
...seriesDescriptionItem,
instanceIndex: index,
},
];
})
.flat()
: [];
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
bottomLeft={getBottomLeftContent()}
bottomRight={getBottomRightContent()}
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'
)}
/>
);
}
function _getViewportInstance(viewportData, imageIndex) {
let imageId = null;
function _getViewportInstances(viewportData) {
const imageIds = [];
if (viewportData.viewportType === Enums.ViewportType.STACK) {
imageId = viewportData.data.imageIds[imageIndex];
imageIds.push(viewportData.data.imageIds[0]);
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
const volumes = viewportData.data;
if (volumes && volumes.length == 1) {
const volume = volumes[0];
imageId = volume.imageIds[imageIndex];
}
volumes.forEach(volume => {
if (!volume?.imageIds || volume.imageIds.length === 0) {
return;
}
imageIds.push(volume.imageIds[0]);
});
}
return imageId ? metaData.get('instance', imageId) || {} : {};
const instances = [];
imageIds.forEach(imageId => {
const instance = metaData.get('instance', imageId) || {};
instances.push(instance);
});
return instances;
}
function _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService) {
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
if (!instanceNumber && instanceNumber !== 0) {
return null;
}
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportId,
cornerstoneViewportService
);
switch (viewportData.viewportType) {
case Enums.ViewportType.STACK:
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
break;
case Enums.ViewportType.ORTHOGRAPHIC:
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
viewportId,
cornerstoneViewportService,
imageIndex
);
break;
}
return instanceNumber;
}
return instanceNumber ?? null;
};
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.data.imageIds;
@ -403,15 +380,20 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
// Since volume viewports can be in any view direction, they can render
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewportService) {
const volumes = viewportData.volumes;
function _getInstanceNumberFromVolume(
viewportData,
viewportId,
cornerstoneViewportService,
imageIndex
) {
const volumes = viewportData.data;
// Todo: support fusion of acquisition plane which has instanceNumber
if (!volumes || volumes.length > 1) {
if (!volumes) {
return;
}
const volume = volumes[0];
// Todo: support fusion of acquisition plane which has instanceNumber
const { volume } = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
@ -442,6 +424,72 @@ function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewp
}
}
/**
* Window Level / Center Overlay item
*/
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi;
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div
className="overlay-item flex flex-row"
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>
<span className="mr-1 shrink-0">L:</span>
<span className="ml-1 shrink-0">{windowCenter.toFixed(0)}</span>
</div>
);
}
/**
* Zoom Level Overlay item
*/
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span>{scale.toFixed(2)}x</span>
</div>
);
}
/**
* Instance Number Overlay Item
*/
function InstanceNumberOverlayItem({
instanceNumber,
imageSliceData,
customization,
}: OverlayItemProps) {
const { imageIndex, numberOfSlices } = imageSliceData;
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span>
{instanceNumber !== undefined && instanceNumber !== null ? (
<>
<span className="mr-1 shrink-0">I:</span>
<span>{`${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`}</span>
</>
) : (
`${imageIndex + 1}/${numberOfSlices}`
)}
</span>
</div>
);
}
CustomizableViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,

View File

@ -23,7 +23,7 @@ function CornerstoneImageScrollbar({
if (isCineEnabled) {
// on image scrollbar change, stop the CINE if it is playing
cineService.stopClip(element);
cineService.stopClip(element, { viewportId });
cineService.setCine({ id: viewportId, isPlaying: false });
}

View File

@ -108,17 +108,15 @@ function ViewportOrientationMarkers({
console.log('ViewportOrientationMarkers::No viewport');
return null;
}
const backgroundColor = ohifViewport.getViewportOptions().background;
// Todo: probably this can be done in a better way in which we identify bright
// background
const isLight = backgroundColor ? csUtils.isEqual(backgroundColor, [1, 1, 1]) : false;
return orientationMarkers.map((m, index) => (
<div
className={classNames(
'overlay-text',
`${m}-mid orientation-marker`,
isLight ? 'text-[#726F7E]' : 'text-[#ccc]'
'text-aqua-pale',
'text-[13px]',
'leading-5'
)}
key={`${m}-mid orientation-marker`}
>
@ -135,7 +133,7 @@ function ViewportOrientationMarkers({
element,
]);
return <div className="ViewportOrientationMarkers noselect">{markers}</div>;
return <div className="ViewportOrientationMarkers select-none">{markers}</div>;
}
ViewportOrientationMarkers.propTypes = {

View File

@ -1,278 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui';
import { ServicesManager } from '@ohif/core';
const EPSILON = 1e-4;
function CornerstoneViewportOverlay({
element,
viewportData,
imageSliceData,
viewportId,
servicesManager,
}) {
const { cornerstoneViewportService, toolbarService } = servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(toolbarService.getActiveTools());
}, []);
useEffect(() => {
let isMounted = true;
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
if (!isMounted) {
return;
}
setActiveTools(toolbarService.getActiveTools());
}
);
return () => {
isMounted = false;
unsubscribe();
};
}, []);
/**
* Updating the VOI when the viewport changes its voi
*/
useEffect(() => {
const updateVOI = eventDetail => {
const { range } = eventDetail.detail;
if (!range) {
return;
}
const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
setVOI({ windowCenter, windowWidth });
};
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
};
}, [viewportId, viewportData, voi, element]);
/**
* Updating the scale when the viewport changes its zoom
*/
useEffect(() => {
const updateScale = eventDetail => {
const { previousCamera, camera } = eventDetail.detail;
if (
previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale
) {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
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);
}
};
element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
};
}, [viewportId, viewportData]);
const getTopLeftContent = useCallback(() => {
const { windowWidth, windowCenter } = voi;
if (activeTools.includes('WindowLevel')) {
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div className="flex flex-row text-base">
<span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1">L:</span>
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}
if (activeTools.includes('Zoom')) {
return (
<div className="flex flex-row text-base">
<span className="mr-1">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
return null;
}, [voi, scale, activeTools]);
const getTopRightContent = useCallback(() => {
const { imageIndex, numberOfSlices } = imageSliceData;
if (!viewportData) {
return;
}
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
if (!instanceNumber) {
return null;
}
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportId,
cornerstoneViewportService
);
}
return (
<div className="flex flex-row text-base">
<span className="mr-1">I:</span>
<span className="font-light">
{instanceNumber !== undefined
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}, [imageSliceData, viewportData, viewportId]);
if (!viewportData) {
return null;
}
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
if (!ohifViewport) {
return null;
}
const backgroundColor = ohifViewport.getViewportOptions().background;
// Todo: probably this can be done in a better way in which we identify bright
// background
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
color={isLight && 'text-[#0944B3]'}
/>
);
}
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.data.imageIds;
const imageId = imageIds[imageIndex];
if (!imageId) {
return;
}
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
const stackSize = imageIds.length;
if (stackSize <= 1) {
return;
}
return parseInt(instanceNumber);
}
// Since volume viewports can be in any view direction, they can render
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
function _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportId,
cornerstoneViewportService
) {
const volumes = viewportData.volumes;
// Todo: support fusion of acquisition plane which has instanceNumber
if (!volumes || volumes.length > 1) {
return;
}
const volume = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!cornerstoneViewport) {
return;
}
const camera = cornerstoneViewport.getCamera();
const { viewPlaneNormal } = camera;
// checking if camera is looking at the acquisition plane (defined by the direction on the volume)
const scanAxisNormal = direction.slice(6, 9);
// check if viewPlaneNormal is parallel to scanAxisNormal
const cross = vec3.cross(vec3.create(), viewPlaneNormal, scanAxisNormal);
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
if (isAcquisitionPlane) {
const imageId = imageIds[imageIndex];
if (!imageId) {
return {};
}
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
return parseInt(instanceNumber);
}
}
CornerstoneViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
viewportId: PropTypes.string,
servicesManager: PropTypes.instanceOf(ServicesManager),
};
export default CornerstoneViewportOverlay;

View File

@ -3,6 +3,8 @@ import {
StackViewport,
VolumeViewport,
utilities as csUtils,
Types as CoreTypes,
BaseVolumeViewport,
} from '@cornerstonejs/core';
import {
ToolGroupManager,
@ -11,13 +13,20 @@ import {
ReferenceLinesTool,
} from '@cornerstonejs/tools';
import { Types as OhifTypes } from '@ohif/core';
import { vec3, mat4 } from 'gl-matrix';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import callInputDialog from './utils/callInputDialog';
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/callInputDialog';
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { CornerstoneServices } from './types';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
voi: toggleVOISliceSync,
};
function commandsModule({
servicesManager,
@ -27,11 +36,14 @@ function commandsModule({
viewportGridService,
toolGroupService,
cineService,
toolbarService,
uiDialogService,
cornerstoneViewportService,
uiNotificationService,
measurementService,
customizationService,
colorbarService,
hangingProtocolService,
syncGroupService,
} = servicesManager.services as CornerstoneServices;
const { measurementServiceSource } = this;
@ -39,6 +51,12 @@ function commandsModule({
function _getActiveViewportEnabledElement() {
return getActiveViewportEnabledElement(viewportGridService);
}
function _getActiveViewportToolGroupId() {
const viewport = _getActiveViewportEnabledElement();
return toolGroupService.getToolGroupForViewport(viewport.id);
}
const actions = {
/**
* Generates the selector props for the context menu, specific to
@ -114,38 +132,29 @@ function commandsModule({
? nearbyToolData
: null;
},
// Measurement tool commands:
/** Delete the given measurement */
deleteMeasurement: ({ uid }) => {
if (uid) {
measurementServiceSource.remove(uid);
}
},
/**
* Show the measurement labelling input dialog and update the label
* on the measurement with a response if not cancelled.
*/
setMeasurementLabel: ({ uid }) => {
const labelConfig = customizationService.get('measurementLabels');
const measurement = measurementService.getMeasurement(uid);
callInputDialog(
uiDialogService,
measurement,
(label, actionId) => {
if (actionId === 'cancel') {
return;
}
const updatedMeasurement = Object.assign({}, measurement, {
label,
});
measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
},
false
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(
(val: Map<any, any>) => {
measurementService.update(
uid,
{
...val,
},
true
);
}
);
},
@ -221,47 +230,22 @@ function commandsModule({
viewportGridService.setActiveViewportId(viewportId);
},
arrowTextCallback: ({ callback, data }) => {
callInputDialog(uiDialogService, data, callback);
},
cleanUpCrosshairs: () => {
// if the crosshairs tool is active, deactivate it and set window level active
// since we are going back to main non-mpr HP
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
if (activeViewportToolGroup._toolInstances?.Crosshairs?.mode === Enums.ToolModes.Active) {
actions.toolbarServiceRecordInteraction({
interactionType: 'tool',
commands: [
{
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
}
arrowTextCallback: ({ callback, data, uid }) => {
const labelConfig = customizationService.get('measurementLabels');
callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig);
},
toggleCine: () => {
const { viewports } = viewportGridService.getState();
const { isCineEnabled } = cineService.getState();
cineService.setIsCineEnabled(!isCineEnabled);
toolbarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
},
setWindowLevel({ window, level, toolGroupId }) {
setViewportWindowLevel({ viewportId, window, level }) {
// convert to numbers
const windowWidthNum = Number(window);
const windowCenterNum = Number(level);
const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return;
}
// get actor from the viewport
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewport = renderingEngine.getViewport(viewportId);
@ -277,34 +261,102 @@ function commandsModule({
viewport.render();
},
// Just call the toolbar service record interaction - allows
// executing a toolbar command as a full toolbar command with side affects
// coming from the ToolbarService itself.
toolbarServiceRecordInteraction: props => {
toolbarService.recordInteraction(props);
toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => {
const hasColorbar = colorbarService.hasColorbar(viewportId);
if (hasColorbar) {
colorbarService.removeColorbar(viewportId);
return;
}
colorbarService.addColorbar(viewportId, displaySetInstanceUIDs, options);
},
// Enable or disable a toggleable command, without calling the activation
// Used to setup already active tools from hanging protocols
setToolbarToggled: props => {
toolbarService.setToggled(props.toolId, props.isActive ?? true);
},
setToolActive: ({ toolName, toolGroupId = null, toggledState }) => {
if (toolName === 'Crosshairs') {
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
uiNotificationService.show({
title: 'Crosshairs',
message:
'You need to be in a MPR view to use Crosshairs. Click on MPR button in the toolbar to activate it.',
type: 'info',
duration: 3000,
});
setWindowLevel(props) {
const { toolGroupId } = props;
const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
throw new Error('Crosshairs tool is not available in this viewport');
}
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return;
}
actions.setViewportWindowLevel({ ...props, viewportId });
},
setToolEnabled: ({ toolName, toggle, toolGroupId }) => {
const { viewports } = viewportGridService.getState();
if (!viewports.size) {
return;
}
const toolGroup = toolGroupService.getToolGroup(toolGroupId ?? null);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return;
}
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
// Toggle the tool's state only if the toggle is true
if (toggle) {
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
} else {
toolGroup.setToolEnabled(toolName);
}
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
toggleEnabledDisabledToolbar({ value, itemId, toolGroupId }) {
const toolName = itemId || value;
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId();
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return;
}
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
},
toggleActiveDisabledToolbar({ value, itemId, toolGroupId }) {
const toolName = itemId || value;
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId();
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return;
}
const toolIsActive = [
Enums.ToolModes.Active,
Enums.ToolModes.Enabled,
Enums.ToolModes.Passive,
].includes(toolGroup.getToolOptions(toolName).mode);
toolIsActive
? toolGroup.setToolDisabled(toolName)
: actions.setToolActive({ toolName, toolGroupId });
// we should set the previously active tool to active after we set the
// current tool disabled
if (toolIsActive) {
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
if (prevToolName !== toolName) {
actions.setToolActive({ toolName: prevToolName, toolGroupId });
}
}
},
setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [] }) => {
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
toolName = toolName || itemId || value;
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
toolGroupIds.forEach(toolGroupId => {
actions.setToolActive({ toolName, toolGroupId });
});
},
setToolActive: ({ toolName, toolGroupId = null }) => {
const { viewports } = viewportGridService.getState();
if (!viewports.size) {
@ -317,34 +369,17 @@ function commandsModule({
return;
}
if (!toolGroup.getToolInstance(toolName)) {
uiNotificationService.show({
title: `${toolName} tool`,
message: `The ${toolName} tool is not available in this viewport.`,
type: 'info',
duration: 3000,
});
throw new Error(`ToolGroup ${toolGroup.id} does not have this tool.`);
if (!toolGroup.hasTool(toolName)) {
return;
}
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
if (activeToolName) {
// Todo: this is a hack to prevent the crosshairs to stick around
// after another tool is selected. We should find a better way to do this
if (activeToolName === 'Crosshairs') {
toolGroup.setToolDisabled(activeToolName);
} else {
toolGroup.setToolPassive(activeToolName);
}
}
// If there is a toggle state, then simply set the enabled/disabled state without
// setting the tool active.
if (toggledState != null) {
toggledState ? toolGroup.setToolEnabled(toolName) : toolGroup.setToolDisabled(toolName);
return;
const activeToolOptions = toolGroup.getToolConfiguration(activeToolName);
activeToolOptions?.disableOnPassive
? toolGroup.setToolDisabled(activeToolName)
: toolGroup.setToolPassive(activeToolName);
}
// Set the new toolName to be active
@ -380,6 +415,7 @@ function commandsModule({
onClose: uiModalService.hide,
cornerstoneViewportService,
},
containerDimensions: 'w-[70%] max-w-[900px]',
});
}
},
@ -391,8 +427,16 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { rotation: currentRotation } = viewport.getProperties();
if (viewport instanceof BaseVolumeViewport) {
const camera = viewport.getCamera();
const rotAngle = (rotation * Math.PI) / 180;
const rotMat = mat4.identity(new Float32Array(16));
mat4.rotate(rotMat, rotMat, rotAngle, camera.viewPlaneNormal);
const rotatedViewUp = vec3.transformMat4(vec3.create(), camera.viewUp, rotMat);
viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 });
viewport.render();
} else if (viewport.getRotation !== undefined) {
const currentRotation = viewport.getRotation();
const newRotation = (currentRotation + rotation) % 360;
viewport.setProperties({ rotation: newRotation });
viewport.render();
@ -407,11 +451,9 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { flipHorizontal } = viewport.getCamera();
viewport.setCamera({ flipHorizontal: !flipHorizontal });
viewport.render();
}
const { flipHorizontal } = viewport.getCamera();
viewport.setCamera({ flipHorizontal: !flipHorizontal });
viewport.render();
},
flipViewportVertical: () => {
const enabledElement = _getActiveViewportEnabledElement();
@ -422,11 +464,9 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { flipVertical } = viewport.getCamera();
viewport.setCamera({ flipVertical: !flipVertical });
viewport.render();
}
const { flipVertical } = viewport.getCamera();
viewport.setCamera({ flipVertical: !flipVertical });
viewport.render();
},
invertViewport: ({ element }) => {
let enabledElement;
@ -456,13 +496,8 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
viewport.resetProperties();
viewport.resetCamera();
} else {
viewport.resetProperties();
viewport.resetCamera();
}
viewport.resetProperties?.();
viewport.resetCamera();
viewport.render();
},
@ -534,18 +569,51 @@ function commandsModule({
cstUtils.scroll(viewport, options);
},
setViewportColormap: ({ viewportId, displaySetInstanceUID, colormap, immediate = false }) => {
setViewportColormap: ({
viewportId,
displaySetInstanceUID,
colormap,
opacity = 1,
immediate = false,
}) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const actorEntries = viewport.getActors();
let hpOpacity;
// Retrieve active protocol's viewport match details
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
// Get display set options for the specified viewport ID
const displaySetsInfo = viewportMatchDetails.get(viewportId)?.displaySetsInfo;
const actorEntry = actorEntries.find(actorEntry => {
return actorEntry.uid.includes(displaySetInstanceUID);
});
if (displaySetsInfo) {
// Find the display set that matches the given UID
const matchingDisplaySet = displaySetsInfo.find(
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID
);
// If a matching display set is found, update the opacity with its value
hpOpacity = matchingDisplaySet?.displaySetOptions?.options?.colormap?.opacity;
}
const { actor: volumeActor, uid: volumeId } = actorEntry;
// HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || opacity };
viewport.setProperties({ colormap, volumeActor }, volumeId);
const setViewportProperties = (viewport, uid) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
const { actor: volumeActor, uid: volumeId } = actorEntry;
viewport.setProperties({ colormap, volumeActor }, volumeId);
};
if (viewport instanceof StackViewport) {
setViewportProperties(viewport, viewportId);
}
if (viewport instanceof VolumeViewport) {
if (!displaySetInstanceUID) {
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
}
setViewportProperties(viewport, displaySetInstanceUID);
}
if (immediate) {
viewport.render();
@ -559,32 +627,207 @@ function commandsModule({
(currentIndex + direction + viewportIds.length) % viewportIds.length;
viewportGridService.setActiveViewportId(viewportIds[nextViewportIndex] as string);
},
/**
* If the syncId is given and a synchronizer with that ID already exists, it will
* toggle it on/off for the provided viewports. If not, it will attempt to create
* a new synchronizer using the given syncId and type for the specified viewports.
* If no viewports are provided, you may notice some default behavior.
* - 'voi' type, we will aim to synchronize all viewports with the same modality
* -'imageSlice' type, we will aim to synchronize all viewports with the same orientation.
*
* @param options
* @param options.viewports - The viewports to synchronize
* @param options.syncId - The synchronization group ID
* @param options.type - The type of synchronization to perform
*/
toggleSynchronizer: ({ type, viewports, syncId }) => {
const synchronizer = syncGroupService.getSynchronizer(syncId);
toggleStackImageSync: ({ toggledState }) => {
toggleStackImageSync({
servicesManager,
toggledState,
});
if (synchronizer) {
synchronizer.isDisabled() ? synchronizer.setEnabled(true) : synchronizer.setEnabled(false);
return;
}
const fn = toggleSyncFunctions[type];
if (fn) {
fn({
servicesManager,
viewports,
syncId,
});
}
},
setSourceViewportForReferenceLinesTool: ({ toggledState, viewportId }) => {
setSourceViewportForReferenceLinesTool: ({ viewportId }) => {
if (!viewportId) {
const { activeViewportId } = viewportGridService.getState();
viewportId = activeViewportId;
viewportId = activeViewportId ?? 'default';
}
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
toolGroup.setToolConfiguration(
toolGroup?.setToolConfiguration(
ReferenceLinesTool.toolName,
{
sourceViewportId: viewportId,
},
true // overwrite
);
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
storePresentation: ({ viewportId }) => {
cornerstoneViewportService.storePresentation({ viewportId });
},
updateVolumeData: ({ volume }) => {
// update vtkOpenGLTexture and imageData of computed volume
const { imageData, vtkOpenGLTexture } = volume;
const numSlices = imageData.getDimensions()[2];
const slicesToUpdate = [...Array(numSlices).keys()];
slicesToUpdate.forEach(i => {
vtkOpenGLTexture.setUpdatedFrame(i);
});
imageData.modified();
},
attachProtocolViewportDataListener: ({ protocol, stageIndex }) => {
const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const command = protocol.callbacks.onViewportDataInitialized;
const numPanes = protocol.stages?.[stageIndex]?.viewports.length ?? 1;
let numPanesWithData = 0;
const { unsubscribe } = cornerstoneViewportService.subscribe(EVENT, evt => {
numPanesWithData++;
if (numPanesWithData === numPanes) {
commandsManager.run(...command);
// Unsubscribe from the event
unsubscribe(EVENT);
}
});
},
setViewportPreset: ({ viewportId, preset }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
viewport.setProperties({
preset,
});
viewport.render();
},
/**
* Sets the volume quality for a given viewport.
* @param {string} viewportId - The ID of the viewport to set the volume quality.
* @param {number} volumeQuality - The desired quality level of the volume rendering.
*/
setVolumeRenderingQulaity: ({ viewportId, volumeQuality }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const mapper = actor.getMapper();
const image = mapper.getInputData();
const dims = image.getDimensions();
const spacing = image.getSpacing();
const spatialDiagonal = vec3.length(
vec3.fromValues(dims[0] * spacing[0], dims[1] * spacing[1], dims[2] * spacing[2])
);
let sampleDistance = spacing.reduce((a, b) => a + b) / 3.0;
sampleDistance /= volumeQuality > 1 ? 0.5 * volumeQuality ** 2 : 1.0;
const samplesPerRay = spatialDiagonal / sampleDistance + 1;
mapper.setMaximumSamplesPerRay(samplesPerRay);
mapper.setSampleDistance(sampleDistance);
viewport.render();
},
/**
* Shifts opacity points for a given viewport id.
* @param {string} viewportId - The ID of the viewport to set the mapping range.
* @param {number} shift - The shift value to shift the points by.
*/
shiftVolumeOpacityPoints: ({ viewportId, shift }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const ofun = actor.getProperty().getScalarOpacity(0);
const opacityPointValues = []; // Array to hold values
// Gather Existing Values
const size = ofun.getSize();
for (let pointIdx = 0; pointIdx < size; pointIdx++) {
const opacityPointValue = [0, 0, 0, 0];
ofun.getNodeValue(pointIdx, opacityPointValue);
// opacityPointValue now holds [xLocation, opacity, midpoint, sharpness]
opacityPointValues.push(opacityPointValue);
}
// Add offset
opacityPointValues.forEach(opacityPointValue => {
opacityPointValue[0] += shift; // Change the location value
});
// Set new values
ofun.removeAllPoints();
opacityPointValues.forEach(opacityPointValue => {
ofun.addPoint(...opacityPointValue);
});
viewport.render();
},
/**
* Sets the volume lighting settings for a given viewport.
* @param {string} viewportId - The ID of the viewport to set the lighting settings.
* @param {Object} options - The lighting settings to be set.
* @param {boolean} options.shade - The shade setting for the lighting.
* @param {number} options.ambient - The ambient setting for the lighting.
* @param {number} options.diffuse - The diffuse setting for the lighting.
* @param {number} options.specular - The specular setting for the lighting.
**/
setVolumeLighting: ({ viewportId, options }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const property = actor.getProperty();
if (options.shade !== undefined) {
property.setShade(options.shade);
}
if (options.ambient !== undefined) {
property.setAmbient(options.ambient);
}
if (options.diffuse !== undefined) {
property.setDiffuse(options.diffuse);
}
if (options.specular !== undefined) {
property.setSpecular(options.specular);
}
viewport.render();
},
resetCrosshairs: ({ viewportId }) => {
const crosshairInstances = [];
const getCrosshairInstances = toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
crosshairInstances.push(toolGroup.getToolInstance('Crosshairs'));
};
if (!viewportId) {
const toolGroupIds = toolGroupService.getToolGroupIds();
toolGroupIds.forEach(getCrosshairInstances);
} else {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
getCrosshairInstances(toolGroup.id);
}
crosshairInstances.forEach(ins => {
ins?.resetCrosshairs();
});
},
};
const definitions = {
@ -592,7 +835,6 @@ function commandsModule({
// context menu
showCornerstoneContextMenu: {
commandFn: actions.showCornerstoneContextMenu,
storeContexts: [],
options: {
menuCustomizationId: 'measurementsContextMenu',
commands: [
@ -611,7 +853,9 @@ function commandsModule({
storeContexts: [],
options: {},
},
toggleViewportColorbar: {
commandFn: actions.toggleViewportColorbar,
},
deleteMeasurement: {
commandFn: actions.deleteMeasurement,
},
@ -621,16 +865,21 @@ function commandsModule({
updateMeasurement: {
commandFn: actions.updateMeasurement,
},
setViewportWindowLevel: {
commandFn: actions.setViewportWindowLevel,
},
setWindowLevel: {
commandFn: actions.setWindowLevel,
},
toolbarServiceRecordInteraction: {
commandFn: actions.toolbarServiceRecordInteraction,
},
setToolActive: {
commandFn: actions.setToolActive,
},
setToolActiveToolbar: {
commandFn: actions.setToolActiveToolbar,
},
setToolEnabled: {
commandFn: actions.setToolEnabled,
},
rotateViewportCW: {
commandFn: actions.rotateViewport,
options: { rotation: 90 },
@ -704,20 +953,41 @@ function commandsModule({
setViewportColormap: {
commandFn: actions.setViewportColormap,
},
toggleStackImageSync: {
commandFn: actions.toggleStackImageSync,
},
setSourceViewportForReferenceLinesTool: {
commandFn: actions.setSourceViewportForReferenceLinesTool,
},
storePresentation: {
commandFn: actions.storePresentation,
},
setToolbarToggled: {
commandFn: actions.setToolbarToggled,
attachProtocolViewportDataListener: {
commandFn: actions.attachProtocolViewportDataListener,
},
cleanUpCrosshairs: {
commandFn: actions.cleanUpCrosshairs,
setViewportPreset: {
commandFn: actions.setViewportPreset,
},
setVolumeRenderingQulaity: {
commandFn: actions.setVolumeRenderingQulaity,
},
shiftVolumeOpacityPoints: {
commandFn: actions.shiftVolumeOpacityPoints,
},
setVolumeLighting: {
commandFn: actions.setVolumeLighting,
},
resetCrosshairs: {
commandFn: actions.resetCrosshairs,
},
toggleSynchronizer: {
commandFn: actions.toggleSynchronizer,
},
updateVolumeData: {
commandFn: actions.updateVolumeData,
},
toggleEnabledDisabledToolbar: {
commandFn: actions.toggleEnabledDisabledToolbar,
},
toggleActiveDisabledToolbar: {
commandFn: actions.toggleActiveDisabledToolbar,
},
};

View File

@ -0,0 +1,31 @@
import React, { ReactElement } from 'react';
import PropTypes from 'prop-types';
import { ServicesManager } from '@ohif/core';
import { useViewportGrid } from '@ohif/ui';
import ViewportWindowLevel from '../ViewportWindowLevel/ViewportWindowLevel';
const ActiveViewportWindowLevel = ({
servicesManager,
}: {
servicesManager: ServicesManager;
}): ReactElement => {
const [viewportGrid] = useViewportGrid();
const { activeViewportId } = viewportGrid;
return (
<>
{activeViewportId && (
<ViewportWindowLevel
servicesManager={servicesManager}
viewportId={activeViewportId}
/>
)}
</>
);
};
ActiveViewportWindowLevel.propTypes = {
servicesManager: PropTypes.instanceOf(ServicesManager),
};
export default ActiveViewportWindowLevel;

View File

@ -0,0 +1 @@
export { default } from './ActiveViewportWindowLevel';

View File

@ -1,96 +1,237 @@
import React, { useEffect } from 'react';
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
import { Enums, eventTarget } from '@cornerstonejs/core';
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { CinePlayer, useCine } from '@ohif/ui';
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
import { useAppConfig } from '@state';
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
const { toolbarService, customizationService } = servicesManager.services;
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
const [{ isCineEnabled, cines }, cineService] = useCine();
const [{ activeViewportId }] = useViewportGrid();
const { component: CinePlayerComponent = CinePlayer } =
customizationService.get('cinePlayer') ?? {};
const handleCineClose = () => {
toolbarService.recordInteraction({
groupId: 'MoreTools',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleCine',
commandOptions: {},
toolName: 'cine',
context: 'CORNERSTONE',
},
],
});
};
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
const [dynamicInfo, setDynamicInfo] = useState(null);
const [appConfig] = useAppConfig();
const isMountedRef = useRef(null);
const cineHandler = () => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
if (!cines?.[viewportId] || !enabledVPElement) {
return;
}
const cine = cines[viewportId];
const isPlaying = cine.isPlaying || false;
const frameRate = cine.frameRate || 24;
const { isPlaying = false, frameRate = 24 } = cines[viewportId];
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(enabledVPElement);
}
return isPlaying
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate, viewportId })
: cineService.stopClip(enabledVPElement);
};
const newDisplaySetHandler = useCallback(() => {
if (!enabledVPElement || !isCineEnabled) {
return;
}
const { viewports } = viewportGridService.getState();
const { displaySetInstanceUIDs } = viewports.get(viewportId);
let frameRate = 24;
let isPlaying = cines[viewportId]?.isPlaying || false;
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (displaySet.FrameRate) {
// displaySet.FrameRate corresponds to DICOM tag (0018,1063) which is defined as the the frame time in milliseconds
// So a bit of math to get the actual frame rate.
frameRate = Math.round(1000 / displaySet.FrameRate);
isPlaying ||= !!appConfig.autoPlayCine;
}
// check if the displaySet is dynamic and set the dynamic info
if (displaySet.isDynamicVolume) {
const { dynamicVolumeInfo } = displaySet;
const numTimePoints = dynamicVolumeInfo.timePoints.length;
const label = dynamicVolumeInfo.splittingTag;
const timePointIndex = dynamicVolumeInfo.timePointIndex || 0;
setDynamicInfo({
volumeId: displaySet.displaySetInstanceUID,
timePointIndex,
numTimePoints,
label,
});
} else {
setDynamicInfo(null);
}
});
if (isPlaying) {
cineService.setIsCineEnabled(isPlaying);
}
cineService.setCine({ id: viewportId, isPlaying, frameRate });
setNewStackFrameRate(frameRate);
}, [displaySetService, viewportId, viewportGridService, cines, isCineEnabled, enabledVPElement]);
useEffect(() => {
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, cineHandler);
isMountedRef.current = true;
newDisplaySetHandler();
return () => {
isMountedRef.current = false;
};
}, [isCineEnabled, newDisplaySetHandler]);
useEffect(() => {
if (!isCineEnabled) {
return;
}
cineHandler();
}, [isCineEnabled, cineHandler, enabledVPElement]);
/**
* Use effect for handling new display set
*/
useEffect(() => {
if (!enabledVPElement) {
return;
}
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
// this doesn't makes sense that we are listening to this event on viewport element
enabledVPElement.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
newDisplaySetHandler
);
return () => {
cineService.setCine({ id: viewportId, isPlaying: false });
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, cineHandler);
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
enabledVPElement.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
newDisplaySetHandler
);
};
}, [enabledVPElement]);
}, [enabledVPElement, newDisplaySetHandler, viewportId]);
useEffect(() => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
return;
}
cineHandler();
return () => {
if (enabledVPElement && cines?.[viewportId]?.isPlaying) {
cineService.stopClip(enabledVPElement);
}
cineService.stopClip(enabledVPElement, { viewportId });
};
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
if (!isCineEnabled) {
return null;
}
const cine = cines[viewportId];
const isPlaying = (cine && cine.isPlaying) || false;
const isPlaying = cine?.isPlaying || false;
return (
isCineEnabled && (
<CinePlayerComponent
className="absolute left-1/2 bottom-3 -translate-x-1/2"
isPlaying={isPlaying}
onClose={handleCineClose}
onPlayPauseChange={isPlaying =>
cineService.setCine({
id: activeViewportId,
isPlaying,
})
}
onFrameRateChange={frameRate =>
cineService.setCine({
id: activeViewportId,
frameRate,
})
}
/>
)
<RenderCinePlayer
viewportId={viewportId}
cineService={cineService}
newStackFrameRate={newStackFrameRate}
isPlaying={isPlaying}
dynamicInfo={dynamicInfo}
customizationService={customizationService}
/>
);
}
function RenderCinePlayer({
viewportId,
cineService,
newStackFrameRate,
isPlaying,
dynamicInfo: dynamicInfoProp,
customizationService,
}) {
const { component: CinePlayerComponent = CinePlayer } =
customizationService.get('cinePlayer') ?? {};
const [dynamicInfo, setDynamicInfo] = useState(dynamicInfoProp);
useEffect(() => {
setDynamicInfo(dynamicInfoProp);
}, [dynamicInfoProp]);
/**
* Use effect for handling 4D time index changed
*/
useEffect(() => {
if (!dynamicInfo) {
return;
}
const handleTimePointIndexChange = evt => {
const { volumeId, timePointIndex, numTimePoints, splittingTag } = evt.detail;
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
};
eventTarget.addEventListener(
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange
);
return () => {
eventTarget.removeEventListener(
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange
);
};
}, [dynamicInfo]);
useEffect(() => {
if (!dynamicInfo) {
return;
}
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
const volume = cache.getVolume(volumeId);
volume.timePointIndex = timePointIndex;
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
}, []);
const updateDynamicInfo = useCallback(props => {
const { volumeId, timePointIndex } = props;
const volume = cache.getVolume(volumeId);
volume.timePointIndex = timePointIndex;
}, []);
return (
<CinePlayerComponent
className="absolute left-1/2 bottom-3 -translate-x-1/2"
frameRate={newStackFrameRate}
isPlaying={isPlaying}
onClose={() => {
// also stop the clip
cineService.setCine({
id: viewportId,
isPlaying: false,
});
cineService.setIsCineEnabled(false);
}}
onPlayPauseChange={isPlaying => {
cineService.setCine({
id: viewportId,
isPlaying,
});
}}
onFrameRateChange={frameRate =>
cineService.setCine({
id: viewportId,
frameRate,
})
}
dynamicInfo={dynamicInfo}
updateDynamicInfo={updateDynamicInfo}
/>
);
}

View File

@ -0,0 +1,23 @@
import React from 'react';
import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
import { ViewportActionCorners } from '@ohif/ui';
export type OHIFViewportActionCornersProps = {
viewportId: string;
};
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
const [viewportActionCornersState] = useViewportActionCornersContext();
if (!viewportActionCornersState[viewportId]) {
return null;
}
return (
<ViewportActionCorners
cornerComponents={viewportActionCornersState[viewportId]}
></ViewportActionCorners>
);
}
export default OHIFViewportActionCorners;

View File

@ -0,0 +1,387 @@
import React, { useEffect, useCallback, useState, ReactElement } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import { ServicesManager } from '@ohif/core';
import { PanelSection, WindowLevel } from '@ohif/ui';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core';
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
const { Events } = Enums;
const ViewportWindowLevel = ({
servicesManager,
viewportId,
}: {
servicesManager: ServicesManager;
viewportId: string;
}): ReactElement => {
const { cornerstoneViewportService } = servicesManager.services;
const [windowLevels, setWindowLevels] = useState([]);
const [cachedHistograms, setCachedHistograms] = useState({});
/**
* Looks for all viewports that has exactly all volumeIds passed as parameter.
*/
const getViewportsWithVolumeIds = useCallback(
(volumeIds: string[]) => {
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewports = renderingEngine.getVolumeViewports();
return viewports.filter(vp => {
const viewportVolumeIds = vp.getActors().map(actor => actor.uid);
return (
volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
);
});
},
[cornerstoneViewportService]
);
const getNodeOpacity = (volumeActor, nodeIndex) => {
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
const nodeValue = [];
volumeOpacity.getNodeValue(nodeIndex, nodeValue);
return nodeValue[1];
};
/**
* Checks if the opacity applied to the PET volume is something like
* [{x: 0, y: 0}, {x: 0.1, y: [C]}, {x: [ANY], y: [C]}] where C is a
* constant opacity value for all x's greater than 0.1
*/
const isPetVolumeWithDefaultOpacity = (volumeId, volumeActor) => {
const volume = cs3DCache.getVolume(volumeId);
if (!volume) {
return false;
}
const modality = volume.metadata.Modality;
if (modality !== 'PT') {
return false;
}
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
// It must have at least two points (0 and 0.1)
if (volumeOpacity.getSize() < 2) {
return false;
}
const node1Value = [];
const node2Value = [];
volumeOpacity.getNodeValue(0, node1Value);
volumeOpacity.getNodeValue(1, node2Value);
// First node must be (x:0, y:0} and the second one {x:0.1, y:any}
if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
return false;
}
const expectedOpacity = node2Value[1];
const opacitySize = volumeOpacity.getSize();
const currentNodeValue = [];
// Any point after 0.1 must have the same opacity
for (let i = 2; i < opacitySize; i++) {
volumeOpacity.getNodeValue(i, currentNodeValue);
if (currentNodeValue[1] !== expectedOpacity) {
return false;
}
}
return true;
};
/**
* Checks if the opacity function has a constance opacity value for all x's
*/
const isVolumeWithConstantOpacity = volumeActor => {
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
const opacitySize = volumeOpacity.getSize();
const firstNodeValue = [];
volumeOpacity.getNodeValue(0, firstNodeValue);
const firstNodeOpacity = firstNodeValue[1];
for (let i = 0; i < opacitySize; i++) {
const currentNodeValue = [];
volumeOpacity.getNodeValue(0, currentNodeValue);
if (currentNodeValue[1] !== firstNodeOpacity) {
return false;
}
}
return true;
};
const getVolumeOpacity = useCallback((viewport, volumeId) => {
const volumeActor = viewport.getActor(volumeId).actor;
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
// Get the opacity from the second node at 0.1
return getNodeOpacity(volumeActor, 1);
} else if (isVolumeWithConstantOpacity(volumeActor)) {
return getNodeOpacity(volumeActor, 0);
}
return undefined;
}, []);
const getWindowLevelsData = useCallback(
async (viewportId: number) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return [];
}
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const volumeIds = viewport.getActors().map(actor => actor.uid);
const viewportProperties = viewport.getProperties();
const { voiRange } = viewportProperties;
const viewportVoi = voiRange
? {
windowWidth: voiRange.upper - voiRange.lower,
windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2,
}
: undefined;
const windowLevels = await Promise.all(
volumeIds.map(async (volumeId, volumeIndex) => {
const volume = cs3DCache.getVolume(volumeId);
if (!volume) {
return null;
}
const opacity = getVolumeOpacity(viewport, volumeId);
const { metadata, scaling } = volume;
const modality = metadata.Modality;
// TODO: find a proper way to fix the histogram
const options = {
min: modality === 'PT' ? 0.1 : -999,
max: modality === 'PT' ? 5 : 10000,
};
const histogram =
cachedHistograms[volumeId] ??
(await getViewportVolumeHistogram(viewport, volume, options));
const { voi: displaySetVOI, colormap: displaySetColormap } =
viewportInfo.displaySetOptions[volumeIndex];
let colormap;
if (displaySetColormap) {
colormap =
csUtils.colormap.getColormap(displaySetColormap.name) ??
vtkColorMaps.getPresetByName(displaySetColormap.name);
}
const voi = !volumeIndex ? viewportVoi ?? displaySetVOI : displaySetVOI;
return {
viewportId,
modality,
volumeId,
volumeIndex,
voi,
histogram,
colormap,
step: scaling?.PT ? 0.05 : 1,
opacity,
showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
};
})
);
const data = windowLevels.filter(Boolean);
return data;
},
[cachedHistograms, cornerstoneViewportService, getVolumeOpacity]
);
const updateViewportHistograms = useCallback(() => {
getWindowLevelsData(viewportId).then(windowLevels => {
setWindowLevels(windowLevels);
});
}, [viewportId, getWindowLevelsData]);
const handleCornerstoneVOIModified = useCallback(
e => {
const { detail } = e;
const { volumeId, range } = detail;
const oldWindowLevel = windowLevels.find(wl => wl.volumeId === volumeId);
if (!oldWindowLevel) {
return;
}
const oldVOI = oldWindowLevel.voi;
const windowWidth = range.upper - range.lower;
const windowCenter = range.lower + windowWidth / 2;
if (windowWidth === oldVOI.windowWidth && windowCenter === oldVOI.windowCenter) {
return;
}
const newWindowLevel = {
...oldWindowLevel,
voi: {
windowWidth,
windowCenter,
},
};
setWindowLevels(
windowLevels.map(windowLevel =>
windowLevel === oldWindowLevel ? newWindowLevel : windowLevel
)
);
},
[windowLevels]
);
const debouncedHandleCornerstoneVOIModified = useCallback(
debounce(handleCornerstoneVOIModified, 100),
[handleCornerstoneVOIModified]
);
const handleVOIChange = useCallback(
(volumeId, voi) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const newRange = {
lower: voi.windowCenter - voi.windowWidth / 2,
upper: voi.windowCenter + voi.windowWidth / 2,
};
viewport.setProperties({ voiRange: newRange }, volumeId);
viewport.render();
},
[cornerstoneViewportService, viewportId]
);
const handleOpacityChange = useCallback(
(viewportId, _volumeIndex, volumeId, opacity) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
const viewportVolumeIds = viewport.getActors().map(actor => actor.uid);
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => {
vp.setProperties({ colormap: { opacity } }, volumeId);
vp.render();
});
},
[getViewportsWithVolumeIds, cornerstoneViewportService]
);
// Listen to windowLevels changes and caches all the new ones
useEffect(() => {
const newVolumeHistograms = windowLevels
.filter(windowLevel => !cachedHistograms[windowLevel.volumeId] && windowLevel.histogram)
.reduce((volumeHistograms, windowLevel) => {
// If the histogram exists, add it to the volumeHistograms object
if (windowLevel?.histogram) {
volumeHistograms[windowLevel.volumeId] = windowLevel.histogram;
}
return volumeHistograms;
}, {});
if (Object.keys(newVolumeHistograms).length) {
setCachedHistograms(prev => ({ ...prev, ...newVolumeHistograms }));
}
}, [windowLevels, cachedHistograms]);
// Updates the histogram when the viewport index prop has changed
useEffect(() => updateViewportHistograms(), [viewportId, updateViewportHistograms]);
// Listen to cornerstone events on "eventTarget" and at the document level
useEffect(() => {
eventTarget.addEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, updateViewportHistograms);
document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true);
return () => {
eventTarget.removeEventListener(
Events.IMAGE_VOLUME_LOADING_COMPLETED,
updateViewportHistograms
);
document.removeEventListener(
Events.VOI_MODIFIED,
debouncedHandleCornerstoneVOIModified,
true
);
};
}, [updateViewportHistograms, debouncedHandleCornerstoneVOIModified]);
// Updates the viewport when the context of the viewport has changed. This is
// necessary when moving across different stages because the viewport index
// may not change but the volumes loaded on it may change.
useEffect(() => {
const { unsubscribe } = cornerstoneViewportService.subscribe(
cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED,
({ viewportInfo }) => {
if (viewportInfo.viewportId === viewportId) {
updateViewportHistograms();
}
}
);
return () => {
unsubscribe();
};
}, [viewportId, cornerstoneViewportService, updateViewportHistograms]);
return (
<PanelSection title="Window Level">
{windowLevels.map((windowLevel, i) => {
if (!windowLevel.histogram) {
return null;
}
return (
<WindowLevel
key={windowLevel.volumeId}
title={`${windowLevel.modality}`}
histogram={windowLevel.histogram}
voi={windowLevel.voi}
step={windowLevel.step}
showOpacitySlider={windowLevel.showOpacitySlider}
colormap={windowLevel.colormap}
onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)}
opacity={windowLevel.opacity}
onOpacityChange={opacity =>
handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity)
}
/>
);
})}
</PanelSection>
);
};
ViewportWindowLevel.propTypes = {
servicesManager: PropTypes.instanceOf(ServicesManager),
viewportId: PropTypes.string.isRequired,
};
export default ViewportWindowLevel;

View File

@ -0,0 +1,72 @@
import { getWebWorkerManager } from '@cornerstonejs/core';
const workerManager = getWebWorkerManager();
const WorkerOptions = {
maxWorkerInstances: 1,
autoTerminateOnIdle: {
enabled: true,
idleTimeThreshold: 1000,
},
};
// Register the task
const workerFn = () => {
return new Worker(new URL('./histogramWorker.js', import.meta.url), {
name: 'histogram-worker', // name used by the browser to name the worker
});
};
const getViewportVolumeHistogram = async (viewport, volume, options?) => {
workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions);
if (!volume?.loadStatus.loaded) {
return undefined;
}
const volumeImageData = viewport.getImageData(volume.volumeId);
if (!volumeImageData) {
return undefined;
}
let scalarData = volume.scalarData;
let prevTimePoint;
if (volume.numTimePoints > 1) {
prevTimePoint = volume.timePointIndex;
const middleTimePoint = Math.round(volume.numTimePoints / 2);
volume.timePointIndex = middleTimePoint;
scalarData = volume.getScalarData(middleTimePoint);
}
const { dimensions, origin, direction, spacing } = volume;
const range = await workerManager.executeTask('histogram-worker', 'getRange', {
dimensions,
origin,
direction,
spacing,
scalarData,
});
// after we calculate the range let's reset the timePointIndex
if (volume.numTimePoints > 1) {
volume.timePointIndex = prevTimePoint;
}
const { minimum: min, maximum: max } = range;
const calcHistOptions = {
numBins: 256,
min: Math.max(min, options?.min ?? min),
max: Math.min(max, options?.max ?? max),
};
const histogram = await workerManager.executeTask('histogram-worker', 'calcHistogram', {
data: scalarData,
options: calcHistOptions,
});
return histogram;
};
export { getViewportVolumeHistogram };

View File

@ -0,0 +1,97 @@
import { expose } from 'comlink';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
/**
* This object simulates a heavy task by implementing a sleep function and a recursive Fibonacci function.
* It's used for testing or demonstrating purposes where a heavy or time-consuming task is needed.
*/
const obj = {
getRange: ({ dimensions, origin, direction, spacing, scalarData }) => {
const imageData = vtkImageData.newInstance();
imageData.setDimensions(dimensions);
imageData.setOrigin(origin);
imageData.setDirection(direction);
imageData.setSpacing(spacing);
const scalarArray = vtkDataArray.newInstance({
name: 'Pixels',
numberOfComponents: 1,
values: scalarData,
});
imageData.getPointData().setScalars(scalarArray);
imageData.modified();
const range = imageData.computeHistogram(imageData.getBounds());
return range;
},
calcHistogram: ({ data, options }) => {
if (options === undefined) {
options = {};
}
const histogram = {
numBins: options.numBins || 256,
range: { min: 0, max: 0 },
bins: new Int32Array(1),
maxBin: 0,
maxBinValue: 0,
};
let minToUse = options.min;
let maxToUse = options.max;
if (minToUse === undefined || maxToUse === undefined) {
let min = Infinity;
let max = -Infinity;
let index = data.length;
while (index--) {
const value = data[index];
if (value < min) {
min = value;
}
if (value > max) {
max = value;
}
}
minToUse = min;
maxToUse = max;
}
histogram.range = { min: minToUse, max: maxToUse };
const bins = new Int32Array(histogram.numBins);
const binScale = histogram.numBins / (maxToUse - minToUse);
for (let index = 0; index < data.length; index++) {
const value = data[index];
if (value < minToUse) {
continue;
}
if (value > maxToUse) {
continue;
}
const bin = Math.floor((value - minToUse) * binScale);
bins[bin] += 1;
}
histogram.bins = bins;
histogram.maxBin = 0;
histogram.maxBinValue = 0;
for (let bin = 0; bin < histogram.numBins; bin++) {
if (histogram.bins[bin] > histogram.maxBinValue) {
histogram.maxBin = bin;
histogram.maxBinValue = histogram.bins[bin];
}
}
return histogram;
},
};
expose(obj);

View File

@ -0,0 +1 @@
export { default } from './ViewportWindowLevel';

View File

@ -0,0 +1,115 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { SwitchButton } from '@ohif/ui';
import { StackViewport, VolumeViewport } from '@cornerstonejs/core';
import { ColorbarProps } from '../../types/Colorbar';
import { utilities } from '@cornerstonejs/core';
export function setViewportColorbar(
viewportId,
displaySets,
commandsManager,
serviceManager,
colorbarOptions
) {
const { cornerstoneViewportService } = serviceManager.services;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
if (isLight) {
colorbarOptions.ticks = {
position: 'left',
style: {
font: '12px Arial',
color: '#000000',
maxNumTicks: 8,
tickSize: 5,
tickWidth: 1,
labelMargin: 3,
},
};
}
const displaySetInstanceUIDs = [];
if (viewport instanceof StackViewport) {
displaySetInstanceUIDs.push(viewportId);
}
if (viewport instanceof VolumeViewport) {
displaySets.forEach(ds => {
displaySetInstanceUIDs.push(ds.displaySetInstanceUID);
});
}
commandsManager.run({
commandName: 'toggleViewportColorbar',
commandOptions: {
viewportId,
options: colorbarOptions,
displaySetInstanceUIDs,
},
context: 'CORNERSTONE',
});
}
export function Colorbar({
viewportId,
displaySets,
commandsManager,
serviceManager,
colorbarProperties,
}: ColorbarProps): ReactElement {
const { colorbarService } = serviceManager.services;
const {
width: colorbarWidth,
colorbarTickPosition,
colorbarContainerPosition,
colormaps,
colorbarInitialColormap,
} = colorbarProperties;
const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId));
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, displaySets, commandsManager, serviceManager, {
viewportId,
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const updateColorbarState = () => {
setShowColorbar(colorbarService.hasColorbar(viewportId));
};
const { unsubscribe } = colorbarService.subscribe(
colorbarService.EVENTS.STATE_CHANGED,
updateColorbarState
);
return () => {
unsubscribe();
};
}, [viewportId]);
return (
<div className="all-in-one-menu-item flex w-full justify-center">
<div className="mr-2 w-[28px]"></div>
<SwitchButton
label="Display Color bar"
checked={showColorbar}
onChange={() => {
onSetColorbar();
}}
/>
</div>
);
}

View File

@ -0,0 +1,158 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui';
import { StackViewport } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap';
export function Colormap({
colormaps,
viewportId,
displaySets,
commandsManager,
serviceManager,
}: ColormapProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]);
const [showPreview, setShowPreview] = useState(false);
const [prePreviewColormap, setPrePreviewColormap] = useState(null);
const showPreviewRef = useRef(showPreview);
showPreviewRef.current = showPreview;
const prePreviewColormapRef = useRef(prePreviewColormap);
prePreviewColormapRef.current = prePreviewColormap;
const activeDisplaySetRef = useRef(activeDisplaySet);
activeDisplaySetRef.current = activeDisplaySet;
const onSetColorLUT = useCallback(
props => {
// TODO: Better way to check if it's a fusion
const oneOpacityColormaps = ['Grayscale', 'X Ray'];
const opacity =
displaySets.length > 1 && !oneOpacityColormaps.includes(props.colormap.name) ? 0.5 : 1;
commandsManager.run({
commandName: 'setViewportColormap',
commandOptions: {
...props,
opacity,
immediate: true,
},
context: 'CORNERSTONE',
});
},
[commandsManager]
);
const getViewportColormap = (viewportId, displaySet) => {
const { displaySetInstanceUID } = displaySet;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof StackViewport) {
const { colormap } = viewport.getProperties();
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
return colormap;
}
const actorEntries = viewport.getActors();
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
const { colormap } = viewport.getProperties(actorEntry.uid);
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
return colormap;
};
const buttons = useMemo(() => {
return displaySets.map((displaySet, index) => ({
children: displaySet.Modality,
key: index,
style: {
minWidth: `calc(100% / ${displaySets.length})`,
fontSize: '0.8rem',
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
}));
}, [displaySets]);
useEffect(() => {
setActiveDisplaySet(displaySets[displaySets.length - 1]);
}, [displaySets]);
return (
<>
{buttons.length > 1 && (
<div className="all-in-one-menu-item flex w-full justify-center">
<ButtonGroup
onActiveIndexChange={index => {
setActiveDisplaySet(displaySets[index]);
setPrePreviewColormap(null);
}}
activeIndex={
displaySets.findIndex(
ds => ds.displaySetInstanceUID === activeDisplaySetRef.current.displaySetInstanceUID
) || 1
}
className="w-[70%] text-[10px]"
>
{buttons.map(({ children, key, style }) => (
<div
key={key}
style={style}
>
{children}
</div>
))}
</ButtonGroup>
</div>
)}
<div className="all-in-one-menu-item flex w-full justify-center">
<SwitchButton
label="Preview in viewport"
checked={showPreview}
onChange={checked => {
setShowPreview(checked);
}}
/>
</div>
<AllInOneMenu.DividerItem />
<AllInOneMenu.ItemPanel>
{colormaps.map((colormap, index) => (
<AllInOneMenu.Item
key={index}
label={colormap.description}
onClick={() => {
onSetColorLUT({
viewportId,
colormap,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
setPrePreviewColormap(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
setPrePreviewColormap(getViewportColormap(viewportId, activeDisplaySetRef.current));
onSetColorLUT({
viewportId,
colormap,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewColormapRef.current) {
onSetColorLUT({
viewportId,
colormap: prePreviewColormapRef.current,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
}
}}
></AllInOneMenu.Item>
))}
</AllInOneMenu.ItemPanel>
</>
);
}

View File

@ -0,0 +1,130 @@
import React, { ReactElement, useState, useEffect, useCallback } from 'react';
import { VolumeLightingProps } from '../../types/ViewportPresets';
export function VolumeLighting({
serviceManager,
commandsManager,
viewportId,
}: VolumeLightingProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [ambient, setAmbient] = useState(null);
const [diffuse, setDiffuse] = useState(null);
const [specular, setSpecular] = useState(null);
const onAmbientChange = useCallback(() => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { ambient } });
}, [ambient, commandsManager, viewportId]);
const onDiffuseChange = useCallback(() => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { diffuse } });
}, [diffuse, commandsManager, viewportId]);
const onSpecularChange = useCallback(() => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { specular } });
}, [specular, commandsManager, viewportId]);
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const ambient = actor.getProperty().getAmbient();
const diffuse = actor.getProperty().getDiffuse();
const specular = actor.getProperty().getSpecular();
setAmbient(ambient);
setDiffuse(diffuse);
setSpecular(specular);
}, [viewportId, cornerstoneViewportService]);
return (
<>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="ambient"
>
Ambient
</label>
{ambient !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={ambient}
onChange={e => {
setAmbient(e.target.value);
onAmbientChange();
}}
id="ambient"
max={1}
min={0}
type="range"
step={0.1}
style={{
background: calculateBackground(ambient),
'--thumb-inner-color': '#5acce6',
'--thumb-outer-color': '#090c29',
}}
/>
)}
</div>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="diffuse"
>
Diffuse
</label>
{diffuse !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={diffuse}
onChange={e => {
setDiffuse(e.target.value);
onDiffuseChange();
}}
id="diffuse"
max={1}
min={0}
type="range"
step={0.1}
style={{
background: calculateBackground(diffuse),
'--thumb-inner-color': '#5acce6',
'--thumb-outer-color': '#090c29',
}}
/>
)}
</div>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="specular"
>
Specular
</label>
{specular !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={specular}
onChange={e => {
setSpecular(e.target.value);
onSpecularChange();
}}
id="specular"
max={1}
min={0}
type="range"
step={0.1}
style={{
background: calculateBackground(specular),
'--thumb-inner-color': '#5acce6',
'--thumb-outer-color': '#090c29',
}}
/>
)}
</div>
</>
);
}

View File

@ -0,0 +1,46 @@
import React, { ReactElement } from 'react';
import { AllInOneMenu } from '@ohif/ui';
import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets';
import { VolumeRenderingQuality } from './VolumeRenderingQuality';
import { VolumeShift } from './VolumeShift';
import { VolumeLighting } from './VolumeLighting';
import { VolumeShade } from './VolumeShade';
export function VolumeRenderingOptions({
viewportId,
commandsManager,
volumeRenderingQualityRange,
serviceManager,
}: VolumeRenderingOptionsProps): ReactElement {
return (
<AllInOneMenu.ItemPanel>
<VolumeRenderingQuality
viewportId={viewportId}
commandsManager={commandsManager}
serviceManager={serviceManager}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
<VolumeShift
viewportId={viewportId}
commandsManager={commandsManager}
serviceManager={serviceManager}
/>
<div className="all-in-one-menu-item mt-2 flex !h-[20px] w-full justify-start">
<div className="text-aqua-pale text-[13px]">LIGHTING</div>
</div>
<div className="bg-primary-dark mt-1 mb-1 h-[2px] w-full"></div>
<div className="all-in-one-menu-item flex w-full justify-center">
<VolumeShade
commandsManager={commandsManager}
serviceManager={serviceManager}
viewportId={viewportId}
/>
</div>
<VolumeLighting
viewportId={viewportId}
commandsManager={commandsManager}
serviceManager={serviceManager}
/>
</AllInOneMenu.ItemPanel>
);
}

View File

@ -0,0 +1,38 @@
import { AllInOneMenu, Icon } from '@ohif/ui';
import React, { ReactElement } from 'react';
import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets';
import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent';
export function VolumeRenderingPresets({
viewportId,
serviceManager,
commandsManager,
volumeRenderingPresets,
}: VolumeRenderingPresetsProps): ReactElement {
const { uiModalService } = serviceManager.services;
const onClickPresets = () => {
uiModalService.show({
content: VolumeRenderingPresetsContent,
title: 'Rendering Presets',
movable: true,
contentProps: {
onClose: uiModalService.hide,
presets: volumeRenderingPresets,
viewportId,
commandsManager,
},
containerDimensions: 'h-[543px] w-[460px]',
contentDimensions: 'h-[493px] w-[460px] pl-[12px] pr-[12px]',
});
};
return (
<AllInOneMenu.Item
label="Rendering Presets"
icon={<Icon name="VolumeRendering" />}
rightIcon={<Icon name="action-new-dialog" />}
onClick={onClickPresets}
/>
);
}

View File

@ -0,0 +1,95 @@
import { Icon } from '@ohif/ui';
import { ButtonEnums } from '@ohif/ui';
import React, { ReactElement, useState, useCallback } from 'react';
import { Button, InputFilterText } from '@ohif/ui';
import { ViewportPreset, VolumeRenderingPresetsContentProps } from '../../types/ViewportPresets';
export function VolumeRenderingPresetsContent({
presets,
viewportId,
commandsManager,
onClose,
}: VolumeRenderingPresetsContentProps): ReactElement {
const [filteredPresets, setFilteredPresets] = useState(presets);
const [searchValue, setSearchValue] = useState('');
const [selectedPreset, setSelectedPreset] = useState<ViewportPreset | null>(null);
const handleSearchChange = useCallback(
(value: string) => {
setSearchValue(value);
const filtered = value
? presets.filter(preset => preset.name.toLowerCase().includes(value.toLowerCase()))
: presets;
setFilteredPresets(filtered);
},
[presets]
);
const handleApply = useCallback(
props => {
commandsManager.runCommand('setViewportPreset', {
...props,
});
},
[commandsManager]
);
const formatLabel = (label: string, maxChars: number) => {
return label.length > maxChars ? `${label.slice(0, maxChars)}...` : label;
};
return (
<div className="flex min-h-full w-full flex-col justify-between">
<div className="border-secondary-light h-[433px] w-full overflow-hidden rounded border bg-black px-2.5">
<div className="flex h-[46px] w-full items-center justify-start">
<div className="h-[26px] w-[200px]">
<InputFilterText
value={searchValue}
onDebounceChange={handleSearchChange}
placeholder={'Search all'}
/>
</div>
</div>
<div className="ohif-scrollbar overflow h-[385px] w-full overflow-y-auto">
<div className="grid grid-cols-4 gap-3 pt-2 pr-3">
{filteredPresets.map((preset, index) => (
<div
key={index}
className="flex cursor-pointer flex-col items-start"
onClick={() => {
setSelectedPreset(preset);
handleApply({ preset: preset.name, viewportId });
}}
>
<Icon
name={preset.name}
className={
selectedPreset?.name === preset.name
? 'border-primary-light h-[75px] w-[95px] max-w-none rounded border-2'
: 'hover:border-primary-light h-[75px] w-[95px] max-w-none rounded border-2 border-black'
}
/>
<label className="text-aqua-pale mt-2 text-left text-xs">
{formatLabel(preset.name, 11)}
</label>
</div>
))}
</div>
</div>
</div>
<footer className="flex h-[60px] w-full items-center justify-end">
<div className="flex">
<Button
name="Cancel"
size={ButtonEnums.size.medium}
type={ButtonEnums.type.secondary}
onClick={onClose}
>
{' '}
Cancel{' '}
</Button>
</div>
</footer>
</div>
);
}

View File

@ -0,0 +1,73 @@
import React, { ReactElement, useCallback, useState, useEffect } from 'react';
import { VolumeRenderingQualityProps } from '../../types/ViewportPresets';
export function VolumeRenderingQuality({
volumeRenderingQualityRange,
commandsManager,
serviceManager,
viewportId,
}: VolumeRenderingQualityProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const { min, max, step } = volumeRenderingQualityRange;
const [quality, setQuality] = useState(null);
const onChange = useCallback(
(value: number) => {
commandsManager.runCommand('setVolumeRenderingQulaity', {
viewportId,
volumeQuality: value,
});
setQuality(value);
},
[commandsManager, viewportId]
);
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const mapper = actor.getMapper();
const image = mapper.getInputData();
const spacing = image.getSpacing();
const sampleDistance = mapper.getSampleDistance();
const averageSpacing = spacing.reduce((a, b) => a + b) / 3.0;
if (sampleDistance === averageSpacing) {
setQuality(1);
} else {
setQuality(Math.sqrt(averageSpacing / (sampleDistance * 0.5)));
}
}, [cornerstoneViewportService, viewportId]);
return (
<>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="volume"
>
Quality
</label>
{quality !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={quality}
id="volume"
max={max}
min={min}
type="range"
step={step}
onChange={e => onChange(parseInt(e.target.value, 10))}
style={{
background: calculateBackground((quality - min) / (max - min)),
'--thumb-inner-color': '#5acce6',
'--thumb-outer-color': '#090c29',
}}
/>
)}
</div>
</>
);
}

View File

@ -0,0 +1,39 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { SwitchButton } from '@ohif/ui';
import { VolumeShadeProps } from '../../types/ViewportPresets';
export function VolumeShade({
commandsManager,
viewportId,
serviceManager,
}: VolumeShadeProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [shade, setShade] = useState(true);
const [key, setKey] = useState(0);
const onShadeChange = useCallback(
(checked: boolean) => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { shade: checked } });
},
[commandsManager, viewportId]
);
useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const shade = actor.getProperty().getShade();
setShade(shade);
setKey(key + 1);
}, [viewportId, cornerstoneViewportService]);
return (
<SwitchButton
key={key}
label="Shade"
checked={shade}
onChange={() => {
setShade(!shade);
onShadeChange(!shade);
}}
/>
);
}

View File

@ -0,0 +1,93 @@
import React, { ReactElement, useCallback, useEffect, useState, useRef } from 'react';
import { VolumeShiftProps } from '../../types/ViewportPresets';
export function VolumeShift({
viewportId,
commandsManager,
serviceManager,
}: VolumeShiftProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [minShift, setMinShift] = useState<number | null>(null);
const [maxShift, setMaxShift] = useState<number | null>(null);
const [shift, setShift] = useState<number | null>(
cornerstoneViewportService.getCornerstoneViewport(viewportId)?.shiftedBy || 0
);
const [step, setStep] = useState<number | null>(null);
const [isBlocking, setIsBlocking] = useState(false);
const prevShiftRef = useRef<number>(shift);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const ofun = actor.getProperty().getScalarOpacity(0);
useEffect(() => {
if (isBlocking) {
return;
}
const range = ofun.getRange();
const transferFunctionWidth = range[1] - range[0];
const minShift = -transferFunctionWidth;
const maxShift = transferFunctionWidth;
setMinShift(minShift);
setMaxShift(maxShift);
setStep(Math.pow(10, Math.floor(Math.log10(transferFunctionWidth / 500))));
}, [cornerstoneViewportService, viewportId, actor, ofun, isBlocking]);
const onChangeRange = useCallback(
newShift => {
const shiftDifference = newShift - prevShiftRef.current;
prevShiftRef.current = newShift;
viewport.shiftedBy = newShift;
commandsManager.runCommand('shiftVolumeOpacityPoints', {
viewportId,
shift: shiftDifference,
});
},
[commandsManager, viewportId, viewport]
);
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
return (
<>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="shift"
>
Shift
</label>
{step !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={shift}
onChange={e => {
const shiftValue = parseInt(e.target.value, 10);
setShift(shiftValue);
onChangeRange(shiftValue);
}}
id="shift"
onMouseDown={() => setIsBlocking(true)}
onMouseUp={() => setIsBlocking(false)}
max={maxShift}
min={minShift}
type="range"
step={step}
style={{
background: calculateBackground((shift - minShift) / (maxShift - minShift)),
'--thumb-inner-color': '#5acce6',
'--thumb-outer-color': '#090c29',
}}
/>
)}
</div>
</>
);
}

View File

@ -0,0 +1,57 @@
import React, { ReactElement, useCallback } from 'react';
import { AllInOneMenu } from '@ohif/ui';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { CommandsManager } from '@ohif/core';
import { useTranslation } from 'react-i18next';
export type WindowLevelProps = {
viewportId: string;
presets: Array<Record<string, Array<WindowLevelPreset>>>;
commandsManager: CommandsManager;
};
export function WindowLevel({
viewportId,
commandsManager,
presets,
}: WindowLevelProps): ReactElement {
const { t } = useTranslation('WindowLevelActionMenu');
const onSetWindowLevel = useCallback(
props => {
commandsManager.run({
commandName: 'setViewportWindowLevel',
commandOptions: {
...props,
viewportId,
},
context: 'CORNERSTONE',
});
},
[commandsManager, viewportId]
);
return (
<AllInOneMenu.ItemPanel>
{presets.map((modalityPresets, modalityIndex) => (
<React.Fragment key={modalityIndex}>
{Object.entries(modalityPresets).map(([modality, presetsArray]) => (
<React.Fragment key={modality}>
<AllInOneMenu.HeaderItem>
{t('Modality Presets', { modality })}
</AllInOneMenu.HeaderItem>
{presetsArray.map((preset, index) => (
<AllInOneMenu.Item
key={`${modality}-${index}`}
label={preset.description}
secondaryLabel={`${preset.window} / ${preset.level}`}
onClick={() => onSetWindowLevel(preset)}
/>
))}
</React.Fragment>
))}
</React.Fragment>
))}
</AllInOneMenu.ItemPanel>
);
}

View File

@ -0,0 +1,198 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { AllInOneMenu, useViewportGrid } from '@ohif/ui';
import { CommandsManager, ServicesManager } from '@ohif/core';
import { Colormap } from './Colormap';
import { Colorbar } from './Colorbar';
import { setViewportColorbar } from './Colorbar';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { ColorbarProperties } from '../../types/Colorbar';
import { VolumeRenderingQualityRange } from '../../types/ViewportPresets';
import { WindowLevel } from './WindowLevel';
import { VolumeRenderingPresets } from './VolumeRenderingPresets';
import { VolumeRenderingOptions } from './VolumeRenderingOptions';
import { ViewportPreset } from '../../types/ViewportPresets';
import { VolumeViewport3D } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core';
export type WindowLevelActionMenuProps = {
viewportId: string;
element: HTMLElement;
presets: Array<Record<string, Array<WindowLevelPreset>>>;
verticalDirection: AllInOneMenu.VerticalDirection;
horizontalDirection: AllInOneMenu.HorizontalDirection;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
colorbarProperties: ColorbarProperties;
displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
};
export function WindowLevelActionMenu({
viewportId,
element,
presets,
verticalDirection,
horizontalDirection,
commandsManager,
serviceManager,
colorbarProperties,
displaySets,
volumeRenderingPresets,
volumeRenderingQualityRange,
}: WindowLevelActionMenuProps): ReactElement {
const {
colormaps,
colorbarContainerPosition,
colorbarInitialColormap,
colorbarTickPosition,
width: colorbarWidth,
} = colorbarProperties;
const { colorbarService, cornerstoneViewportService } = serviceManager.services;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
const { t } = useTranslation('WindowLevelActionMenu');
const [viewportGrid] = useViewportGrid();
const { activeViewportId } = viewportGrid;
const [vpHeight, setVpHeight] = useState(element?.clientHeight);
const [menuKey, setMenuKey] = useState(0);
const [is3DVolume, setIs3DVolume] = useState(false);
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, displaySets, commandsManager, serviceManager, {
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const newVpHeight = element?.clientHeight;
if (vpHeight !== newVpHeight) {
setVpHeight(newVpHeight);
}
}, [element, vpHeight]);
useEffect(() => {
if (!colorbarService.hasColorbar(viewportId)) {
return;
}
window.setTimeout(() => {
colorbarService.removeColorbar(viewportId);
onSetColorbar();
}, 0);
}, [viewportId, displaySets, viewport]);
useEffect(() => {
setMenuKey(menuKey + 1);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof VolumeViewport3D) {
setIs3DVolume(true);
} else {
setIs3DVolume(false);
}
}, [
displaySets,
viewportId,
presets,
volumeRenderingQualityRange,
volumeRenderingPresets,
colorbarProperties,
activeViewportId,
viewportGrid,
]);
return (
<AllInOneMenu.IconMenu
icon="viewport-window-level"
verticalDirection={verticalDirection}
horizontalDirection={horizontalDirection}
iconClassName={classNames(
// Visible on hover and for the active viewport
activeViewportId === viewportId ? 'visible' : 'invisible group-hover:visible',
'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light',
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60'
)}
menuStyle={{ maxHeight: vpHeight - 32, minWidth: 218 }}
onVisibilityChange={() => {
setVpHeight(element.clientHeight);
}}
menuKey={menuKey}
>
<AllInOneMenu.ItemPanel>
{!is3DVolume && (
<Colorbar
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
commandsManager={commandsManager}
serviceManager={serviceManager}
colorbarProperties={colorbarProperties}
/>
)}
{colormaps && !is3DVolume && (
<AllInOneMenu.SubMenu
key="colorLUTPresets"
itemLabel="Color LUT"
itemIcon="icon-color-lut"
>
<Colormap
colormaps={colormaps}
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
commandsManager={commandsManager}
serviceManager={serviceManager}
/>
</AllInOneMenu.SubMenu>
)}
{presets && presets.length > 0 && !is3DVolume && (
<AllInOneMenu.SubMenu
key="windowLevelPresets"
itemLabel={t('Modality Window Presets')}
itemIcon="viewport-window-level"
>
<WindowLevel
viewportId={viewportId}
commandsManager={commandsManager}
presets={presets}
/>
</AllInOneMenu.SubMenu>
)}
{volumeRenderingPresets && is3DVolume && (
<VolumeRenderingPresets
serviceManager={serviceManager}
viewportId={viewportId}
commandsManager={commandsManager}
volumeRenderingPresets={volumeRenderingPresets}
/>
)}
{volumeRenderingQualityRange && is3DVolume && (
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
<VolumeRenderingOptions
viewportId={viewportId}
commandsManager={commandsManager}
volumeRenderingQualityRange={volumeRenderingQualityRange}
serviceManager={serviceManager}
/>
</AllInOneMenu.SubMenu>
)}
</AllInOneMenu.ItemPanel>
</AllInOneMenu.IconMenu>
);
}

View File

@ -0,0 +1,23 @@
// The following are the default window level presets and can be further
// configured via the customization service.
const defaultWindowLevelPresets = {
CT: [
{ description: 'Soft tissue', window: '400', level: '40' },
{ description: 'Lung', window: '1500', level: '-600' },
{ description: 'Liver', window: '150', level: '90' },
{ description: 'Bone', window: '2500', level: '480' },
{ description: 'Brain', window: '80', level: '40' },
],
PT: [
{ description: 'Default', window: '5', level: '2.5' },
{ description: 'SUV', window: '0', level: '3' },
{ description: 'SUV', window: '0', level: '5' },
{ description: 'SUV', window: '0', level: '7' },
{ description: 'SUV', window: '0', level: '8' },
{ description: 'SUV', window: '0', level: '10' },
{ description: 'SUV', window: '0', level: '15' },
],
};
export default defaultWindowLevelPresets;

View File

@ -0,0 +1,42 @@
import React, { ReactNode } from 'react';
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
export function getWindowLevelActionMenu({
viewportId,
element,
displaySets,
servicesManager,
commandsManager,
verticalDirection,
horizontalDirection,
}): ReactNode {
const { customizationService } = servicesManager.services;
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
const colorbarProperties = customizationService.get('cornerstone.colorbar');
const { volumeRenderingPresets, volumeRenderingQualityRange } = customizationService.get(
'cornerstone.3dVolumeRendering'
);
const displaySetPresets = displaySets
.filter(displaySet => presets[displaySet.Modality])
.map(displaySet => {
return { [displaySet.Modality]: presets[displaySet.Modality] };
});
return (
<WindowLevelActionMenu
viewportId={viewportId}
element={element}
presets={displaySetPresets}
verticalDirection={verticalDirection}
horizontalDirection={horizontalDirection}
commandsManager={commandsManager}
serviceManager={servicesManager}
colorbarProperties={colorbarProperties}
displaySets={displaySets}
volumeRenderingPresets={volumeRenderingPresets}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
);
}

View File

@ -0,0 +1,159 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
} from 'react';
import PropTypes from 'prop-types';
import { Types, ViewportActionCornersLocations } from '@ohif/ui';
import ViewportActionCornersService, {
ActionComponentInfo,
} from '../services/ViewportActionCornersService/ViewportActionCornersService';
interface StateComponentInfo extends Types.ViewportActionCornersComponentInfo {
indexPriority: number;
}
type State = Record<string, Record<ViewportActionCornersLocations, Array<StateComponentInfo>>>;
const DEFAULT_STATE: State = {
// default here is the viewportId of the default viewport
default: {
[ViewportActionCornersLocations.topLeft]: [],
[ViewportActionCornersLocations.topRight]: [],
[ViewportActionCornersLocations.bottomLeft]: [],
[ViewportActionCornersLocations.bottomRight]: [],
},
// [anotherViewportId]: { ..... }
};
export const ViewportActionCornersContext = createContext(DEFAULT_STATE);
export function ViewportActionCornersProvider({ children, service }) {
const viewportActionCornersReducer = (state, action) => {
switch (action.type) {
case 'SET_ACTION_COMPONENT': {
const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
// Get the components at the specified location of the specified viewport.
let locationComponents = state?.[viewportId]?.[location]
? [...state[viewportId][location]]
: [];
// If the component (id) already exists at the location specified in the payload,
// then it must be replaced with the component in the payload so first
// remove it from that location.
const deletionIndex = locationComponents.findIndex(component => component.id === id);
if (deletionIndex !== -1) {
locationComponents = [
...locationComponents.slice(0, deletionIndex),
...locationComponents.slice(deletionIndex + 1),
];
}
// Insert the component from the payload but
// do not insert an undefined or null component.
if (component) {
const insertionIndex = locationComponents.findIndex(
component => indexPriority <= component.indexPriority
);
locationComponents = [
...locationComponents.slice(0, insertionIndex),
{
id,
component,
indexPriority,
},
...locationComponents.slice(insertionIndex + 1),
];
}
return {
...state,
...{
[viewportId]: {
...state[viewportId],
[location]: locationComponents,
},
},
};
}
case 'CLEAR_ACTION_COMPONENTS': {
const viewportId = action.payload;
const nextState = { ...state };
delete nextState[viewportId];
return nextState;
}
default:
return { ...state };
}
};
const [viewportActionCornersState, dispatch] = useReducer(
viewportActionCornersReducer,
DEFAULT_STATE
);
const getState = useCallback(() => {
return viewportActionCornersState;
}, [viewportActionCornersState]);
const setComponent = useCallback(
(actionComponentInfo: ActionComponentInfo) => {
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo });
},
[dispatch]
);
const setComponents = useCallback(
(actionComponentInfos: Array<ActionComponentInfo>) => {
actionComponentInfos.forEach(actionComponentInfo =>
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo })
);
},
[dispatch]
);
const clear = useCallback(
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
[dispatch]
);
useEffect(() => {
if (service) {
service.setServiceImplementation({
getState,
setComponent,
setComponents,
clear,
});
}
}, [getState, service, setComponent, setComponents]);
// run many of the calls through the service itself since we want to publish events
const api = {
getState,
setComponent: props => service.setComponent(props),
setComponents: props => service.setComponents(props),
clear: props => service.clear(props),
};
const contextValue = useMemo(
() => [viewportActionCornersState, api],
[viewportActionCornersState, api]
);
return (
<ViewportActionCornersContext.Provider value={contextValue}>
{children}
</ViewportActionCornersContext.Provider>
);
}
ViewportActionCornersProvider.propTypes = {
children: PropTypes.node,
service: PropTypes.instanceOf(ViewportActionCornersService).isRequired,
};
export const useViewportActionCornersContext = () => useContext(ViewportActionCornersContext);

View File

@ -1,6 +1,12 @@
import { Enums } from '@cornerstonejs/tools';
import { toolNames } from './initCornerstoneTools';
import DicomUpload from './components/DicomUpload/DicomUpload';
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
import { colormaps } from './utils/colormaps';
import { CONSTANTS } from '@cornerstonejs/core';
const DefaultColormap = 'Grayscale';
const { VIEWPORT_PRESETS } = CONSTANTS;
const tools = {
active: [
@ -37,6 +43,157 @@ function getCustomizationModule() {
id: 'cornerstone.overlayViewportTools',
tools,
},
{
id: 'cornerstone.windowLevelPresets',
presets: defaultWindowLevelPresets,
},
{
id: 'cornerstone.colorbar',
width: '16px',
colorbarTickPosition: 'left',
colormaps,
colorbarContainerPosition: 'right',
colorbarInitialColormap: DefaultColormap,
},
{
id: 'cornerstone.3dVolumeRendering',
volumeRenderingPresets: VIEWPORT_PRESETS,
volumeRenderingQualityRange: {
min: 1,
max: 4,
step: 1,
},
},
{
id: 'cornerstone.measurements',
Angle: {
displayText: [],
report: [],
},
CobbAngle: {
displayText: [],
report: [],
},
ArrowAnnotate: {
displayText: [],
report: [],
},
RectangleROi: {
displayText: [],
report: [],
},
CircleROI: {
displayText: [],
report: [],
},
EllipticalROI: {
displayText: [],
report: [],
},
Bidirectional: {
displayText: [],
report: [],
},
Length: {
displayText: [],
report: [],
},
LivewireContour: {
displayText: [],
report: [],
},
SplineROI: {
displayText: [
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
value: 'areaUnit',
for: ['area'],
type: 'unit',
},
/**
{
displayName: 'Modality',
value: 'Modality',
type: 'value',
},
*/
],
report: [
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
displayName: 'Unit',
value: 'areaUnit',
type: 'value',
},
],
},
PlanarFreehandROI: {
displayText: [
{
displayName: 'Mean',
value: 'mean',
type: 'value',
},
{
displayName: 'Max',
value: 'max',
type: 'value',
},
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
value: 'modalityUnit',
for: ['mean', 'max' /** 'stdDev **/],
type: 'unit',
},
{
value: 'areaUnit',
for: ['area'],
type: 'unit',
},
/**
{
displayName: 'Std Dev',
value: 'stdDev',
type: 'value',
},
*/
],
report: [
{
displayName: 'Mean',
value: 'mean',
type: 'value',
},
{
displayName: 'Max',
value: 'max',
type: 'value',
},
{
displayName: 'Area',
value: 'area',
type: 'value',
},
{
displayName: 'Unit',
value: 'unit',
type: 'value',
},
],
},
},
],
},
];

View File

@ -1,295 +1,10 @@
import { Types } from '@ohif/core';
const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
name: 'Multi-Planar Reconstruction',
locked: true,
createdDate: '2021-02-23',
modifiedDate: '2023-08-15',
availableTo: {},
editableBy: {},
// Unknown number of priors referenced - so just match any study
numberOfPriorsReferenced: 0,
protocolMatchingRules: [],
imageLoadStrategy: 'nth',
callbacks: {
// Switches out of MPR mode when the layout change button is used
onLayoutChange: [
{
commandName: 'toggleHangingProtocol',
commandOptions: { protocolId: 'mpr' },
context: 'DEFAULT',
},
],
// Turns off crosshairs when switching out of MPR mode
onProtocolExit: [
{
commandName: 'cleanUpCrosshairs',
},
],
},
displaySetSelectors: {
activeDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
name: 'MPR 1x3',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 3,
layoutOptions: [
{
x: 0,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 1 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 2 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
],
},
},
viewports: [
{
viewportOptions: {
viewportId: 'mpr-axial',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'mpr-sagittal',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'mpr-coronal',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
],
},
],
};
const mprAnd3DVolumeViewport = {
id: 'mprAnd3DVolumeViewport',
locked: true,
name: 'mpr',
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
{
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
},
required: true,
},
],
},
},
stages: [
{
id: 'mpr3Stage',
name: 'mpr',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 2,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
displayPreset: 'CT-Bone',
},
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};
import { fourUp } from './hps/fourUp';
import { main3D } from './hps/main3D';
import { mpr } from './hps/mpr';
import { mprAnd3DVolumeViewport } from './hps/mprAnd3DVolumeViewport';
import { only3D } from './hps/only3D';
import { primary3D } from './hps/primary3D';
import { primaryAxial } from './hps/primaryAxial';
function getHangingProtocolModule() {
return [
@ -301,6 +16,26 @@ function getHangingProtocolModule() {
name: mprAnd3DVolumeViewport.id,
protocol: mprAnd3DVolumeViewport,
},
{
name: fourUp.id,
protocol: fourUp,
},
{
name: main3D.id,
protocol: main3D,
},
{
name: primaryAxial.id,
protocol: primaryAxial,
},
{
name: only3D.id,
protocol: only3D,
},
{
name: primary3D.id,
protocol: primary3D,
},
];
}

View File

@ -0,0 +1,303 @@
import { Enums } from '@cornerstonejs/tools';
const getToggledClassName = (isToggled: boolean) => {
return isToggled
? '!text-primary-active'
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
};
export default function getToolbarModule({ commandsManager, servicesManager }) {
const {
toolGroupService,
toolbarService,
syncGroupService,
cornerstoneViewportService,
hangingProtocolService,
displaySetService,
viewportGridService,
} = servicesManager.services;
return [
// functions/helpers to be used by the toolbar buttons to decide if they should
// enabled or not
{
name: 'evaluate.cornerstoneTool',
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isPrimaryActive = toolNames
? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool())
: toolGroup.getActivePrimaryMouseButtonTool() === toolName;
return {
disabled: false,
className: isPrimaryActive
? '!text-black bg-primary-light rounded'
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light rounded',
// Todo: isActive right now is used for nested buttons where the primary
// button needs to be fully rounded (vs partial rounded) when active
// otherwise it does not have any other use
isActive: isPrimaryActive,
};
},
},
{
name: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
evaluate: ({ viewportId, button, itemId }) => {
const { items } = button.props;
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return {
primary: button.props.primary,
items,
};
}
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
// check if the active toolName is part of the items then we need
// to move it to the primary button
const activeToolIndex = items.findIndex(item => {
const toolName = toolbarService.getToolNameForButton(item);
return toolName === activeToolName;
});
// if there is an active tool in the items dropdown bound to the primary mouse/touch
// we should show that no matter what
if (activeToolIndex > -1) {
return {
primary: items[activeToolIndex],
items,
};
}
if (!itemId) {
return {
primary: button.props.primary,
items,
};
}
// other wise we can move the clicked tool to the primary button
const clickedItemProps = items.find(item => item.id === itemId || item.itemId === itemId);
return {
primary: clickedItemProps,
items,
};
},
},
{
name: 'evaluate.action',
evaluate: ({ viewportId, button }) => {
return {
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
};
},
},
{
name: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
evaluate: ({ viewportId, button, disabledText }) =>
_evaluateToggle({
viewportId,
button,
toolbarService,
disabledText,
offModes: [Enums.ToolModes.Disabled],
toolGroupService,
}),
},
{
name: 'evaluate.cornerstoneTool.toggle',
evaluate: ({ viewportId, button, disabledText }) =>
_evaluateToggle({
viewportId,
button,
toolbarService,
disabledText,
offModes: [Enums.ToolModes.Disabled, Enums.ToolModes.Passive],
toolGroupService,
}),
},
{
name: 'evaluate.cornerstone.synchronizer',
evaluate: ({ viewportId, button }) => {
let synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
if (!synchronizers?.length) {
return {
className: getToggledClassName(false),
};
}
const isArray = Array.isArray(button.commands);
const synchronizerType = isArray
? button.commands?.[0].commandOptions.type
: button.commands?.commandOptions.type;
synchronizers = syncGroupService.getSynchronizersOfType(synchronizerType);
if (!synchronizers?.length) {
return {
className: getToggledClassName(false),
};
}
// Todo: we need a better way to find the synchronizers based on their
// type, but for now we just check the first one and see if it is
// enabled
const synchronizer = synchronizers[0];
const isEnabled = synchronizer?._enabled;
return {
className: getToggledClassName(isEnabled),
};
},
},
{
name: 'evaluate.not3D',
evaluate: ({ viewportId, disabledText }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport?.type === 'volume3d') {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
},
},
{
name: 'evaluate.isUS',
evaluate: ({ viewportId, disabledText }) => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const isUS = displaySets.some(displaySet => displaySet?.Modality === 'US');
if (!isUS) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
},
},
{
name: 'evaluate.viewportProperties.toggle',
evaluate: ({ viewportId, button }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport.isDisabled) {
return;
}
const propId = button.id;
const properties = viewport.getProperties();
const camera = viewport.getCamera();
const prop = camera?.[propId] || properties?.[propId];
if (!prop) {
return {
disabled: false,
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
};
}
const isToggled = prop;
return {
className: getToggledClassName(isToggled),
};
},
},
{
name: 'evaluate.mpr',
evaluate: ({ viewportId, disabledText = 'Selected viewport is not reconstructable' }) => {
const { protocol } = hangingProtocolService.getActiveProtocol();
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const areReconstructable = displaySets.every(displaySet => {
return displaySet.isReconstructable;
});
if (!areReconstructable) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isMpr = protocol?.id === 'mpr';
return {
disabled: false,
className: getToggledClassName(isMpr),
};
},
},
];
}
function _evaluateToggle({
viewportId,
toolbarService,
button,
disabledText,
offModes,
toolGroupService,
}) {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
return {
className: getToggledClassName(!isOff),
};
}

View File

@ -0,0 +1,144 @@
export const fourUp = {
id: 'fourUp',
locked: true,
name: '3D four up',
icon: 'layout-advanced-3d-four-up',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
id: 'fourUpStage',
name: 'fourUp',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 2,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
displayPreset: {
CT: 'CT-Bone',
MR: 'MR-Default',
default: 'CT-Bone',
},
},
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};

View File

@ -0,0 +1,170 @@
export const main3D = {
id: 'main3D',
locked: true,
name: '3D main',
icon: 'layout-advanced-3d-main',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
id: 'main3DStage',
name: 'main3D',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 3,
layoutOptions: [
{
x: 0,
y: 0,
width: 1,
height: 1 / 2,
},
{
x: 0,
y: 1 / 2,
width: 1 / 3,
height: 1 / 2,
},
{
x: 1 / 3,
y: 1 / 2,
width: 1 / 3,
height: 1 / 2,
},
{
x: 2 / 3,
y: 1 / 2,
width: 1 / 3,
height: 1 / 2,
},
],
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
displayPreset: {
CT: 'CT-Bone',
MR: 'MR-Default',
default: 'CT-Bone',
},
},
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};

View File

@ -0,0 +1,149 @@
import { Types } from '@ohif/core';
export const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
name: 'MPR',
locked: true,
icon: 'layout-advanced-mpr',
isPreset: true,
createdDate: '2021-02-23',
modifiedDate: '2023-08-15',
availableTo: {},
editableBy: {},
// Unknown number of priors referenced - so just match any study
numberOfPriorsReferenced: 0,
protocolMatchingRules: [],
imageLoadStrategy: 'nth',
callbacks: {},
displaySetSelectors: {
activeDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
name: 'MPR 1x3',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 3,
layoutOptions: [
{
x: 0,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 1 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 2 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
],
},
},
viewports: [
{
viewportOptions: {
viewportId: 'mpr-axial',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'mpr-sagittal',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'mpr-coronal',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
],
},
],
};

View File

@ -0,0 +1,151 @@
export const mprAnd3DVolumeViewport = {
id: 'mprAnd3DVolumeViewport',
locked: true,
name: 'mpr',
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
{
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
},
required: true,
},
],
},
},
stages: [
{
id: 'mpr3Stage',
name: 'mpr',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 2,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
displayPreset: {
CT: 'CT-Bone',
MR: 'MR-Default',
default: 'CT-Bone',
},
},
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};

View File

@ -0,0 +1,66 @@
export const only3D = {
id: 'only3D',
locked: true,
name: '3D only',
icon: 'layout-advanced-3d-only',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
id: 'only3DStage',
name: 'only3D',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 1,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
displayPreset: {
CT: 'CT-Bone',
MR: 'MR-Default',
default: 'CT-Bone',
},
},
},
],
},
],
},
],
};

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