feat(v3p8): advanced 3D layouts and 4d data support (#4071)
This commit is contained in:
commit
1dcdc9dabe
@ -13,7 +13,7 @@ version: 2.1
|
|||||||
##
|
##
|
||||||
orbs:
|
orbs:
|
||||||
codecov: codecov/codecov@1.0.5
|
codecov: codecov/codecov@1.0.5
|
||||||
cypress: cypress-io/cypress@3.1.4
|
cypress: cypress-io/cypress@3.3.1
|
||||||
|
|
||||||
executors:
|
executors:
|
||||||
cypress-custom:
|
cypress-custom:
|
||||||
|
|||||||
25
.codecov.yml
25
.codecov.yml
@ -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
|
|
||||||
22
.github/workflows/codespell.yml
vendored
22
.github/workflows/codespell.yml
vendored
@ -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
1
.gitignore
vendored
@ -24,6 +24,7 @@ yarn-error.log
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
.directory
|
||||||
|
|
||||||
# Common Example Data Directories
|
# Common Example Data Directories
|
||||||
sampledata/
|
sampledata/
|
||||||
|
|||||||
265
.scripts/dicom-json-generator.js
Normal file
265
.scripts/dicom-json-generator.js
Normal 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);
|
||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -31,6 +31,6 @@
|
|||||||
"prettier.endOfLine": "lf",
|
"prettier.endOfLine": "lf",
|
||||||
"workbench.colorCustomizations": {},
|
"workbench.colorCustomizations": {},
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": "explicit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -120,6 +120,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
|||||||
test: /\.wasm/,
|
test: /\.wasm/,
|
||||||
type: 'asset/resource',
|
type: 'asset/resource',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpe?g|gif|svg)$/i,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: 'assets/images/[name].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
], //.concat(vtkRules),
|
], //.concat(vtkRules),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
996
CHANGELOG.md
996
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
15
README.md
15
README.md
@ -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="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="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="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="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="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="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="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="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-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
|
## About
|
||||||
|
|||||||
@ -26,6 +26,7 @@ module.exports = {
|
|||||||
'@babel/plugin-transform-typescript',
|
'@babel/plugin-transform-typescript',
|
||||||
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
||||||
['@babel/plugin-proposal-private-methods', { loose: true }],
|
['@babel/plugin-proposal-private-methods', { loose: true }],
|
||||||
|
'@babel/plugin-transform-class-static-block',
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
test: {
|
test: {
|
||||||
@ -45,6 +46,7 @@ module.exports = {
|
|||||||
'@babel/plugin-proposal-object-rest-spread',
|
'@babel/plugin-proposal-object-rest-spread',
|
||||||
'@babel/plugin-syntax-dynamic-import',
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
'@babel/plugin-transform-regenerator',
|
'@babel/plugin-transform-regenerator',
|
||||||
|
'@babel/transform-destructuring',
|
||||||
'@babel/plugin-transform-runtime',
|
'@babel/plugin-transform-runtime',
|
||||||
'@babel/plugin-transform-typescript',
|
'@babel/plugin-transform-typescript',
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
5ddf8a16027255d28dc01c1740099cf85bbcf458
|
f1a736d1934733a434cb87b2c284907a3122403f
|
||||||
@ -3,7 +3,769 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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
|
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone-dicom-rt",
|
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||||
"version": "3.7.0",
|
"version": "3.8.0-beta.93",
|
||||||
"description": "DICOM RT read workflow",
|
"description": "DICOM RT read workflow",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -24,6 +24,8 @@
|
|||||||
"yarn": ">=1.18.0"
|
"yarn": ">=1.18.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||||
"dev:dicom-seg": "yarn run dev",
|
"dev:dicom-seg": "yarn run dev",
|
||||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
@ -31,10 +33,10 @@
|
|||||||
"start": "yarn run dev"
|
"start": "yarn run dev"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.7.0",
|
"@ohif/core": "3.8.0-beta.93",
|
||||||
"@ohif/extension-cornerstone": "3.7.0",
|
"@ohif/extension-cornerstone": "3.8.0-beta.93",
|
||||||
"@ohif/extension-default": "3.7.0",
|
"@ohif/extension-default": "3.8.0-beta.93",
|
||||||
"@ohif/i18n": "3.7.0",
|
"@ohif/i18n": "3.8.0-beta.93",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
|||||||
@ -66,6 +66,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
|||||||
uiViewportDialogService.hide();
|
uiViewportDialogService.hide();
|
||||||
resolve(RESPONSE.CANCEL);
|
resolve(RESPONSE.CANCEL);
|
||||||
},
|
},
|
||||||
|
onKeyPress: event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import OHIF, { utils } from '@ohif/core';
|
import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
|
||||||
import { ViewportActionBar, useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
|
|
||||||
|
|
||||||
import promptHydrateRT from '../utils/promptHydrateRT';
|
import promptHydrateRT from '../utils/promptHydrateRT';
|
||||||
import _getStatusComponent from './_getStatusComponent';
|
import _getStatusComponent from './_getStatusComponent';
|
||||||
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
|
||||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||||
|
|
||||||
function OHIFCornerstoneRTViewport(props) {
|
function OHIFCornerstoneRTViewport(props) {
|
||||||
@ -15,7 +13,6 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
children,
|
children,
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportOptions,
|
viewportOptions,
|
||||||
viewportLabel,
|
|
||||||
servicesManager,
|
servicesManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
@ -27,6 +24,7 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
segmentationService,
|
segmentationService,
|
||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
customizationService,
|
customizationService,
|
||||||
|
viewportActionCornersService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const viewportId = viewportOptions.viewportId;
|
const viewportId = viewportOptions.viewportId;
|
||||||
@ -118,7 +116,10 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
orientation: viewportOptions.orientation,
|
orientation: viewportOptions.orientation,
|
||||||
viewportId: viewportOptions.viewportId,
|
viewportId: viewportOptions.viewportId,
|
||||||
}}
|
}}
|
||||||
onElementEnabled={onElementEnabled}
|
onElementEnabled={evt => {
|
||||||
|
props.onElementEnabled?.(evt);
|
||||||
|
onElementEnabled(evt);
|
||||||
|
}}
|
||||||
onElementDisabled={onElementDisabled}
|
onElementDisabled={onElementDisabled}
|
||||||
></Component>
|
></Component>
|
||||||
);
|
);
|
||||||
@ -126,7 +127,6 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
|
|
||||||
const onSegmentChange = useCallback(
|
const onSegmentChange = useCallback(
|
||||||
direction => {
|
direction => {
|
||||||
direction = direction === 'left' ? -1 : 1;
|
|
||||||
const segmentationId = rtDisplaySet.displaySetInstanceUID;
|
const segmentationId = rtDisplaySet.displaySetInstanceUID;
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
|
||||||
@ -168,6 +168,8 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
|
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// I'm not sure what is this, since in RT we support Overlapping segments
|
||||||
|
// via contours
|
||||||
const { unsubscribe } = segmentationService.subscribe(
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||||
evt => {
|
evt => {
|
||||||
@ -280,20 +282,7 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const onStatusClick = useCallback(async () => {
|
||||||
PatientID,
|
|
||||||
PatientName,
|
|
||||||
PatientSex,
|
|
||||||
PatientAge,
|
|
||||||
SliceThickness,
|
|
||||||
ManufacturerModelName,
|
|
||||||
StudyDate,
|
|
||||||
SeriesDescription,
|
|
||||||
SpacingBetweenSlices,
|
|
||||||
SeriesNumber,
|
|
||||||
} = referencedDisplaySetRef.current.metadata;
|
|
||||||
|
|
||||||
const onStatusClick = async () => {
|
|
||||||
// Before hydrating a RT and make it added to all viewports in the grid
|
// 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
|
// 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
|
// presentation state, so that we can restore it after hydrating the RT. This is
|
||||||
@ -307,41 +296,47 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setIsHydrated(isHydrated);
|
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 (
|
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">
|
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||||
{rtIsLoading && (
|
{rtIsLoading && (
|
||||||
<LoadingIndicatorTotalPercent
|
<LoadingIndicatorTotalPercent
|
||||||
|
|||||||
@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
|||||||
let ToolTipMessage = null;
|
let ToolTipMessage = null;
|
||||||
let StatusIcon = null;
|
let StatusIcon = null;
|
||||||
|
|
||||||
const { t } = useTranslation('Common');
|
|
||||||
const loadStr = t('LOAD');
|
|
||||||
|
|
||||||
switch (isHydrated) {
|
switch (isHydrated) {
|
||||||
case true:
|
case true:
|
||||||
StatusIcon = () => <Icon name="status-alert" />;
|
StatusIcon = () => <Icon name="status-alert" />;
|
||||||
@ -26,23 +23,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
|||||||
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
|
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusArea = () => (
|
const StatusArea = () => {
|
||||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
const { t } = useTranslation('Common');
|
||||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
const loadStr = t('LOAD');
|
||||||
<StatusIcon />
|
|
||||||
<span className="ml-1">RTSTRUCT</span>
|
return (
|
||||||
</div>
|
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||||
{!isHydrated && (
|
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||||
<div
|
<StatusIcon />
|
||||||
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
|
<span className="ml-1">RTSTRUCT</span>
|
||||||
// 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>
|
||||||
)}
|
{!isHydrated && (
|
||||||
</div>
|
<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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -3,7 +3,820 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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
|
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||||
"version": "3.7.0",
|
"version": "3.8.0-beta.93",
|
||||||
"description": "DICOM SEG read workflow",
|
"description": "DICOM SEG read workflow",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -24,6 +24,8 @@
|
|||||||
"yarn": ">=1.18.0"
|
"yarn": ">=1.18.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||||
"dev:dicom-seg": "yarn run dev",
|
"dev:dicom-seg": "yarn run dev",
|
||||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
@ -31,10 +33,10 @@
|
|||||||
"start": "yarn run dev"
|
"start": "yarn run dev"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.7.0",
|
"@ohif/core": "3.8.0-beta.93",
|
||||||
"@ohif/extension-cornerstone": "3.7.0",
|
"@ohif/extension-cornerstone": "3.8.0-beta.93",
|
||||||
"@ohif/extension-default": "3.7.0",
|
"@ohif/extension-default": "3.8.0-beta.93",
|
||||||
"@ohif/i18n": "3.7.0",
|
"@ohif/i18n": "3.8.0-beta.93",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
@ -44,9 +46,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^1.20.3",
|
"@cornerstonejs/adapters": "^1.70.14",
|
||||||
"@cornerstonejs/tools": "^1.20.3",
|
"@cornerstonejs/core": "^1.70.14",
|
||||||
"@kitware/vtk.js": "27.3.1",
|
"@kitware/vtk.js": "30.4.1",
|
||||||
"react-color": "^2.19.3"
|
"react-color": "^2.19.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { cache, metaData } from '@cornerstonejs/core';
|
|||||||
import {
|
import {
|
||||||
segmentation as cornerstoneToolsSegmentation,
|
segmentation as cornerstoneToolsSegmentation,
|
||||||
Enums as cornerstoneToolsEnums,
|
Enums as cornerstoneToolsEnums,
|
||||||
|
utilities,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
|
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
|
||||||
import { classes, DicomMetadataStore } from '@ohif/core';
|
import { classes, DicomMetadataStore } from '@ohif/core';
|
||||||
@ -18,6 +19,7 @@ import {
|
|||||||
getUpdatedViewportsForSegmentation,
|
getUpdatedViewportsForSegmentation,
|
||||||
getTargetViewport,
|
getTargetViewport,
|
||||||
} from './utils/hydrationUtils';
|
} from './utils/hydrationUtils';
|
||||||
|
const { segmentation: segmentationUtils } = utilities;
|
||||||
|
|
||||||
const { datasetToBlob } = dcmjs.data;
|
const { datasetToBlob } = dcmjs.data;
|
||||||
|
|
||||||
@ -45,6 +47,7 @@ const commandsModule = ({
|
|||||||
uiDialogService,
|
uiDialogService,
|
||||||
displaySetService,
|
displaySetService,
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
|
toolGroupService,
|
||||||
} = (servicesManager as ServicesManager).services;
|
} = (servicesManager as ServicesManager).services;
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@ -203,6 +206,9 @@ const commandsModule = ({
|
|||||||
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
|
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
|
||||||
// Todo: handle adding more than one segmentation
|
// Todo: handle adding more than one segmentation
|
||||||
const displaySet = displaySets[0];
|
const displaySet = displaySets[0];
|
||||||
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||||
|
displaySet.referencedDisplaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
updateViewportsForSegmentationRendering({
|
updateViewportsForSegmentationRendering({
|
||||||
viewportId,
|
viewportId,
|
||||||
@ -218,7 +224,8 @@ const commandsModule = ({
|
|||||||
|
|
||||||
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
|
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
|
||||||
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
|
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
|
||||||
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
|
||||||
return segmentationId;
|
return segmentationId;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -397,6 +404,36 @@ const commandsModule = ({
|
|||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setBrushSize: ({ value, toolNames }) => {
|
||||||
|
const brushSize = Number(value);
|
||||||
|
|
||||||
|
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||||
|
if (toolNames?.length === 0) {
|
||||||
|
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize);
|
||||||
|
} else {
|
||||||
|
toolNames?.forEach(toolName => {
|
||||||
|
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setThresholdRange: ({
|
||||||
|
value,
|
||||||
|
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||||
|
}) => {
|
||||||
|
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||||
|
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||||
|
toolNames?.forEach(toolName => {
|
||||||
|
toolGroup.setToolConfiguration(toolName, {
|
||||||
|
strategySpecificConfiguration: {
|
||||||
|
THRESHOLD: {
|
||||||
|
threshold: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
@ -424,11 +461,18 @@ const commandsModule = ({
|
|||||||
downloadRTSS: {
|
downloadRTSS: {
|
||||||
commandFn: actions.downloadRTSS,
|
commandFn: actions.downloadRTSS,
|
||||||
},
|
},
|
||||||
|
setBrushSize: {
|
||||||
|
commandFn: actions.setBrushSize,
|
||||||
|
},
|
||||||
|
setThresholdRange: {
|
||||||
|
commandFn: actions.setThresholdRange,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actions,
|
actions,
|
||||||
definitions,
|
definitions,
|
||||||
|
defaultContext: 'SEGMENTATION',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
|
import { Toolbox } from '@ohif/ui';
|
||||||
import PanelSegmentation from './panels/PanelSegmentation';
|
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 { customizationService } = servicesManager.services;
|
||||||
|
|
||||||
const wrappedPanelSegmentation = configuration => {
|
const wrappedPanelSegmentation = configuration => {
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelSegmentation
|
<PanelSegmentation
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
@ -19,7 +23,8 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
|
|||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
configuration={{
|
configuration={{
|
||||||
...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 wrappedPanelSegmentationWithTools = configuration => {
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SegmentationToolbox
|
<Toolbox
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
|
buttonSectionId="segmentationToolbox"
|
||||||
|
title="Segmentation Tools"
|
||||||
configuration={{
|
configuration={{
|
||||||
...configuration,
|
...configuration,
|
||||||
}}
|
}}
|
||||||
@ -43,6 +51,8 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
|
|||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
configuration={{
|
configuration={{
|
||||||
...configuration,
|
...configuration,
|
||||||
|
disableEditing: appConfig.disableEditing,
|
||||||
|
...customizationService.get('segmentation.panel'),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
||||||
|
import { CONSTANTS } from '@cornerstonejs/tools';
|
||||||
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
||||||
|
|
||||||
import { SOPClassHandlerId } from './id';
|
import { SOPClassHandlerId } from './id';
|
||||||
@ -141,7 +142,7 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
|
|||||||
'@ohif/extension-cornerstone.utilityModule.common'
|
'@ohif/extension-cornerstone.utilityModule.common'
|
||||||
);
|
);
|
||||||
|
|
||||||
const { segmentationService } = servicesManager.services;
|
const { segmentationService, uiNotificationService } = servicesManager.services;
|
||||||
|
|
||||||
const { dicomLoaderService } = utilityModule.exports;
|
const { dicomLoaderService } = utilityModule.exports;
|
||||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
|
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
|
||||||
@ -174,12 +175,40 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
|
|||||||
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
|
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let usedRecommendedDisplayCIELabValue = true;
|
||||||
results.segMetadata.data.forEach((data, i) => {
|
results.segMetadata.data.forEach((data, i) => {
|
||||||
if (i > 0) {
|
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);
|
Object.assign(segDisplaySet, results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
extensions/cornerstone-dicom-seg/src/getToolbarModule.ts
Normal file
57
extensions/cornerstone-dicom-seg/src/getToolbarModule.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ import getSopClassHandlerModule from './getSopClassHandlerModule';
|
|||||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import getPanelModule from './getPanelModule';
|
import getPanelModule from './getPanelModule';
|
||||||
import getCommandsModule from './commandsModule';
|
import getCommandsModule from './commandsModule';
|
||||||
import preRegistration from './init';
|
import { getToolbarModule } from './getToolbarModule';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
|
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
|
||||||
@ -28,8 +28,6 @@ const extension = {
|
|||||||
* You ID can be anything you want, but it should be unique.
|
* You ID can be anything you want, but it should be unique.
|
||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
preRegistration,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PanelModule should provide a list of panels that will be available in OHIF
|
* PanelModule should provide a list of panels that will be available in OHIF
|
||||||
* for Modes to consume and render. Each panel is defined by a {name,
|
* for Modes to consume and render. Each panel is defined by a {name,
|
||||||
@ -38,8 +36,8 @@ const extension = {
|
|||||||
*/
|
*/
|
||||||
getPanelModule,
|
getPanelModule,
|
||||||
getCommandsModule,
|
getCommandsModule,
|
||||||
|
getToolbarModule,
|
||||||
getViewportModule({ servicesManager, extensionManager }) {
|
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||||
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
||||||
return (
|
return (
|
||||||
<OHIFCornerstoneSEGViewport
|
<OHIFCornerstoneSEGViewport
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
import { addTool, BrushTool } from '@cornerstonejs/tools';
|
|
||||||
|
|
||||||
export default function init({ configuration = {} }): void {
|
|
||||||
addTool(BrushTool);
|
|
||||||
}
|
|
||||||
@ -1,23 +1,35 @@
|
|||||||
import { createReportAsync } from '@ohif/extension-default';
|
import { createReportAsync } from '@ohif/extension-default';
|
||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { SegmentationGroupTable, LegacyButtonGroup, LegacyButton } from '@ohif/ui';
|
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui';
|
||||||
|
import { SegmentationPanelMode } from '../types/segmentation';
|
||||||
import callInputDialog from './callInputDialog';
|
import callInputDialog from './callInputDialog';
|
||||||
import callColorPickerDialog from './colorPickerDialog';
|
import callColorPickerDialog from './colorPickerDialog';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
|
||||||
|
[SegmentationPanelMode.Dropdown]: SegmentationGroupTable,
|
||||||
|
};
|
||||||
|
|
||||||
export default function PanelSegmentation({
|
export default function PanelSegmentation({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
configuration,
|
configuration,
|
||||||
}) {
|
}) {
|
||||||
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
|
const {
|
||||||
|
segmentationService,
|
||||||
|
viewportGridService,
|
||||||
|
uiDialogService,
|
||||||
|
displaySetService,
|
||||||
|
cornerstoneViewportService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
const { t } = useTranslation('PanelSegmentation');
|
const { t } = useTranslation('PanelSegmentation');
|
||||||
|
|
||||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||||
|
const [addSegmentationClassName, setAddSegmentationClassName] = useState('');
|
||||||
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
|
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
|
||||||
segmentationService.getConfiguration()
|
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 getToolGroupIds = segmentationId => {
|
||||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
||||||
|
|
||||||
@ -54,7 +124,9 @@ export default function PanelSegmentation({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSegmentationAdd = async () => {
|
const onSegmentationAdd = async () => {
|
||||||
commandsManager.runCommand('createEmptySegmentationForViewport');
|
commandsManager.runCommand('createEmptySegmentationForViewport', {
|
||||||
|
viewportId: viewportGridService.getActiveViewportId(),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSegmentationClick = (segmentationId: string) => {
|
const onSegmentationClick = (segmentationId: string) => {
|
||||||
@ -147,6 +219,7 @@ export default function PanelSegmentation({
|
|||||||
segmentationService.removeSegment(segmentationId, segmentIndex);
|
segmentationService.removeSegment(segmentationId, segmentIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// segment hide
|
||||||
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
const segmentInfo = segmentation.segments[segmentIndex];
|
const segmentInfo = segmentation.segments[segmentIndex];
|
||||||
@ -170,6 +243,22 @@ export default function PanelSegmentation({
|
|||||||
|
|
||||||
const onToggleSegmentationVisibility = segmentationId => {
|
const onToggleSegmentationVisibility = segmentationId => {
|
||||||
segmentationService.toggleSegmentationVisibility(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(
|
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 (
|
return (
|
||||||
<>
|
<SegmentationGroupTableComponent
|
||||||
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
|
title={t('Segmentations')}
|
||||||
<SegmentationGroupTable
|
segmentations={segmentations}
|
||||||
title={t('Segmentations')}
|
disableEditing={configuration.disableEditing}
|
||||||
segmentations={segmentations}
|
activeSegmentationId={selectedSegmentationId || ''}
|
||||||
disableEditing={configuration.disableEditing}
|
onSegmentationAdd={onSegmentationAddWrapper}
|
||||||
activeSegmentationId={selectedSegmentationId || ''}
|
addSegmentationClassName={addSegmentationClassName}
|
||||||
onSegmentationAdd={onSegmentationAdd}
|
showAddSegment={allowAddSegment}
|
||||||
onSegmentationClick={onSegmentationClick}
|
onSegmentationClick={onSegmentationClick}
|
||||||
onSegmentationDelete={onSegmentationDelete}
|
onSegmentationDelete={onSegmentationDelete}
|
||||||
onSegmentationDownload={onSegmentationDownload}
|
onSegmentationDownload={onSegmentationDownload}
|
||||||
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
||||||
storeSegmentation={storeSegmentation}
|
storeSegmentation={storeSegmentation}
|
||||||
onSegmentationEdit={onSegmentationEdit}
|
onSegmentationEdit={onSegmentationEdit}
|
||||||
onSegmentClick={onSegmentClick}
|
onSegmentClick={onSegmentClick}
|
||||||
onSegmentEdit={onSegmentEdit}
|
onSegmentEdit={onSegmentEdit}
|
||||||
onSegmentAdd={onSegmentAdd}
|
onSegmentAdd={onSegmentAdd}
|
||||||
onSegmentColorClick={onSegmentColorClick}
|
onSegmentColorClick={onSegmentColorClick}
|
||||||
onSegmentDelete={onSegmentDelete}
|
onSegmentDelete={onSegmentDelete}
|
||||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||||
onToggleSegmentLock={onToggleSegmentLock}
|
onToggleSegmentLock={onToggleSegmentLock}
|
||||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||||
showDeleteSegment={true}
|
showDeleteSegment={true}
|
||||||
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
||||||
setRenderOutline={value =>
|
setRenderOutline={value =>
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
||||||
}
|
}
|
||||||
setOutlineOpacityActive={value =>
|
setOutlineOpacityActive={value =>
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
||||||
}
|
}
|
||||||
setRenderFill={value =>
|
setRenderFill={value =>
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
||||||
}
|
}
|
||||||
setRenderInactiveSegmentations={value =>
|
setRenderInactiveSegmentations={value =>
|
||||||
_setSegmentationConfiguration(
|
_setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value)
|
||||||
selectedSegmentationId,
|
}
|
||||||
'renderInactiveSegmentations',
|
setOutlineWidthActive={value =>
|
||||||
value
|
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
||||||
)
|
}
|
||||||
}
|
setFillAlpha={value =>
|
||||||
setOutlineWidthActive={value =>
|
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
}
|
||||||
}
|
setFillAlphaInactive={value =>
|
||||||
setFillAlpha={value =>
|
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
}
|
||||||
}
|
/>
|
||||||
setFillAlphaInactive={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export enum SegmentationPanelMode {
|
||||||
|
Expanded = 'expanded',
|
||||||
|
Dropdown = 'dropdown',
|
||||||
|
}
|
||||||
@ -138,7 +138,7 @@ function getUpdatedViewportsForSegmentation({
|
|||||||
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
|
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
|
||||||
servicesManager.services;
|
servicesManager.services;
|
||||||
|
|
||||||
const { viewports } = viewportGridService.getState();
|
const { viewports, isHangingProtocolLayout } = viewportGridService.getState();
|
||||||
|
|
||||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
const targetViewportId = viewport.viewportOptions.viewportId;
|
||||||
@ -153,7 +153,8 @@ function getUpdatedViewportsForSegmentation({
|
|||||||
|
|
||||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||||
targetViewportId,
|
targetViewportId,
|
||||||
referenceDisplaySetInstanceUID
|
referenceDisplaySetInstanceUID,
|
||||||
|
isHangingProtocolLayout
|
||||||
);
|
);
|
||||||
|
|
||||||
viewports.forEach((viewport, viewportId) => {
|
viewports.forEach((viewport, viewportId) => {
|
||||||
@ -180,7 +181,8 @@ function getUpdatedViewportsForSegmentation({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return updatedViewports;
|
|
||||||
|
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -63,6 +63,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
|||||||
uiViewportDialogService.hide();
|
uiViewportDialogService.hide();
|
||||||
resolve(RESPONSE.CANCEL);
|
resolve(RESPONSE.CANCEL);
|
||||||
},
|
},
|
||||||
|
onKeyPress: event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import OHIF, { utils } from '@ohif/core';
|
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||||
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionBar } from '@ohif/ui';
|
|
||||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||||
import _getStatusComponent from './_getStatusComponent';
|
import _getStatusComponent from './_getStatusComponent';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
|
||||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||||
|
|
||||||
function OHIFCornerstoneSEGViewport(props) {
|
function OHIFCornerstoneSEGViewport(props) {
|
||||||
@ -15,7 +13,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
children,
|
children,
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportOptions,
|
viewportOptions,
|
||||||
viewportLabel,
|
|
||||||
servicesManager,
|
servicesManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
@ -28,8 +25,8 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
displaySetService,
|
displaySetService,
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
segmentationService,
|
segmentationService,
|
||||||
uiNotificationService,
|
|
||||||
customizationService,
|
customizationService,
|
||||||
|
viewportActionCornersService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
||||||
@ -114,16 +111,17 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
orientation: viewportOptions.orientation,
|
orientation: viewportOptions.orientation,
|
||||||
viewportId: viewportOptions.viewportId,
|
viewportId: viewportOptions.viewportId,
|
||||||
}}
|
}}
|
||||||
onElementEnabled={onElementEnabled}
|
onElementEnabled={evt => {
|
||||||
|
props.onElementEnabled?.(evt);
|
||||||
|
onElementEnabled(evt);
|
||||||
|
}}
|
||||||
onElementDisabled={onElementDisabled}
|
onElementDisabled={onElementDisabled}
|
||||||
// initialImageIndex={initialImageIndex}
|
|
||||||
></Component>
|
></Component>
|
||||||
);
|
);
|
||||||
}, [viewportId, segDisplaySet, toolGroupId]);
|
}, [viewportId, segDisplaySet, toolGroupId]);
|
||||||
|
|
||||||
const onSegmentChange = useCallback(
|
const onSegmentChange = useCallback(
|
||||||
direction => {
|
direction => {
|
||||||
direction = direction === 'left' ? -1 : 1;
|
|
||||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
|
||||||
@ -172,14 +170,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
|
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
|
||||||
setSegIsLoading(false);
|
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]);
|
}, [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;
|
let childrenWithProps = null;
|
||||||
|
|
||||||
@ -291,61 +344,8 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
SpacingBetweenSlices,
|
SpacingBetweenSlices,
|
||||||
} = referencedDisplaySetRef.current.metadata;
|
} = 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 (
|
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">
|
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||||
{segIsLoading && (
|
{segIsLoading && (
|
||||||
<LoadingIndicatorTotalPercent
|
<LoadingIndicatorTotalPercent
|
||||||
|
|||||||
@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
|||||||
let ToolTipMessage = null;
|
let ToolTipMessage = null;
|
||||||
let StatusIcon = null;
|
let StatusIcon = null;
|
||||||
|
|
||||||
const { t } = useTranslation('Common');
|
|
||||||
const loadStr = t('LOAD');
|
|
||||||
|
|
||||||
switch (isHydrated) {
|
switch (isHydrated) {
|
||||||
case true:
|
case true:
|
||||||
StatusIcon = () => <Icon name="status-alert" />;
|
StatusIcon = () => <Icon name="status-alert" />;
|
||||||
@ -26,23 +23,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
|||||||
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
|
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusArea = () => (
|
const StatusArea = () => {
|
||||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
const { t } = useTranslation('Common');
|
||||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
const loadStr = t('LOAD');
|
||||||
<StatusIcon />
|
|
||||||
<span className="ml-1">SEG</span>
|
return (
|
||||||
</div>
|
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||||
{!isHydrated && (
|
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||||
<div
|
<StatusIcon />
|
||||||
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
|
<span className="ml-1">SEG</span>
|
||||||
// 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>
|
||||||
)}
|
{!isHydrated && (
|
||||||
</div>
|
<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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -3,7 +3,820 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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
|
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||||
"version": "3.7.0",
|
"version": "3.8.0-beta.93",
|
||||||
"description": "OHIF extension for an SR Cornerstone Viewport",
|
"description": "OHIF extension for an SR Cornerstone Viewport",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -23,6 +23,8 @@
|
|||||||
"ohif-extension"
|
"ohif-extension"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"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": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||||
"dev:cornerstone": "yarn run dev",
|
"dev:cornerstone": "yarn run dev",
|
||||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
@ -32,11 +34,11 @@
|
|||||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.7.0",
|
"@ohif/core": "3.8.0-beta.93",
|
||||||
"@ohif/extension-cornerstone": "3.7.0",
|
"@ohif/extension-cornerstone": "3.8.0-beta.93",
|
||||||
"@ohif/extension-measurement-tracking": "3.7.0",
|
"@ohif/extension-measurement-tracking": "3.8.0-beta.93",
|
||||||
"@ohif/ui": "3.7.0",
|
"@ohif/ui": "3.8.0-beta.93",
|
||||||
"dcmjs": "^0.29.5",
|
"dcmjs": "^0.29.12",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
@ -44,9 +46,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^1.20.3",
|
"@cornerstonejs/adapters": "^1.70.14",
|
||||||
"@cornerstonejs/core": "^1.20.3",
|
"@cornerstonejs/core": "^1.70.14",
|
||||||
"@cornerstonejs/tools": "^1.20.3",
|
"@cornerstonejs/tools": "^1.70.14",
|
||||||
"classnames": "^2.3.2"
|
"classnames": "^2.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,9 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
|
|||||||
return dataset;
|
return dataset;
|
||||||
};
|
};
|
||||||
|
|
||||||
const commandsModule = ({}) => {
|
const commandsModule = props => {
|
||||||
|
const { servicesManager } = props;
|
||||||
|
const { customizationService } = servicesManager.services;
|
||||||
const actions = {
|
const actions = {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -95,7 +97,15 @@ const commandsModule = ({}) => {
|
|||||||
throw new Error('Invalid report, no content');
|
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) {
|
if (StudyInstanceUID) {
|
||||||
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
|
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||||
@ -118,13 +128,9 @@ const commandsModule = ({}) => {
|
|||||||
const definitions = {
|
const definitions = {
|
||||||
downloadReport: {
|
downloadReport: {
|
||||||
commandFn: actions.downloadReport,
|
commandFn: actions.downloadReport,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
storeMeasurements: {
|
storeMeasurements: {
|
||||||
commandFn: actions.storeMeasurements,
|
commandFn: actions.storeMeasurements,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
||||||
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
||||||
import addMeasurement from './utils/addMeasurement';
|
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
|
||||||
import isRehydratable from './utils/isRehydratable';
|
import isRehydratable from './utils/isRehydratable';
|
||||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||||
|
|
||||||
@ -150,13 +150,37 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
|||||||
return [displaySet];
|
return [displaySet];
|
||||||
}
|
}
|
||||||
|
|
||||||
function _load(displaySet, servicesManager, extensionManager) {
|
async function _load(displaySet, servicesManager, extensionManager) {
|
||||||
const { displaySetService, measurementService } = servicesManager.services;
|
const { displaySetService, measurementService } = servicesManager.services;
|
||||||
const dataSources = extensionManager.getDataSources();
|
const dataSources = extensionManager.getDataSources();
|
||||||
const dataSource = dataSources[0];
|
const dataSource = dataSources[0];
|
||||||
|
|
||||||
const { ContentSequence } = displaySet.instance;
|
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.referencedImages = _getReferencedImagesList(ContentSequence);
|
||||||
displaySet.measurements = _getMeasurements(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.
|
// Check currently added displaySets and add measurements if the sources exist.
|
||||||
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
|
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
|
||||||
_checkIfCanAddMeasurementsToDisplaySet(displaySet, activeDisplaySet, dataSource);
|
_checkIfCanAddMeasurementsToDisplaySet(
|
||||||
|
displaySet,
|
||||||
|
activeDisplaySet,
|
||||||
|
dataSource,
|
||||||
|
servicesManager
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to new displaySets as the source may come in after.
|
// Subscribe to new displaySets as the source may come in after.
|
||||||
@ -180,12 +209,23 @@ function _load(displaySet, servicesManager, extensionManager) {
|
|||||||
// If there are still some measurements that have not yet been loaded into cornerstone,
|
// 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.
|
// See if we can load them onto any of the new displaySets.
|
||||||
displaySetsAdded.forEach(newDisplaySet => {
|
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(
|
let unloadedMeasurements = srDisplaySet.measurements.filter(
|
||||||
measurement => measurement.loaded === false
|
measurement => measurement.loaded === false
|
||||||
);
|
);
|
||||||
@ -195,12 +235,16 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newDisplaySet instanceof ImageSet) {
|
if ((!newDisplaySet) instanceof ImageSet) {
|
||||||
// This also filters out _this_ displaySet, as it is not an ImageSet.
|
// This also filters out _this_ displaySet, as it is not an ImageSet.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sopClassUids, images } = newDisplaySet;
|
if (newDisplaySet.unsupported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sopClassUids } = newDisplaySet;
|
||||||
|
|
||||||
// Check if any have the newDisplaySet is the correct SOPClass.
|
// Check if any have the newDisplaySet is the correct SOPClass.
|
||||||
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
|
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
|
||||||
@ -240,9 +284,37 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat
|
|||||||
|
|
||||||
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
||||||
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
||||||
const measurement = unloadedMeasurements[j];
|
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)) {
|
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);
|
unloadedMeasurements.splice(j, 1);
|
||||||
}
|
}
|
||||||
@ -258,7 +330,7 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
|
|||||||
// Standard. But for now, we will support only one ReferenceFrameNumber.
|
// Standard. But for now, we will support only one ReferenceFrameNumber.
|
||||||
const ReferencedFrameNumber =
|
const ReferencedFrameNumber =
|
||||||
(measurement.coords[0].ReferencedSOPSequence &&
|
(measurement.coords[0].ReferencedSOPSequence &&
|
||||||
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
|
||||||
1;
|
1;
|
||||||
|
|
||||||
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
|
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||||
import getHangingProtocolModule, { srProtocol } from './getHangingProtocolModule';
|
import { srProtocol } from './getHangingProtocolModule';
|
||||||
import onModeEnter from './onModeEnter';
|
import onModeEnter from './onModeEnter';
|
||||||
import getCommandsModule from './commandsModule';
|
import getCommandsModule from './commandsModule';
|
||||||
import preRegistration from './init';
|
import preRegistration from './init';
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
addTool,
|
|
||||||
AngleTool,
|
AngleTool,
|
||||||
annotation,
|
annotation,
|
||||||
ArrowAnnotateTool,
|
ArrowAnnotateTool,
|
||||||
@ -9,6 +8,7 @@ import {
|
|||||||
CircleROITool,
|
CircleROITool,
|
||||||
LengthTool,
|
LengthTool,
|
||||||
PlanarFreehandROITool,
|
PlanarFreehandROITool,
|
||||||
|
RectangleROITool,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||||
import addToolInstance from './utils/addToolInstance';
|
import addToolInstance from './utils/addToolInstance';
|
||||||
@ -19,24 +19,25 @@ import toolNames from './tools/toolNames';
|
|||||||
* @param {object} configuration
|
* @param {object} configuration
|
||||||
*/
|
*/
|
||||||
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
|
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
|
||||||
addTool(DICOMSRDisplayTool);
|
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
|
||||||
addToolInstance(toolNames.SRLength, LengthTool, {});
|
addToolInstance(toolNames.SRLength, LengthTool);
|
||||||
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
||||||
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
|
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
|
||||||
addToolInstance(toolNames.SRCircleROI, CircleROITool);
|
addToolInstance(toolNames.SRCircleROI, CircleROITool);
|
||||||
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
|
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
|
||||||
addToolInstance(toolNames.SRAngle, AngleTool);
|
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
|
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
|
||||||
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
|
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
|
// Modify annotation tools to use dashed lines on SR
|
||||||
const dashedLine = {
|
const dashedLine = {
|
||||||
lineDash: '4,4',
|
lineDash: '4,4',
|
||||||
};
|
};
|
||||||
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
|
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
|
||||||
|
[toolNames.DICOMSRDisplay]: dashedLine,
|
||||||
SRLength: dashedLine,
|
SRLength: dashedLine,
|
||||||
SRBidirectional: dashedLine,
|
SRBidirectional: dashedLine,
|
||||||
SREllipticalROI: dashedLine,
|
SREllipticalROI: dashedLine,
|
||||||
@ -45,6 +46,7 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
|
|||||||
SRCobbAngle: dashedLine,
|
SRCobbAngle: dashedLine,
|
||||||
SRAngle: dashedLine,
|
SRAngle: dashedLine,
|
||||||
SRPlanarFreehandROI: dashedLine,
|
SRPlanarFreehandROI: dashedLine,
|
||||||
|
SRRectangleROI: dashedLine,
|
||||||
global: {},
|
global: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,14 +22,14 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getTextBoxLinesFromLabels(labels) {
|
_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 = [];
|
const lines = [];
|
||||||
|
|
||||||
for (let i = 0; i < labelLength; i++) {
|
for (let i = 0; i < labelLength; i++) {
|
||||||
const labelEntry = labels[i];
|
const labelEntry = labels[i];
|
||||||
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
|
lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
@ -65,7 +65,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
|
|
||||||
// Filter toolData to only render the data for the active SR.
|
// Filter toolData to only render the data for the active SR.
|
||||||
const filteredAnnotations = annotations.filter(annotation =>
|
const filteredAnnotations = annotations.filter(annotation =>
|
||||||
trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier)
|
trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!viewport._actors?.size) {
|
if (!viewport._actors?.size) {
|
||||||
@ -77,20 +77,24 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
toolName: this.getToolName(),
|
toolName: this.getToolName(),
|
||||||
viewportId: enabledElement.viewport.id,
|
viewportId: enabledElement.viewport.id,
|
||||||
};
|
};
|
||||||
|
const { style: annotationStyle } = annotation.config;
|
||||||
|
|
||||||
for (let i = 0; i < filteredAnnotations.length; i++) {
|
for (let i = 0; i < filteredAnnotations.length; i++) {
|
||||||
const annotation = filteredAnnotations[i];
|
const annotation = filteredAnnotations[i];
|
||||||
const annotationUID = annotation.annotationUID;
|
const annotationUID = annotation.annotationUID;
|
||||||
const { renderableData } = annotation.data.cachedStats;
|
const { renderableData, TrackingUniqueIdentifier } = annotation.data;
|
||||||
const { cachedStats } = annotation.data;
|
|
||||||
const { referencedImageId } = annotation.metadata;
|
const { referencedImageId } = annotation.metadata;
|
||||||
|
|
||||||
styleSpecifier.annotationUID = annotationUID;
|
styleSpecifier.annotationUID = annotationUID;
|
||||||
|
|
||||||
|
const groupStyle = annotationStyle.getToolGroupToolStyles(this.toolGroupId)[
|
||||||
|
this.getToolName()
|
||||||
|
];
|
||||||
|
|
||||||
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
||||||
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
||||||
const color =
|
const color =
|
||||||
cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||||
? 'rgb(0, 255, 0)'
|
? 'rgb(0, 255, 0)'
|
||||||
: this.getStyle('color', styleSpecifier, annotation);
|
: this.getStyle('color', styleSpecifier, annotation);
|
||||||
|
|
||||||
@ -98,6 +102,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
color,
|
color,
|
||||||
lineDash,
|
lineDash,
|
||||||
lineWidth,
|
lineWidth,
|
||||||
|
...groupStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(renderableData).forEach(GraphicType => {
|
Object.keys(renderableData).forEach(GraphicType => {
|
||||||
@ -160,6 +165,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
const drawingOptions = {
|
const drawingOptions = {
|
||||||
color: options.color,
|
color: options.color,
|
||||||
width: options.lineWidth,
|
width: options.lineWidth,
|
||||||
|
lineDash: options.lineDash,
|
||||||
};
|
};
|
||||||
let allCanvasCoordinates = [];
|
let allCanvasCoordinates = [];
|
||||||
renderableData.map((data, index) => {
|
renderableData.map((data, index) => {
|
||||||
@ -307,6 +313,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
{
|
{
|
||||||
color: options.color,
|
color: options.color,
|
||||||
width: options.lineWidth,
|
width: options.lineWidth,
|
||||||
|
lineDash: options.lineDash,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -339,7 +346,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
const textLines = this._getTextBoxLinesFromLabels(label);
|
const textLines = this._getTextBoxLinesFromLabels(label);
|
||||||
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
|
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);
|
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
import { vec3 } from 'gl-matrix';
|
import { vec3 } from 'gl-matrix';
|
||||||
import { Types, annotation } from '@cornerstonejs/tools';
|
import { Types, annotation } from '@cornerstonejs/tools';
|
||||||
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
|
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
|
||||||
|
|
||||||
import toolNames from '../tools/toolNames';
|
import toolNames from '../tools/toolNames';
|
||||||
import SCOORD_TYPES from '../constants/scoordTypes';
|
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||||
|
|
||||||
const EPSILON = 1e-4;
|
const EPSILON = 1e-4;
|
||||||
|
|
||||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) {
|
||||||
|
|
||||||
export default function addMeasurement(measurement, imageId, displaySetInstanceUID) {
|
|
||||||
// TODO -> Render rotated ellipse .
|
|
||||||
const toolName = toolNames.DICOMSRDisplay;
|
const toolName = toolNames.DICOMSRDisplay;
|
||||||
|
|
||||||
const measurementData = {
|
const measurementData = {
|
||||||
@ -27,59 +25,43 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
|
|||||||
}
|
}
|
||||||
|
|
||||||
measurementData.renderableData[GraphicType].push(
|
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 imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
||||||
|
|
||||||
const annotationManager = annotation.state.getAnnotationManager();
|
/**
|
||||||
|
* This annotation (DICOMSRDisplay) is only used by the SR viewport.
|
||||||
// Create Cornerstone3D Annotation from measurement
|
* This is used before the annotation is hydrated. If hydrated the measurement will be added
|
||||||
const frameNumber =
|
* to the measurement service and will be available for the other viewports.
|
||||||
(measurement.coords[0].ReferencedSOPSequence &&
|
*/
|
||||||
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
|
||||||
1;
|
|
||||||
|
|
||||||
const SRAnnotation: Types.Annotation = {
|
const SRAnnotation: Types.Annotation = {
|
||||||
annotationUID: measurement.TrackingUniqueIdentifier,
|
annotationUID: measurement.TrackingUniqueIdentifier,
|
||||||
|
highlighted: false,
|
||||||
|
isLocked: false,
|
||||||
|
invalidated: false,
|
||||||
metadata: {
|
metadata: {
|
||||||
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
|
||||||
toolName: toolName,
|
toolName: toolName,
|
||||||
|
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
||||||
referencedImageId: imageId,
|
referencedImageId: imageId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
label: measurement.labels,
|
label: measurement.labels,
|
||||||
handles: {
|
handles: {
|
||||||
textBox: {},
|
textBox: measurement.textBox ?? {},
|
||||||
},
|
},
|
||||||
cachedStats: {
|
cachedStats: {},
|
||||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||||
renderableData: measurementData.renderableData,
|
renderableData: measurementData.renderableData,
|
||||||
},
|
frameNumber,
|
||||||
frameNumber: frameNumber,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const annotationManager = annotation.state.getAnnotationManager();
|
||||||
annotationManager.addAnnotation(SRAnnotation);
|
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) {
|
function _getRenderableData(GraphicType, GraphicData, imageId) {
|
||||||
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
|
||||||
|
|
||||||
let renderableData: csTypes.Point3[];
|
let renderableData: csTypes.Point3[];
|
||||||
|
|
||||||
switch (GraphicType) {
|
switch (GraphicType) {
|
||||||
@ -90,7 +90,7 @@ export default function hydrateStructuredReport(
|
|||||||
const datasetToUse = _mapLegacyDataSet(instance);
|
const datasetToUse = _mapLegacyDataSet(instance);
|
||||||
|
|
||||||
// Use dcmjs to generate toolState.
|
// Use dcmjs to generate toolState.
|
||||||
const storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
|
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
|
||||||
datasetToUse,
|
datasetToUse,
|
||||||
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
|
// 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
|
// for the imageToWorld transformation. The following assumes that the order
|
||||||
@ -101,6 +101,16 @@ export default function hydrateStructuredReport(
|
|||||||
metaData
|
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.
|
// Filter what is found by DICOM SR to measurements we support.
|
||||||
const mappingDefinitions = mappings.map(m => m.annotationType);
|
const mappingDefinitions = mappings.map(m => m.annotationType);
|
||||||
const hydratableMeasurementsInSR = {};
|
const hydratableMeasurementsInSR = {};
|
||||||
|
|||||||
@ -1,35 +1,31 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { 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 hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
|
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
|
||||||
const { formatDate } = utils;
|
|
||||||
|
|
||||||
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
||||||
|
|
||||||
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||||
|
|
||||||
function OHIFCornerstoneSRViewport(props) {
|
function OHIFCornerstoneSRViewport(props) {
|
||||||
const {
|
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
|
||||||
children,
|
props;
|
||||||
dataSource,
|
|
||||||
displaySets,
|
|
||||||
viewportLabel,
|
|
||||||
viewportOptions,
|
|
||||||
servicesManager,
|
|
||||||
extensionManager,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
const { displaySetService, cornerstoneViewportService, measurementService } =
|
const {
|
||||||
servicesManager.services;
|
displaySetService,
|
||||||
|
cornerstoneViewportService,
|
||||||
|
measurementService,
|
||||||
|
viewportActionCornersService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
const viewportId = viewportOptions.viewportId;
|
const viewportId = viewportOptions.viewportId;
|
||||||
|
|
||||||
@ -48,6 +44,8 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
const [element, setElement] = useState(null);
|
const [element, setElement] = useState(null);
|
||||||
const { viewports, activeViewportId } = viewportGrid;
|
const { viewports, activeViewportId } = viewportGrid;
|
||||||
|
|
||||||
|
const { t } = useTranslation('Common');
|
||||||
|
|
||||||
// Optional hook into tracking extension, if present.
|
// Optional hook into tracking extension, if present.
|
||||||
let trackedMeasurements;
|
let trackedMeasurements;
|
||||||
let sendTrackedMeasurementsEvent;
|
let sendTrackedMeasurementsEvent;
|
||||||
@ -128,6 +126,10 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
console.warn('More than one SOPClassUID in the same series is not yet supported.');
|
console.warn('More than one SOPClassUID in the same series is not yet supported.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
_getViewportReferencedDisplaySetData(
|
_getViewportReferencedDisplaySetData(
|
||||||
srDisplaySet,
|
srDisplaySet,
|
||||||
newMeasurementSelected,
|
newMeasurementSelected,
|
||||||
@ -202,7 +204,10 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
// The positionIds for the viewport aren't meaningful for the child display sets
|
// The positionIds for the viewport aren't meaningful for the child display sets
|
||||||
positionIds: null,
|
positionIds: null,
|
||||||
}}
|
}}
|
||||||
onElementEnabled={onElementEnabled}
|
onElementEnabled={evt => {
|
||||||
|
props.onElementEnabled?.(evt);
|
||||||
|
onElementEnabled(evt);
|
||||||
|
}}
|
||||||
initialImageIndex={initialImageIndex}
|
initialImageIndex={initialImageIndex}
|
||||||
isJumpToMeasurementDisabled={true}
|
isJumpToMeasurementDisabled={true}
|
||||||
></Component>
|
></Component>
|
||||||
@ -213,18 +218,11 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
direction => {
|
direction => {
|
||||||
let newMeasurementSelected = measurementSelected;
|
let newMeasurementSelected = measurementSelected;
|
||||||
|
|
||||||
if (direction === 'right') {
|
newMeasurementSelected += direction;
|
||||||
newMeasurementSelected++;
|
if (newMeasurementSelected >= measurementCount) {
|
||||||
|
newMeasurementSelected = 0;
|
||||||
if (newMeasurementSelected >= measurementCount) {
|
} else if (newMeasurementSelected < 0) {
|
||||||
newMeasurementSelected = 0;
|
newMeasurementSelected = measurementCount - 1;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newMeasurementSelected--;
|
|
||||||
|
|
||||||
if (newMeasurementSelected < 0) {
|
|
||||||
newMeasurementSelected = measurementCount - 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTrackingIdentifiers(newMeasurementSelected);
|
setTrackingIdentifiers(newMeasurementSelected);
|
||||||
@ -263,12 +261,16 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
* if it is hydrated we don't even use the SR viewport.
|
* if it is hydrated we don't even use the SR viewport.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!srDisplaySet.isLoaded) {
|
const loadSR = async () => {
|
||||||
srDisplaySet.load();
|
if (!srDisplaySet.isLoaded) {
|
||||||
}
|
await srDisplaySet.load();
|
||||||
const numMeasurements = srDisplaySet.measurements.length;
|
}
|
||||||
setMeasurementCount(numMeasurements);
|
const numMeasurements = srDisplaySet.measurements.length;
|
||||||
}, [srDisplaySet]);
|
setMeasurementCount(numMeasurements);
|
||||||
|
updateViewport(measurementSelected);
|
||||||
|
};
|
||||||
|
loadSR();
|
||||||
|
}, [dataSource, srDisplaySet]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to update the tracking identifiers when the selected measurement changes or
|
* 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,
|
* Todo: what is this, not sure what it does regarding the react aspect,
|
||||||
* it is updating a local variable? which is not state.
|
* 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(() => {
|
useEffect(() => {
|
||||||
isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
|
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||||
}, [trackedMeasurements]);
|
}, [trackedMeasurements]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Data fetching for the SR displaySet, which updates the measurements and
|
|
||||||
* also gets the referenced image displaySet that SR is based on.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateViewport(measurementSelected);
|
viewportActionCornersService.setComponents([
|
||||||
}, [dataSource, srDisplaySet]);
|
{
|
||||||
|
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;
|
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 (
|
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">
|
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||||
{getCornerstoneViewport()}
|
{getCornerstoneViewport()}
|
||||||
{childrenWithProps}
|
{childrenWithProps}
|
||||||
@ -384,7 +369,6 @@ OHIFCornerstoneSRViewport.propTypes = {
|
|||||||
viewportLabel: PropTypes.string,
|
viewportLabel: PropTypes.string,
|
||||||
customProps: PropTypes.object,
|
customProps: PropTypes.object,
|
||||||
viewportOptions: PropTypes.object,
|
viewportOptions: PropTypes.object,
|
||||||
viewportLabel: PropTypes.string,
|
|
||||||
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
||||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||||
};
|
};
|
||||||
@ -402,6 +386,13 @@ async function _getViewportReferencedDisplaySetData(
|
|||||||
const measurement = measurements[measurementSelected];
|
const measurement = measurements[measurementSelected];
|
||||||
|
|
||||||
const { displaySetInstanceUID } = measurement;
|
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);
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||||
|
|
||||||
@ -429,6 +420,7 @@ function _getStatusComponent({
|
|||||||
isRehydratable,
|
isRehydratable,
|
||||||
isLocked,
|
isLocked,
|
||||||
sendTrackedMeasurementsEvent,
|
sendTrackedMeasurementsEvent,
|
||||||
|
t,
|
||||||
}) {
|
}) {
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
||||||
@ -437,7 +429,6 @@ function _getStatusComponent({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { t } = useTranslation('Common');
|
|
||||||
const loadStr = t('LOAD');
|
const loadStr = t('LOAD');
|
||||||
|
|
||||||
// 1 - Incompatible
|
// 1 - Incompatible
|
||||||
|
|||||||
@ -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 });
|
||||||
|
};
|
||||||
@ -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`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
196
extensions/cornerstone-dynamic-volume/CHANGELOG.md
Normal file
196
extensions/cornerstone-dynamic-volume/CHANGELOG.md
Normal 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))
|
||||||
20
extensions/cornerstone-dynamic-volume/LICENSE
Normal file
20
extensions/cornerstone-dynamic-volume/LICENSE
Normal 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.
|
||||||
8
extensions/cornerstone-dynamic-volume/README.md
Normal file
8
extensions/cornerstone-dynamic-volume/README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# cornerstone-dynamic-volume
|
||||||
|
## Description
|
||||||
|
|
||||||
|
## Author
|
||||||
|
OHIF
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT
|
||||||
1
extensions/cornerstone-dynamic-volume/babel.config.js
Normal file
1
extensions/cornerstone-dynamic-volume/babel.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('../../babel.config.js');
|
||||||
50
extensions/cornerstone-dynamic-volume/package.json
Normal file
50
extensions/cornerstone-dynamic-volume/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import updateSegmentationsChartDisplaySet from './updateSegmentationsChartDisplaySet';
|
||||||
|
|
||||||
|
export { updateSegmentationsChartDisplaySet };
|
||||||
@ -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 };
|
||||||
407
extensions/cornerstone-dynamic-volume/src/commandsModule.ts
Normal file
407
extensions/cornerstone-dynamic-volume/src/commandsModule.ts
Normal 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;
|
||||||
@ -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;
|
||||||
68
extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx
Normal file
68
extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx
Normal 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;
|
||||||
6
extensions/cornerstone-dynamic-volume/src/id.js
Normal file
6
extensions/cornerstone-dynamic-volume/src/id.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import packageJson from '../package.json';
|
||||||
|
|
||||||
|
const id = packageJson.name;
|
||||||
|
const SOPClassHandlerName = 'dynamic-volume';
|
||||||
|
|
||||||
|
export { id, SOPClassHandlerName };
|
||||||
57
extensions/cornerstone-dynamic-volume/src/index.ts
Normal file
57
extensions/cornerstone-dynamic-volume/src/index.ts
Normal 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 };
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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,
|
||||||
|
};
|
||||||
@ -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;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import DynamicDataPanel from './DynamicDataPanel';
|
||||||
|
import WorkflowPanel from './WorkflowPanel';
|
||||||
|
import PanelGenerateImage from './PanelGenerateImage';
|
||||||
|
|
||||||
|
export { DynamicDataPanel, WorkflowPanel, PanelGenerateImage };
|
||||||
@ -3,7 +3,893 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "3.7.0",
|
"version": "3.8.0-beta.93",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -25,6 +25,8 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||||
"dev:cornerstone": "yarn run dev",
|
"dev:cornerstone": "yarn run dev",
|
||||||
"build": "cross-env NODE_ENV=production webpack --progress --config .webpack/webpack.prod.js",
|
"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-libjpeg-turbo-8bit": "^1.2.2",
|
||||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||||
"@cornerstonejs/codec-openjph": "^2.4.2",
|
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||||
"@cornerstonejs/dicom-image-loader": "^1.20.3",
|
"@cornerstonejs/dicom-image-loader": "^1.70.14",
|
||||||
"@ohif/core": "3.7.0",
|
"@icr/polyseg-wasm": "^0.4.0",
|
||||||
"@ohif/ui": "3.7.0",
|
"@ohif/core": "3.8.0-beta.93",
|
||||||
"dcmjs": "^0.29.6",
|
"@ohif/ui": "3.8.0-beta.93",
|
||||||
|
"dcmjs": "^0.29.12",
|
||||||
"dicom-parser": "^1.8.21",
|
"dicom-parser": "^1.8.21",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
@ -52,11 +55,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^1.20.3",
|
"@cornerstonejs/adapters": "^1.70.14",
|
||||||
"@cornerstonejs/core": "^1.20.3",
|
"@cornerstonejs/core": "^1.70.14",
|
||||||
"@cornerstonejs/streaming-image-volume-loader": "^1.20.3",
|
"@cornerstonejs/streaming-image-volume-loader": "^1.70.14",
|
||||||
"@cornerstonejs/tools": "^1.20.3",
|
"@cornerstonejs/tools": "^1.70.14",
|
||||||
"@kitware/vtk.js": "27.3.1",
|
"@icr/polyseg-wasm": "^0.4.0",
|
||||||
|
"@kitware/vtk.js": "30.4.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
utilities as csUtils,
|
utilities as csUtils,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import { MeasurementService } from '@ohif/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 { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
|
||||||
|
|
||||||
import { setEnabledElement } from '../state';
|
import { setEnabledElement } from '../state';
|
||||||
@ -22,6 +22,12 @@ import CornerstoneServices from '../types/CornerstoneServices';
|
|||||||
import CinePlayer from '../components/CinePlayer';
|
import CinePlayer from '../components/CinePlayer';
|
||||||
import { Types } from '@ohif/core';
|
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';
|
const STACK = 'stack';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,7 +107,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
viewportOptions,
|
viewportOptions,
|
||||||
displaySetOptions,
|
displaySetOptions,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
|
||||||
onElementEnabled,
|
onElementEnabled,
|
||||||
onElementDisabled,
|
onElementDisabled,
|
||||||
isJumpToMeasurementDisabled,
|
isJumpToMeasurementDisabled,
|
||||||
@ -109,12 +114,32 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||||
// to set the initial state of the viewport's first image to render
|
// to set the initial state of the viewport's first image to render
|
||||||
initialImageIndex,
|
initialImageIndex,
|
||||||
|
// 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;
|
} = props;
|
||||||
|
|
||||||
const viewportId = viewportOptions.viewportId;
|
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 [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||||
const elementRef = useRef();
|
const elementRef = useRef();
|
||||||
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
measurementService,
|
measurementService,
|
||||||
@ -126,12 +151,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
cornerstoneCacheService,
|
cornerstoneCacheService,
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
stateSyncService,
|
stateSyncService,
|
||||||
|
viewportActionCornersService,
|
||||||
} = servicesManager.services as CornerstoneServices;
|
} = servicesManager.services as CornerstoneServices;
|
||||||
|
|
||||||
const [viewportDialogState] = useViewportDialog();
|
const [viewportDialogState] = useViewportDialog();
|
||||||
// useCallback for scroll bar height calculation
|
// useCallback for scroll bar height calculation
|
||||||
const setImageScrollBarHeight = useCallback(() => {
|
const setImageScrollBarHeight = useCallback(() => {
|
||||||
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
|
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
|
||||||
setScrollbarHeight(scrollbarHeight);
|
setScrollbarHeight(scrollbarHeight);
|
||||||
}, [elementRef]);
|
}, [elementRef]);
|
||||||
|
|
||||||
@ -151,6 +177,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
||||||
|
|
||||||
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
||||||
|
|
||||||
|
viewportActionCornersService.clear(viewportId);
|
||||||
},
|
},
|
||||||
[viewportId]
|
[viewportId]
|
||||||
);
|
);
|
||||||
@ -175,6 +203,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
|
|
||||||
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
|
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) {
|
if (onElementEnabled) {
|
||||||
onElementEnabled(evt);
|
onElementEnabled(evt);
|
||||||
}
|
}
|
||||||
@ -197,13 +232,18 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanUpServices(viewportInfo);
|
|
||||||
cornerstoneViewportService.storePresentation({ viewportId });
|
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) {
|
if (onElementDisabled) {
|
||||||
onElementDisabled(viewportInfo);
|
onElementDisabled(viewportInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cornerstoneViewportService.disableElement(viewportId);
|
||||||
|
|
||||||
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
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
|
// The presentation state will have been stored previously by closing
|
||||||
// a viewport. Otherwise, this viewport will be unchanged and the
|
// a viewport. Otherwise, this viewport will be unchanged and the
|
||||||
// presentation information will be directly carried over.
|
// presentation information will be directly carried over.
|
||||||
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
|
const state = stateSyncService.getState();
|
||||||
|
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
|
||||||
|
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
|
||||||
|
|
||||||
const { presentationIds } = viewportOptions;
|
const { presentationIds } = viewportOptions;
|
||||||
const presentations = {
|
const presentations = {
|
||||||
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
||||||
@ -344,12 +387,38 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
};
|
};
|
||||||
}, [displaySets, elementRef, viewportId]);
|
}, [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 (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="viewport-wrapper">
|
<div className="viewport-wrapper">
|
||||||
<ReactResizeDetector
|
<ReactResizeDetector
|
||||||
refreshMode="debounce"
|
|
||||||
refreshRate={50} // Wait 50 ms after last move to render
|
|
||||||
onResize={onResize}
|
onResize={onResize}
|
||||||
targetRef={elementRef.current}
|
targetRef={elementRef.current}
|
||||||
/>
|
/>
|
||||||
@ -373,7 +442,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute w-full">
|
{/* top offset of 24px to account for ViewportActionCorners. */}
|
||||||
|
<div className="absolute top-[24px] w-full">
|
||||||
{viewportDialogState.viewportId === viewportId && (
|
{viewportDialogState.viewportId === viewportId && (
|
||||||
<Notification
|
<Notification
|
||||||
id="viewport-notification"
|
id="viewport-notification"
|
||||||
@ -382,9 +452,12 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
actions={viewportDialogState.actions}
|
actions={viewportDialogState.actions}
|
||||||
onSubmit={viewportDialogState.onSubmit}
|
onSubmit={viewportDialogState.onSubmit}
|
||||||
onOutsideClick={viewportDialogState.onOutsideClick}
|
onOutsideClick={viewportDialogState.onOutsideClick}
|
||||||
|
onKeyPress={viewportDialogState.onKeyPress}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
|
||||||
|
<OHIFViewportActionCorners viewportId={viewportId} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}, areEqual);
|
}, areEqual);
|
||||||
@ -412,7 +485,10 @@ function _subscribeToJumpToMeasurementEvents(
|
|||||||
cornerstoneViewportService.getViewportIdToJump(
|
cornerstoneViewportService.getViewportIdToJump(
|
||||||
jumpId,
|
jumpId,
|
||||||
measurement.displaySetInstanceUID,
|
measurement.displaySetInstanceUID,
|
||||||
{ referencedImageId: measurement.referencedImageId }
|
{
|
||||||
|
referencedImageId:
|
||||||
|
measurement.referencedImageId || measurement.metadata?.referencedImageId,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
|
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
|
||||||
@ -521,6 +597,9 @@ function _jumpToMeasurement(
|
|||||||
i => i.SOPInstanceUID === SOPInstanceUID
|
i => i.SOPInstanceUID === SOPInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// the index is reversed in the volume viewport
|
||||||
|
// imageIdIndex = referencedDisplaySet.images.length - 1 - imageIdIndex;
|
||||||
|
|
||||||
const { viewPlaneNormal: viewportViewPlane } = viewport.getCamera();
|
const { viewPlaneNormal: viewportViewPlane } = viewport.getCamera();
|
||||||
|
|
||||||
// should compare abs for both planes since the direction can be flipped
|
// 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
|
// Component displayName
|
||||||
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
|
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
|
||||||
|
|
||||||
|
|||||||
@ -2,21 +2,25 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { vec3 } from 'gl-matrix';
|
import { vec3 } from 'gl-matrix';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||||
import { ViewportOverlay } from '@ohif/ui';
|
|
||||||
import { 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 { 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';
|
import './CustomizableViewportOverlay.css';
|
||||||
|
|
||||||
const EPSILON = 1e-4;
|
const EPSILON = 1e-4;
|
||||||
|
|
||||||
|
type ViewportData = StackViewportData | VolumeViewportData;
|
||||||
|
|
||||||
interface OverlayItemProps {
|
interface OverlayItemProps {
|
||||||
element: any;
|
element: HTMLElement;
|
||||||
viewportData: any;
|
viewportData: ViewportData;
|
||||||
imageSliceData: ImageSliceData;
|
imageSliceData: ImageSliceData;
|
||||||
servicesManager: ServicesManager;
|
servicesManager: ServicesManager;
|
||||||
|
viewportId: string;
|
||||||
instance: InstanceMetadata;
|
instance: InstanceMetadata;
|
||||||
customization: any;
|
customization: any;
|
||||||
formatters: {
|
formatters: {
|
||||||
@ -35,67 +39,11 @@ interface OverlayItemProps {
|
|||||||
scale?: number;
|
scale?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const OverlayItemComponents = {
|
||||||
* Window Level / Center Overlay item
|
'ohif.overlayItem.windowLevel': VOIOverlayItem,
|
||||||
*/
|
'ohif.overlayItem.zoomLevel': ZoomOverlayItem,
|
||||||
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Customizable Viewport Overlay
|
* Customizable Viewport Overlay
|
||||||
@ -106,12 +54,17 @@ function CustomizableViewportOverlay({
|
|||||||
imageSliceData,
|
imageSliceData,
|
||||||
viewportId,
|
viewportId,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
|
}: {
|
||||||
|
element: HTMLElement;
|
||||||
|
viewportData: ViewportData;
|
||||||
|
imageSliceData: ImageSliceData;
|
||||||
|
viewportId: string;
|
||||||
|
servicesManager: ServicesManager;
|
||||||
}) {
|
}) {
|
||||||
const { toolbarService, cornerstoneViewportService, customizationService } =
|
const { cornerstoneViewportService, customizationService, toolGroupService } =
|
||||||
servicesManager.services;
|
servicesManager.services;
|
||||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||||
const [scale, setScale] = useState(1);
|
const [scale, setScale] = useState(1);
|
||||||
const [activeTools, setActiveTools] = useState([]);
|
|
||||||
const { imageIndex } = imageSliceData;
|
const { imageIndex } = imageSliceData;
|
||||||
|
|
||||||
const topLeftCustomization = customizationService.getModeCustomization(
|
const topLeftCustomization = customizationService.getModeCustomization(
|
||||||
@ -127,27 +80,21 @@ function CustomizableViewportOverlay({
|
|||||||
'cornerstoneOverlayBottomRight'
|
'cornerstoneOverlayBottomRight'
|
||||||
);
|
);
|
||||||
|
|
||||||
const instance = useMemo(() => {
|
const instances = useMemo(() => {
|
||||||
if (viewportData != null) {
|
if (viewportData != null) {
|
||||||
return _getViewportInstance(viewportData, imageIndex);
|
return _getViewportInstances(viewportData);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [viewportData, imageIndex]);
|
}, [viewportData, imageIndex]);
|
||||||
|
|
||||||
const instanceNumber = useMemo(() => {
|
const instanceNumber = useMemo(
|
||||||
if (viewportData != null) {
|
() =>
|
||||||
return _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService);
|
viewportData
|
||||||
}
|
? getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService)
|
||||||
return null;
|
: null,
|
||||||
}, [viewportData, viewportId, imageIndex, cornerstoneViewportService]);
|
[viewportData, viewportId, imageIndex, cornerstoneViewportService]
|
||||||
|
);
|
||||||
/**
|
|
||||||
* Initial toolbar state
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
setActiveTools(toolbarService.getActiveTools());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updating the VOI when the viewport changes its voi
|
* Updating the VOI when the viewport changes its voi
|
||||||
@ -215,26 +162,9 @@ function CustomizableViewportOverlay({
|
|||||||
};
|
};
|
||||||
}, [viewportId, viewportData, cornerstoneViewportService, element]);
|
}, [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(
|
const _renderOverlayItem = useCallback(
|
||||||
item => {
|
item => {
|
||||||
const overlayItemProps: OverlayItemProps = {
|
const overlayItemProps = {
|
||||||
element,
|
element,
|
||||||
viewportData,
|
viewportData,
|
||||||
imageSliceData,
|
imageSliceData,
|
||||||
@ -242,24 +172,26 @@ function CustomizableViewportOverlay({
|
|||||||
servicesManager,
|
servicesManager,
|
||||||
customization: item,
|
customization: item,
|
||||||
formatters: {
|
formatters: {
|
||||||
formatPN: formatPN,
|
formatPN,
|
||||||
formatDate: formatDICOMDate,
|
formatDate: formatDICOMDate,
|
||||||
formatTime: formatDICOMTime,
|
formatTime: formatDICOMTime,
|
||||||
formatNumberPrecision: formatNumberPrecision,
|
formatNumberPrecision,
|
||||||
},
|
},
|
||||||
instance,
|
instance: instances ? instances[item?.instanceIndex] : null,
|
||||||
// calculated
|
|
||||||
voi,
|
voi,
|
||||||
scale,
|
scale,
|
||||||
instanceNumber,
|
instanceNumber,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (item.customizationType === 'ohif.overlayItem.windowLevel') {
|
if (!item) {
|
||||||
return <VOIOverlayItem {...overlayItemProps} />;
|
return null;
|
||||||
} else if (item.customizationType === 'ohif.overlayItem.zoomLevel') {
|
}
|
||||||
return <ZoomOverlayItem {...overlayItemProps} />;
|
|
||||||
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
|
const { customizationType } = item;
|
||||||
return <InstanceNumberOverlayItem {...overlayItemProps} />;
|
const OverlayItemComponent = OverlayItemComponents[customizationType];
|
||||||
|
|
||||||
|
if (OverlayItemComponent) {
|
||||||
|
return <OverlayItemComponent {...overlayItemProps} />;
|
||||||
} else {
|
} else {
|
||||||
const renderItem = customizationService.transform(item);
|
const renderItem = customizationService.transform(item);
|
||||||
|
|
||||||
@ -275,110 +207,155 @@ function CustomizableViewportOverlay({
|
|||||||
viewportId,
|
viewportId,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
customizationService,
|
customizationService,
|
||||||
instance,
|
instances,
|
||||||
voi,
|
voi,
|
||||||
scale,
|
scale,
|
||||||
instanceNumber,
|
instanceNumber,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getTopLeftContent = useCallback(() => {
|
const getContent = useCallback(
|
||||||
const items = topLeftCustomization?.items || [
|
(customization, defaultItems, keyPrefix) => {
|
||||||
{
|
const items = customization?.items ?? defaultItems;
|
||||||
id: 'WindowLevel',
|
return (
|
||||||
customizationType: 'ohif.overlayItem.windowLevel',
|
<>
|
||||||
},
|
{items.map((item, index) => (
|
||||||
];
|
<div key={`${keyPrefix}_${index}`}>
|
||||||
return (
|
{item?.condition
|
||||||
<>
|
? item.condition({
|
||||||
{items.map((item, i) => (
|
instance: instances ? instances[item?.instanceIndex] : null,
|
||||||
<div key={`topLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
formatters: { formatDate: formatDICOMDate },
|
||||||
))}
|
})
|
||||||
</>
|
? _renderOverlayItem(item)
|
||||||
);
|
: null
|
||||||
}, [topLeftCustomization, _renderOverlayItem]);
|
: _renderOverlayItem(item)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[_renderOverlayItem]
|
||||||
|
);
|
||||||
|
|
||||||
const getTopRightContent = useCallback(() => {
|
const studyDateItem = {
|
||||||
const items = topRightCustomization?.items || [
|
id: 'StudyDate',
|
||||||
{
|
customizationType: 'ohif.overlayItem',
|
||||||
id: 'InstanceNmber',
|
label: '',
|
||||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
title: 'Study date',
|
||||||
},
|
condition: ({ instance }) => instance && instance.StudyDate,
|
||||||
];
|
contentF: ({ instance, formatters: { formatDate } }) => formatDate(instance.StudyDate),
|
||||||
return (
|
};
|
||||||
<>
|
|
||||||
{items.map((item, i) => (
|
|
||||||
<div key={`topRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}, [topRightCustomization, _renderOverlayItem]);
|
|
||||||
|
|
||||||
const getBottomLeftContent = useCallback(() => {
|
const seriesDescriptionItem = {
|
||||||
const items = bottomLeftCustomization?.items || [];
|
id: 'SeriesDescription',
|
||||||
return (
|
customizationType: 'ohif.overlayItem',
|
||||||
<>
|
label: '',
|
||||||
{items.map((item, i) => (
|
title: 'Series description',
|
||||||
<div key={`bottomLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
attribute: 'SeriesDescription',
|
||||||
))}
|
condition: ({ instance }) => {
|
||||||
</>
|
return instance && instance.SeriesDescription;
|
||||||
);
|
},
|
||||||
}, [bottomLeftCustomization, _renderOverlayItem]);
|
};
|
||||||
|
|
||||||
const getBottomRightContent = useCallback(() => {
|
const topLeftItems = instances
|
||||||
const items = bottomRightCustomization?.items || [];
|
? instances
|
||||||
return (
|
.map((instance, index) => {
|
||||||
<>
|
return [
|
||||||
{items.map((item, i) => (
|
{
|
||||||
<div key={`bottomRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
...studyDateItem,
|
||||||
))}
|
instanceIndex: index,
|
||||||
</>
|
},
|
||||||
);
|
{
|
||||||
}, [bottomRightCustomization, _renderOverlayItem]);
|
...seriesDescriptionItem,
|
||||||
|
instanceIndex: index,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewportOverlay
|
<ViewportOverlay
|
||||||
topLeft={getTopLeftContent()}
|
topLeft={
|
||||||
topRight={getTopRightContent()}
|
/**
|
||||||
bottomLeft={getBottomLeftContent()}
|
* Inline default overlay items for a more standard expansion
|
||||||
bottomRight={getBottomRightContent()}
|
*/
|
||||||
|
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) {
|
function _getViewportInstances(viewportData) {
|
||||||
let imageId = null;
|
const imageIds = [];
|
||||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||||
imageId = viewportData.data.imageIds[imageIndex];
|
imageIds.push(viewportData.data.imageIds[0]);
|
||||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||||
const volumes = viewportData.data;
|
const volumes = viewportData.data;
|
||||||
if (volumes && volumes.length == 1) {
|
volumes.forEach(volume => {
|
||||||
const volume = volumes[0];
|
if (!volume?.imageIds || volume.imageIds.length === 0) {
|
||||||
imageId = volume.imageIds[imageIndex];
|
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;
|
let instanceNumber;
|
||||||
|
|
||||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
switch (viewportData.viewportType) {
|
||||||
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
|
case Enums.ViewportType.STACK:
|
||||||
|
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
|
||||||
if (!instanceNumber && instanceNumber !== 0) {
|
break;
|
||||||
return null;
|
case Enums.ViewportType.ORTHOGRAPHIC:
|
||||||
}
|
instanceNumber = _getInstanceNumberFromVolume(
|
||||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
viewportData,
|
||||||
instanceNumber = _getInstanceNumberFromVolume(
|
viewportId,
|
||||||
viewportData,
|
cornerstoneViewportService,
|
||||||
imageIndex,
|
imageIndex
|
||||||
viewportId,
|
);
|
||||||
cornerstoneViewportService
|
break;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return instanceNumber;
|
|
||||||
}
|
return instanceNumber ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||||
const imageIds = viewportData.data.imageIds;
|
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
|
// Since volume viewports can be in any view direction, they can render
|
||||||
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
|
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
|
||||||
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
|
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
|
||||||
function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewportService) {
|
function _getInstanceNumberFromVolume(
|
||||||
const volumes = viewportData.volumes;
|
viewportData,
|
||||||
|
viewportId,
|
||||||
|
cornerstoneViewportService,
|
||||||
|
imageIndex
|
||||||
|
) {
|
||||||
|
const volumes = viewportData.data;
|
||||||
|
|
||||||
// Todo: support fusion of acquisition plane which has instanceNumber
|
if (!volumes) {
|
||||||
if (!volumes || volumes.length > 1) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const volume = volumes[0];
|
// Todo: support fusion of acquisition plane which has instanceNumber
|
||||||
|
const { volume } = volumes[0];
|
||||||
const { direction, imageIds } = volume;
|
const { direction, imageIds } = volume;
|
||||||
|
|
||||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
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 = {
|
CustomizableViewportOverlay.propTypes = {
|
||||||
viewportData: PropTypes.object,
|
viewportData: PropTypes.object,
|
||||||
imageIndex: PropTypes.number,
|
imageIndex: PropTypes.number,
|
||||||
|
|||||||
@ -23,7 +23,7 @@ function CornerstoneImageScrollbar({
|
|||||||
|
|
||||||
if (isCineEnabled) {
|
if (isCineEnabled) {
|
||||||
// on image scrollbar change, stop the CINE if it is playing
|
// on image scrollbar change, stop the CINE if it is playing
|
||||||
cineService.stopClip(element);
|
cineService.stopClip(element, { viewportId });
|
||||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -108,17 +108,15 @@ function ViewportOrientationMarkers({
|
|||||||
console.log('ViewportOrientationMarkers::No viewport');
|
console.log('ViewportOrientationMarkers::No viewport');
|
||||||
return null;
|
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) => (
|
return orientationMarkers.map((m, index) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
'overlay-text',
|
||||||
`${m}-mid orientation-marker`,
|
`${m}-mid orientation-marker`,
|
||||||
isLight ? 'text-[#726F7E]' : 'text-[#ccc]'
|
'text-aqua-pale',
|
||||||
|
'text-[13px]',
|
||||||
|
'leading-5'
|
||||||
)}
|
)}
|
||||||
key={`${m}-mid orientation-marker`}
|
key={`${m}-mid orientation-marker`}
|
||||||
>
|
>
|
||||||
@ -135,7 +133,7 @@ function ViewportOrientationMarkers({
|
|||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <div className="ViewportOrientationMarkers noselect">{markers}</div>;
|
return <div className="ViewportOrientationMarkers select-none">{markers}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewportOrientationMarkers.propTypes = {
|
ViewportOrientationMarkers.propTypes = {
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -3,6 +3,8 @@ import {
|
|||||||
StackViewport,
|
StackViewport,
|
||||||
VolumeViewport,
|
VolumeViewport,
|
||||||
utilities as csUtils,
|
utilities as csUtils,
|
||||||
|
Types as CoreTypes,
|
||||||
|
BaseVolumeViewport,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import {
|
import {
|
||||||
ToolGroupManager,
|
ToolGroupManager,
|
||||||
@ -11,13 +13,20 @@ import {
|
|||||||
ReferenceLinesTool,
|
ReferenceLinesTool,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
import { Types as OhifTypes } from '@ohif/core';
|
import { Types as OhifTypes } from '@ohif/core';
|
||||||
|
import { vec3, mat4 } from 'gl-matrix';
|
||||||
|
|
||||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||||
import callInputDialog from './utils/callInputDialog';
|
import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/callInputDialog';
|
||||||
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
|
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
|
||||||
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||||
import { CornerstoneServices } from './types';
|
import { CornerstoneServices } from './types';
|
||||||
|
import toggleVOISliceSync from './utils/toggleVOISliceSync';
|
||||||
|
|
||||||
|
const toggleSyncFunctions = {
|
||||||
|
imageSlice: toggleImageSliceSync,
|
||||||
|
voi: toggleVOISliceSync,
|
||||||
|
};
|
||||||
|
|
||||||
function commandsModule({
|
function commandsModule({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
@ -27,11 +36,14 @@ function commandsModule({
|
|||||||
viewportGridService,
|
viewportGridService,
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
cineService,
|
cineService,
|
||||||
toolbarService,
|
|
||||||
uiDialogService,
|
uiDialogService,
|
||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
measurementService,
|
measurementService,
|
||||||
|
customizationService,
|
||||||
|
colorbarService,
|
||||||
|
hangingProtocolService,
|
||||||
|
syncGroupService,
|
||||||
} = servicesManager.services as CornerstoneServices;
|
} = servicesManager.services as CornerstoneServices;
|
||||||
|
|
||||||
const { measurementServiceSource } = this;
|
const { measurementServiceSource } = this;
|
||||||
@ -39,6 +51,12 @@ function commandsModule({
|
|||||||
function _getActiveViewportEnabledElement() {
|
function _getActiveViewportEnabledElement() {
|
||||||
return getActiveViewportEnabledElement(viewportGridService);
|
return getActiveViewportEnabledElement(viewportGridService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _getActiveViewportToolGroupId() {
|
||||||
|
const viewport = _getActiveViewportEnabledElement();
|
||||||
|
return toolGroupService.getToolGroupForViewport(viewport.id);
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
/**
|
/**
|
||||||
* Generates the selector props for the context menu, specific to
|
* Generates the selector props for the context menu, specific to
|
||||||
@ -114,38 +132,29 @@ function commandsModule({
|
|||||||
? nearbyToolData
|
? nearbyToolData
|
||||||
: null;
|
: null;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Measurement tool commands:
|
|
||||||
|
|
||||||
/** Delete the given measurement */
|
/** Delete the given measurement */
|
||||||
deleteMeasurement: ({ uid }) => {
|
deleteMeasurement: ({ uid }) => {
|
||||||
if (uid) {
|
if (uid) {
|
||||||
measurementServiceSource.remove(uid);
|
measurementServiceSource.remove(uid);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the measurement labelling input dialog and update the label
|
* Show the measurement labelling input dialog and update the label
|
||||||
* on the measurement with a response if not cancelled.
|
* on the measurement with a response if not cancelled.
|
||||||
*/
|
*/
|
||||||
setMeasurementLabel: ({ uid }) => {
|
setMeasurementLabel: ({ uid }) => {
|
||||||
|
const labelConfig = customizationService.get('measurementLabels');
|
||||||
const measurement = measurementService.getMeasurement(uid);
|
const measurement = measurementService.getMeasurement(uid);
|
||||||
|
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(
|
||||||
callInputDialog(
|
(val: Map<any, any>) => {
|
||||||
uiDialogService,
|
measurementService.update(
|
||||||
measurement,
|
uid,
|
||||||
(label, actionId) => {
|
{
|
||||||
if (actionId === 'cancel') {
|
...val,
|
||||||
return;
|
},
|
||||||
}
|
true
|
||||||
|
);
|
||||||
const updatedMeasurement = Object.assign({}, measurement, {
|
}
|
||||||
label,
|
|
||||||
});
|
|
||||||
|
|
||||||
measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -221,47 +230,22 @@ function commandsModule({
|
|||||||
|
|
||||||
viewportGridService.setActiveViewportId(viewportId);
|
viewportGridService.setActiveViewportId(viewportId);
|
||||||
},
|
},
|
||||||
arrowTextCallback: ({ callback, data }) => {
|
arrowTextCallback: ({ callback, data, uid }) => {
|
||||||
callInputDialog(uiDialogService, data, callback);
|
const labelConfig = customizationService.get('measurementLabels');
|
||||||
},
|
callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig);
|
||||||
cleanUpCrosshairs: () => {
|
|
||||||
// if the crosshairs tool is active, deactivate it and set window level active
|
|
||||||
// since we are going back to main non-mpr HP
|
|
||||||
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
|
|
||||||
|
|
||||||
if (activeViewportToolGroup._toolInstances?.Crosshairs?.mode === Enums.ToolModes.Active) {
|
|
||||||
actions.toolbarServiceRecordInteraction({
|
|
||||||
interactionType: 'tool',
|
|
||||||
commands: [
|
|
||||||
{
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'WindowLevel',
|
|
||||||
},
|
|
||||||
context: 'CORNERSTONE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
toggleCine: () => {
|
toggleCine: () => {
|
||||||
const { viewports } = viewportGridService.getState();
|
const { viewports } = viewportGridService.getState();
|
||||||
const { isCineEnabled } = cineService.getState();
|
const { isCineEnabled } = cineService.getState();
|
||||||
cineService.setIsCineEnabled(!isCineEnabled);
|
cineService.setIsCineEnabled(!isCineEnabled);
|
||||||
toolbarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
|
|
||||||
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
|
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
|
||||||
},
|
},
|
||||||
setWindowLevel({ window, level, toolGroupId }) {
|
|
||||||
|
setViewportWindowLevel({ viewportId, window, level }) {
|
||||||
// convert to numbers
|
// convert to numbers
|
||||||
const windowWidthNum = Number(window);
|
const windowWidthNum = Number(window);
|
||||||
const windowCenterNum = Number(level);
|
const windowCenterNum = Number(level);
|
||||||
|
|
||||||
const { viewportId } = _getActiveViewportEnabledElement();
|
|
||||||
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
|
|
||||||
|
|
||||||
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get actor from the viewport
|
// get actor from the viewport
|
||||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
@ -277,34 +261,102 @@ function commandsModule({
|
|||||||
viewport.render();
|
viewport.render();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Just call the toolbar service record interaction - allows
|
toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => {
|
||||||
// executing a toolbar command as a full toolbar command with side affects
|
const hasColorbar = colorbarService.hasColorbar(viewportId);
|
||||||
// coming from the ToolbarService itself.
|
if (hasColorbar) {
|
||||||
toolbarServiceRecordInteraction: props => {
|
colorbarService.removeColorbar(viewportId);
|
||||||
toolbarService.recordInteraction(props);
|
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) {
|
setWindowLevel(props) {
|
||||||
uiNotificationService.show({
|
const { toolGroupId } = props;
|
||||||
title: 'Crosshairs',
|
const { viewportId } = _getActiveViewportEnabledElement();
|
||||||
message:
|
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
|
||||||
'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,
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
const { viewports } = viewportGridService.getState();
|
||||||
|
|
||||||
if (!viewports.size) {
|
if (!viewports.size) {
|
||||||
@ -317,34 +369,17 @@ function commandsModule({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!toolGroup.getToolInstance(toolName)) {
|
if (!toolGroup.hasTool(toolName)) {
|
||||||
uiNotificationService.show({
|
return;
|
||||||
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.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
|
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
|
||||||
|
|
||||||
if (activeToolName) {
|
if (activeToolName) {
|
||||||
// Todo: this is a hack to prevent the crosshairs to stick around
|
const activeToolOptions = toolGroup.getToolConfiguration(activeToolName);
|
||||||
// after another tool is selected. We should find a better way to do this
|
activeToolOptions?.disableOnPassive
|
||||||
if (activeToolName === 'Crosshairs') {
|
? toolGroup.setToolDisabled(activeToolName)
|
||||||
toolGroup.setToolDisabled(activeToolName);
|
: toolGroup.setToolPassive(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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the new toolName to be active
|
// Set the new toolName to be active
|
||||||
@ -380,6 +415,7 @@ function commandsModule({
|
|||||||
onClose: uiModalService.hide,
|
onClose: uiModalService.hide,
|
||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
},
|
},
|
||||||
|
containerDimensions: 'w-[70%] max-w-[900px]',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -391,8 +427,16 @@ function commandsModule({
|
|||||||
|
|
||||||
const { viewport } = enabledElement;
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
if (viewport instanceof StackViewport) {
|
if (viewport instanceof BaseVolumeViewport) {
|
||||||
const { rotation: currentRotation } = viewport.getProperties();
|
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;
|
const newRotation = (currentRotation + rotation) % 360;
|
||||||
viewport.setProperties({ rotation: newRotation });
|
viewport.setProperties({ rotation: newRotation });
|
||||||
viewport.render();
|
viewport.render();
|
||||||
@ -407,11 +451,9 @@ function commandsModule({
|
|||||||
|
|
||||||
const { viewport } = enabledElement;
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
if (viewport instanceof StackViewport) {
|
const { flipHorizontal } = viewport.getCamera();
|
||||||
const { flipHorizontal } = viewport.getCamera();
|
viewport.setCamera({ flipHorizontal: !flipHorizontal });
|
||||||
viewport.setCamera({ flipHorizontal: !flipHorizontal });
|
viewport.render();
|
||||||
viewport.render();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
flipViewportVertical: () => {
|
flipViewportVertical: () => {
|
||||||
const enabledElement = _getActiveViewportEnabledElement();
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
@ -422,11 +464,9 @@ function commandsModule({
|
|||||||
|
|
||||||
const { viewport } = enabledElement;
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
if (viewport instanceof StackViewport) {
|
const { flipVertical } = viewport.getCamera();
|
||||||
const { flipVertical } = viewport.getCamera();
|
viewport.setCamera({ flipVertical: !flipVertical });
|
||||||
viewport.setCamera({ flipVertical: !flipVertical });
|
viewport.render();
|
||||||
viewport.render();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
invertViewport: ({ element }) => {
|
invertViewport: ({ element }) => {
|
||||||
let enabledElement;
|
let enabledElement;
|
||||||
@ -456,13 +496,8 @@ function commandsModule({
|
|||||||
|
|
||||||
const { viewport } = enabledElement;
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
if (viewport instanceof StackViewport) {
|
viewport.resetProperties?.();
|
||||||
viewport.resetProperties();
|
viewport.resetCamera();
|
||||||
viewport.resetCamera();
|
|
||||||
} else {
|
|
||||||
viewport.resetProperties();
|
|
||||||
viewport.resetCamera();
|
|
||||||
}
|
|
||||||
|
|
||||||
viewport.render();
|
viewport.render();
|
||||||
},
|
},
|
||||||
@ -534,18 +569,51 @@ function commandsModule({
|
|||||||
|
|
||||||
cstUtils.scroll(viewport, options);
|
cstUtils.scroll(viewport, options);
|
||||||
},
|
},
|
||||||
setViewportColormap: ({ viewportId, displaySetInstanceUID, colormap, immediate = false }) => {
|
setViewportColormap: ({
|
||||||
|
viewportId,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
colormap,
|
||||||
|
opacity = 1,
|
||||||
|
immediate = false,
|
||||||
|
}) => {
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
const actorEntries = viewport.getActors();
|
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 => {
|
if (displaySetsInfo) {
|
||||||
return actorEntry.uid.includes(displaySetInstanceUID);
|
// 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) {
|
if (immediate) {
|
||||||
viewport.render();
|
viewport.render();
|
||||||
@ -559,32 +627,207 @@ function commandsModule({
|
|||||||
(currentIndex + direction + viewportIds.length) % viewportIds.length;
|
(currentIndex + direction + viewportIds.length) % viewportIds.length;
|
||||||
viewportGridService.setActiveViewportId(viewportIds[nextViewportIndex] as string);
|
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 }) => {
|
if (synchronizer) {
|
||||||
toggleStackImageSync({
|
synchronizer.isDisabled() ? synchronizer.setEnabled(true) : synchronizer.setEnabled(false);
|
||||||
servicesManager,
|
return;
|
||||||
toggledState,
|
}
|
||||||
});
|
|
||||||
|
const fn = toggleSyncFunctions[type];
|
||||||
|
|
||||||
|
if (fn) {
|
||||||
|
fn({
|
||||||
|
servicesManager,
|
||||||
|
viewports,
|
||||||
|
syncId,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setSourceViewportForReferenceLinesTool: ({ toggledState, viewportId }) => {
|
setSourceViewportForReferenceLinesTool: ({ viewportId }) => {
|
||||||
if (!viewportId) {
|
if (!viewportId) {
|
||||||
const { activeViewportId } = viewportGridService.getState();
|
const { activeViewportId } = viewportGridService.getState();
|
||||||
viewportId = activeViewportId;
|
viewportId = activeViewportId ?? 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||||
|
|
||||||
toolGroup.setToolConfiguration(
|
toolGroup?.setToolConfiguration(
|
||||||
ReferenceLinesTool.toolName,
|
ReferenceLinesTool.toolName,
|
||||||
{
|
{
|
||||||
sourceViewportId: viewportId,
|
sourceViewportId: viewportId,
|
||||||
},
|
},
|
||||||
true // overwrite
|
true // overwrite
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||||
|
renderingEngine.render();
|
||||||
},
|
},
|
||||||
storePresentation: ({ viewportId }) => {
|
storePresentation: ({ viewportId }) => {
|
||||||
cornerstoneViewportService.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 = {
|
const definitions = {
|
||||||
@ -592,7 +835,6 @@ function commandsModule({
|
|||||||
// context menu
|
// context menu
|
||||||
showCornerstoneContextMenu: {
|
showCornerstoneContextMenu: {
|
||||||
commandFn: actions.showCornerstoneContextMenu,
|
commandFn: actions.showCornerstoneContextMenu,
|
||||||
storeContexts: [],
|
|
||||||
options: {
|
options: {
|
||||||
menuCustomizationId: 'measurementsContextMenu',
|
menuCustomizationId: 'measurementsContextMenu',
|
||||||
commands: [
|
commands: [
|
||||||
@ -611,7 +853,9 @@ function commandsModule({
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
toggleViewportColorbar: {
|
||||||
|
commandFn: actions.toggleViewportColorbar,
|
||||||
|
},
|
||||||
deleteMeasurement: {
|
deleteMeasurement: {
|
||||||
commandFn: actions.deleteMeasurement,
|
commandFn: actions.deleteMeasurement,
|
||||||
},
|
},
|
||||||
@ -621,16 +865,21 @@ function commandsModule({
|
|||||||
updateMeasurement: {
|
updateMeasurement: {
|
||||||
commandFn: actions.updateMeasurement,
|
commandFn: actions.updateMeasurement,
|
||||||
},
|
},
|
||||||
|
setViewportWindowLevel: {
|
||||||
|
commandFn: actions.setViewportWindowLevel,
|
||||||
|
},
|
||||||
setWindowLevel: {
|
setWindowLevel: {
|
||||||
commandFn: actions.setWindowLevel,
|
commandFn: actions.setWindowLevel,
|
||||||
},
|
},
|
||||||
toolbarServiceRecordInteraction: {
|
|
||||||
commandFn: actions.toolbarServiceRecordInteraction,
|
|
||||||
},
|
|
||||||
setToolActive: {
|
setToolActive: {
|
||||||
commandFn: actions.setToolActive,
|
commandFn: actions.setToolActive,
|
||||||
},
|
},
|
||||||
|
setToolActiveToolbar: {
|
||||||
|
commandFn: actions.setToolActiveToolbar,
|
||||||
|
},
|
||||||
|
setToolEnabled: {
|
||||||
|
commandFn: actions.setToolEnabled,
|
||||||
|
},
|
||||||
rotateViewportCW: {
|
rotateViewportCW: {
|
||||||
commandFn: actions.rotateViewport,
|
commandFn: actions.rotateViewport,
|
||||||
options: { rotation: 90 },
|
options: { rotation: 90 },
|
||||||
@ -704,20 +953,41 @@ function commandsModule({
|
|||||||
setViewportColormap: {
|
setViewportColormap: {
|
||||||
commandFn: actions.setViewportColormap,
|
commandFn: actions.setViewportColormap,
|
||||||
},
|
},
|
||||||
toggleStackImageSync: {
|
|
||||||
commandFn: actions.toggleStackImageSync,
|
|
||||||
},
|
|
||||||
setSourceViewportForReferenceLinesTool: {
|
setSourceViewportForReferenceLinesTool: {
|
||||||
commandFn: actions.setSourceViewportForReferenceLinesTool,
|
commandFn: actions.setSourceViewportForReferenceLinesTool,
|
||||||
},
|
},
|
||||||
storePresentation: {
|
storePresentation: {
|
||||||
commandFn: actions.storePresentation,
|
commandFn: actions.storePresentation,
|
||||||
},
|
},
|
||||||
setToolbarToggled: {
|
attachProtocolViewportDataListener: {
|
||||||
commandFn: actions.setToolbarToggled,
|
commandFn: actions.attachProtocolViewportDataListener,
|
||||||
},
|
},
|
||||||
cleanUpCrosshairs: {
|
setViewportPreset: {
|
||||||
commandFn: actions.cleanUpCrosshairs,
|
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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ActiveViewportWindowLevel';
|
||||||
@ -1,96 +1,237 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||||
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
|
import { CinePlayer, useCine } from '@ohif/ui';
|
||||||
import { Enums, eventTarget } from '@cornerstonejs/core';
|
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 }) {
|
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||||
const { toolbarService, customizationService } = servicesManager.services;
|
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
|
||||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||||
const [{ activeViewportId }] = useViewportGrid();
|
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
|
||||||
|
const [dynamicInfo, setDynamicInfo] = useState(null);
|
||||||
const { component: CinePlayerComponent = CinePlayer } =
|
const [appConfig] = useAppConfig();
|
||||||
customizationService.get('cinePlayer') ?? {};
|
const isMountedRef = useRef(null);
|
||||||
|
|
||||||
const handleCineClose = () => {
|
|
||||||
toolbarService.recordInteraction({
|
|
||||||
groupId: 'MoreTools',
|
|
||||||
interactionType: 'toggle',
|
|
||||||
commands: [
|
|
||||||
{
|
|
||||||
commandName: 'toggleCine',
|
|
||||||
commandOptions: {},
|
|
||||||
toolName: 'cine',
|
|
||||||
context: 'CORNERSTONE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cineHandler = () => {
|
const cineHandler = () => {
|
||||||
if (!cines || !cines[viewportId] || !enabledVPElement) {
|
if (!cines?.[viewportId] || !enabledVPElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cine = cines[viewportId];
|
const { isPlaying = false, frameRate = 24 } = cines[viewportId];
|
||||||
const isPlaying = cine.isPlaying || false;
|
|
||||||
const frameRate = cine.frameRate || 24;
|
|
||||||
|
|
||||||
const validFrameRate = Math.max(frameRate, 1);
|
const validFrameRate = Math.max(frameRate, 1);
|
||||||
|
|
||||||
if (isPlaying) {
|
return isPlaying
|
||||||
cineService.playClip(enabledVPElement, {
|
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate, viewportId })
|
||||||
framesPerSecond: validFrameRate,
|
: cineService.stopClip(enabledVPElement);
|
||||||
});
|
|
||||||
} else {
|
|
||||||
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(() => {
|
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 () => {
|
return () => {
|
||||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
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(() => {
|
useEffect(() => {
|
||||||
if (!cines || !cines[viewportId] || !enabledVPElement) {
|
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cineHandler();
|
cineHandler();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (enabledVPElement && cines?.[viewportId]?.isPlaying) {
|
cineService.stopClip(enabledVPElement, { viewportId });
|
||||||
cineService.stopClip(enabledVPElement);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
|
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
|
||||||
|
|
||||||
|
if (!isCineEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const cine = cines[viewportId];
|
const cine = cines[viewportId];
|
||||||
const isPlaying = (cine && cine.isPlaying) || false;
|
const isPlaying = cine?.isPlaying || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isCineEnabled && (
|
<RenderCinePlayer
|
||||||
<CinePlayerComponent
|
viewportId={viewportId}
|
||||||
className="absolute left-1/2 bottom-3 -translate-x-1/2"
|
cineService={cineService}
|
||||||
isPlaying={isPlaying}
|
newStackFrameRate={newStackFrameRate}
|
||||||
onClose={handleCineClose}
|
isPlaying={isPlaying}
|
||||||
onPlayPauseChange={isPlaying =>
|
dynamicInfo={dynamicInfo}
|
||||||
cineService.setCine({
|
customizationService={customizationService}
|
||||||
id: activeViewportId,
|
/>
|
||||||
isPlaying,
|
);
|
||||||
})
|
}
|
||||||
}
|
|
||||||
onFrameRateChange={frameRate =>
|
function RenderCinePlayer({
|
||||||
cineService.setCine({
|
viewportId,
|
||||||
id: activeViewportId,
|
cineService,
|
||||||
frameRate,
|
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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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 };
|
||||||
@ -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);
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ViewportWindowLevel';
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
@ -1,6 +1,12 @@
|
|||||||
import { Enums } from '@cornerstonejs/tools';
|
import { Enums } from '@cornerstonejs/tools';
|
||||||
import { toolNames } from './initCornerstoneTools';
|
import { toolNames } from './initCornerstoneTools';
|
||||||
import DicomUpload from './components/DicomUpload/DicomUpload';
|
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 = {
|
const tools = {
|
||||||
active: [
|
active: [
|
||||||
@ -37,6 +43,157 @@ function getCustomizationModule() {
|
|||||||
id: 'cornerstone.overlayViewportTools',
|
id: 'cornerstone.overlayViewportTools',
|
||||||
tools,
|
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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,295 +1,10 @@
|
|||||||
import { Types } from '@ohif/core';
|
import { fourUp } from './hps/fourUp';
|
||||||
|
import { main3D } from './hps/main3D';
|
||||||
const mpr: Types.HangingProtocol.Protocol = {
|
import { mpr } from './hps/mpr';
|
||||||
id: 'mpr',
|
import { mprAnd3DVolumeViewport } from './hps/mprAnd3DVolumeViewport';
|
||||||
name: 'Multi-Planar Reconstruction',
|
import { only3D } from './hps/only3D';
|
||||||
locked: true,
|
import { primary3D } from './hps/primary3D';
|
||||||
createdDate: '2021-02-23',
|
import { primaryAxial } from './hps/primaryAxial';
|
||||||
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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
function getHangingProtocolModule() {
|
function getHangingProtocolModule() {
|
||||||
return [
|
return [
|
||||||
@ -301,6 +16,26 @@ function getHangingProtocolModule() {
|
|||||||
name: mprAnd3DVolumeViewport.id,
|
name: mprAnd3DVolumeViewport.id,
|
||||||
protocol: mprAnd3DVolumeViewport,
|
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,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
303
extensions/cornerstone/src/getToolbarModule.tsx
Normal file
303
extensions/cornerstone/src/getToolbarModule.tsx
Normal 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
144
extensions/cornerstone/src/hps/fourUp.ts
Normal file
144
extensions/cornerstone/src/hps/fourUp.ts
Normal 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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
170
extensions/cornerstone/src/hps/main3D.ts
Normal file
170
extensions/cornerstone/src/hps/main3D.ts
Normal 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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
149
extensions/cornerstone/src/hps/mpr.ts
Normal file
149
extensions/cornerstone/src/hps/mpr.ts
Normal 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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
151
extensions/cornerstone/src/hps/mprAnd3DVolumeViewport.ts
Normal file
151
extensions/cornerstone/src/hps/mprAnd3DVolumeViewport.ts
Normal 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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
66
extensions/cornerstone/src/hps/only3D.ts
Normal file
66
extensions/cornerstone/src/hps/only3D.ts
Normal 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
Loading…
Reference in New Issue
Block a user