OHIF with Cornerstone3D 2.0 (#4347)
This commit is contained in:
parent
8981a61b8c
commit
7ce7127d7c
@ -180,10 +180,6 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Authenticate with NPM registry
|
name: Authenticate with NPM registry
|
||||||
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||||
- run:
|
|
||||||
name: Increase the event emitter limit
|
|
||||||
command: |
|
|
||||||
node ./increaseEventEmitterLimit.mjs
|
|
||||||
- run:
|
- run:
|
||||||
name: build half of the packages (to avoid out of memory in circleci)
|
name: build half of the packages (to avoid out of memory in circleci)
|
||||||
command: |
|
command: |
|
||||||
@ -230,10 +226,6 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Authenticate with NPM registry
|
name: Authenticate with NPM registry
|
||||||
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||||
- run:
|
|
||||||
name: Increase the event emitter limit
|
|
||||||
command: |
|
|
||||||
node ./increaseEventEmitterLimit.mjs
|
|
||||||
- run:
|
- run:
|
||||||
name: build half of the packages (to avoid out of memory in circleci)
|
name: build half of the packages (to avoid out of memory in circleci)
|
||||||
command: |
|
command: |
|
||||||
|
|||||||
@ -144,11 +144,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
enforce: 'pre',
|
|
||||||
use: 'source-map-loader',
|
|
||||||
},
|
|
||||||
transpileJavaScriptRule(mode),
|
transpileJavaScriptRule(mode),
|
||||||
loadWebWorkersRule,
|
loadWebWorkersRule,
|
||||||
// loadShadersRule,
|
// loadShadersRule,
|
||||||
@ -188,8 +183,8 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
|||||||
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
|
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
|
||||||
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
|
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
|
||||||
'@state': path.resolve(__dirname, '../platform/app/src/state'),
|
'@state': path.resolve(__dirname, '../platform/app/src/state'),
|
||||||
'@cornerstonejs/dicom-image-loader':
|
'dicom-microscopy-viewer':
|
||||||
'@cornerstonejs/dicom-image-loader/dist/dynamic-import/cornerstoneDICOMImageLoader.min.js',
|
'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js',
|
||||||
},
|
},
|
||||||
// Which directories to search when resolving modules
|
// Which directories to search when resolving modules
|
||||||
modules: [
|
modules: [
|
||||||
@ -217,7 +212,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
|||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
Buffer: ['buffer', 'Buffer'],
|
Buffer: ['buffer', 'Buffer'],
|
||||||
}),
|
}),
|
||||||
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin()]),
|
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]),
|
||||||
// Uncomment to generate bundle analyzer
|
// Uncomment to generate bundle analyzer
|
||||||
// new BundleAnalyzerPlugin(),
|
// new BundleAnalyzerPlugin(),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -288,3 +288,10 @@ Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Dat
|
|||||||
Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33
|
Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33
|
||||||
|
|
||||||
https://www.cancerimagingarchive.net/collection/ea1141/
|
https://www.cancerimagingarchive.net/collection/ea1141/
|
||||||
|
|
||||||
|
### 1.2.276.0.7230010.3.1.2.2155604110.4180.1021041295.21
|
||||||
|
|
||||||
|
From OFFIS DICOM-Team
|
||||||
|
|
||||||
|
https://www.offis.de/
|
||||||
|
OFFIS DICOM-Team
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@kitware/vtk.js": "30.4.1",
|
"@kitware/vtk.js": "32.1.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
"moment": "^2.9.4"
|
"moment": "^2.9.4"
|
||||||
@ -77,7 +77,7 @@
|
|||||||
"stylus": "^0.59.0",
|
"stylus": "^0.59.0",
|
||||||
"stylus-loader": "^7.1.3",
|
"stylus-loader": "^7.1.3",
|
||||||
"terser-webpack-plugin": "^5.1.4",
|
"terser-webpack-plugin": "^5.1.4",
|
||||||
"typescript": "4.6.4",
|
"typescript": "5.5.4",
|
||||||
"unused-webpack-plugin": "2.4.0",
|
"unused-webpack-plugin": "2.4.0",
|
||||||
"webpack": "5.94.0",
|
"webpack": "5.94.0",
|
||||||
"webpack-bundle-analyzer": "^4.8.0",
|
"webpack-bundle-analyzer": "^4.8.0",
|
||||||
|
|||||||
@ -45,7 +45,7 @@
|
|||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"trim": "^1.0.0",
|
"trim": "^1.0.0",
|
||||||
"package-json": "^8.1.0",
|
"package-json": "^8.1.0",
|
||||||
"typescript": "4.6.4",
|
"typescript": "5.5.4",
|
||||||
"sharp": "^0.32.6"
|
"sharp": "^0.32.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,9 +46,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^1.86.0",
|
"@cornerstonejs/adapters": "^2.1.13",
|
||||||
"@cornerstonejs/core": "^1.86.0",
|
"@cornerstonejs/core": "^2.1.13",
|
||||||
"@kitware/vtk.js": "30.4.1",
|
"@kitware/vtk.js": "32.1.0",
|
||||||
"react-color": "^2.19.3"
|
"react-color": "^2.19.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,7 +97,7 @@ function _getDisplaySetsFromSeries(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Does not get the referenced volumeId during parametric displaySet creation because the
|
// Does not get the referenced volumeId during parametric displaySet creation because the
|
||||||
// referenced displaySet is still not avaialble (getDisplaySetByUID returns `undefined`).
|
// referenced displaySet is still not available (getDisplaySetByUID returns `undefined`).
|
||||||
displaySet.getReferencedVolumeId = () => {
|
displaySet.getReferencedVolumeId = () => {
|
||||||
if (displaySet.referencedVolumeId) {
|
if (displaySet.referencedVolumeId) {
|
||||||
return displaySet.referencedVolumeId;
|
return displaySet.referencedVolumeId;
|
||||||
@ -119,6 +119,22 @@ function _getDisplaySetsFromSeries(
|
|||||||
return [displaySet];
|
return [displaySet];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRangeFromPixelData = (pixelData: Float32Array) => {
|
||||||
|
let lowest = pixelData[0];
|
||||||
|
let highest = pixelData[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < pixelData.length; i++) {
|
||||||
|
if (pixelData[i] < lowest) {
|
||||||
|
lowest = pixelData[i];
|
||||||
|
}
|
||||||
|
if (pixelData[i] > highest) {
|
||||||
|
highest = pixelData[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [lowest, highest];
|
||||||
|
};
|
||||||
|
|
||||||
async function _load(
|
async function _load(
|
||||||
displaySet,
|
displaySet,
|
||||||
servicesManager: AppTypes.ServicesManager,
|
servicesManager: AppTypes.ServicesManager,
|
||||||
@ -132,11 +148,11 @@ async function _load(
|
|||||||
return volumeLoadObject.promise;
|
return volumeLoadObject.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
displaySet.loadStatus.loading = true;
|
displaySet.loading = true;
|
||||||
|
displaySet.isLoaded = false;
|
||||||
|
|
||||||
// We don't want to fire multiple loads, so we'll wait for the first to finish
|
// We don't want to fire multiple loads, so we'll wait for the first to finish
|
||||||
// and also return the same promise to any other callers.
|
// and also return the same promise to any other callers.
|
||||||
// loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
|
||||||
const promise = _loadParametricMap({
|
const promise = _loadParametricMap({
|
||||||
extensionManager,
|
extensionManager,
|
||||||
displaySet,
|
displaySet,
|
||||||
@ -149,18 +165,26 @@ async function _load(
|
|||||||
|
|
||||||
promise
|
promise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
displaySet.loadStatus.loading = false;
|
displaySet.loading = false;
|
||||||
displaySet.loadStatus.loaded = true;
|
displaySet.isLoaded = true;
|
||||||
|
// Broadcast that loading is complete
|
||||||
|
servicesManager.services.segmentationService._broadcastEvent(
|
||||||
|
servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||||
|
{
|
||||||
|
pmapDisplaySet: displaySet,
|
||||||
|
}
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
displaySet.loadStatus.loading = false;
|
displaySet.loading = false;
|
||||||
|
displaySet.isLoaded = false;
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _loadParametricMap({ extensionManager, displaySet, headers }: withAppTypes) {
|
async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
|
||||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
|
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
|
||||||
const referencedVolumeId = displaySet.getReferencedVolumeId();
|
const referencedVolumeId = displaySet.getReferencedVolumeId();
|
||||||
const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
|
const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
|
||||||
@ -189,9 +213,12 @@ async function _loadParametricMap({ extensionManager, displaySet, headers }: wit
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
derivedVolume.getScalarData().set(pixelData);
|
const newPixelData = new TypedArrayConstructor(pixelData.length);
|
||||||
|
for (let i = 0; i < pixelData.length; i++) {
|
||||||
const range = derivedVolume.imageData.getPointData().getScalars().getRange();
|
newPixelData[i] = pixelData[i] * 100;
|
||||||
|
}
|
||||||
|
derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData);
|
||||||
|
const range = getRangeFromPixelData(newPixelData);
|
||||||
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
|
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
|
||||||
|
|
||||||
derivedVolume.metadata.voiLut = [windowLevel];
|
derivedVolume.metadata.voiLut = [windowLevel];
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
function createPMAPToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
|
|
||||||
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
|
||||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createPMAPToolGroupAndAddTools;
|
|
||||||
@ -1,16 +1,19 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useViewportGrid } from '@ohif/ui';
|
import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
|
||||||
import createPMAPToolGroupAndAddTools from '../utils/initPMAPToolGroup';
|
|
||||||
|
|
||||||
const PMAP_TOOLGROUP_BASE_NAME = 'PMAPToolGroup';
|
|
||||||
|
|
||||||
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||||
const { displaySets, viewportOptions, displaySetOptions, servicesManager, extensionManager } =
|
const {
|
||||||
props;
|
displaySets,
|
||||||
|
children,
|
||||||
|
viewportOptions,
|
||||||
|
displaySetOptions,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
|
} = props;
|
||||||
const viewportId = viewportOptions.viewportId;
|
const viewportId = viewportOptions.viewportId;
|
||||||
const { displaySetService, toolGroupService, customizationService } = servicesManager.services;
|
const { displaySetService, segmentationService, uiNotificationService } =
|
||||||
const toolGroupId = `${PMAP_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
servicesManager.services;
|
||||||
|
|
||||||
// PMAP viewport will always have a single display set
|
// PMAP viewport will always have a single display set
|
||||||
if (displaySets.length !== 1) {
|
if (displaySets.length !== 1) {
|
||||||
@ -32,6 +35,24 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
|||||||
metadata: referencedDisplaySetMetadata,
|
metadata: referencedDisplaySetMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [pmapIsLoading, setPmapIsLoading] = useState(!pmapDisplaySet.isLoaded);
|
||||||
|
|
||||||
|
// Add effect to listen for loading complete
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||||
|
evt => {
|
||||||
|
if (evt.pmapDisplaySet?.displaySetInstanceUID === pmapDisplaySet.displaySetInstanceUID) {
|
||||||
|
setPmapIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [pmapDisplaySet]);
|
||||||
|
|
||||||
const getCornerstoneViewport = useCallback(() => {
|
const getCornerstoneViewport = useCallback(() => {
|
||||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||||
const { component: Component } = extensionManager.getModuleEntry(
|
const { component: Component } = extensionManager.getModuleEntry(
|
||||||
@ -46,12 +67,25 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
|||||||
|
|
||||||
Object.assign(pmapDisplaySetOptions.options, {
|
Object.assign(pmapDisplaySetOptions.options, {
|
||||||
colormap: {
|
colormap: {
|
||||||
name: 'rainbow',
|
name: 'rainbow_2',
|
||||||
opacity: [
|
opacity: [
|
||||||
{ value: 0, opacity: 0.5 },
|
{ value: 0, opacity: 0 },
|
||||||
{ value: 1, opacity: 1 },
|
{ value: 0.25, opacity: 0.25 },
|
||||||
|
{ value: 0.5, opacity: 0.5 },
|
||||||
|
{ value: 0.75, opacity: 0.75 },
|
||||||
|
{ value: 0.9, opacity: 0.99 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
voi: {
|
||||||
|
windowCenter: 50,
|
||||||
|
windowWidth: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
uiNotificationService.show({
|
||||||
|
title: 'Parametric Map',
|
||||||
|
type: 'warning',
|
||||||
|
message: 'The values are multiplied by 100 in the viewport for better visibility',
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,7 +95,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
|||||||
displaySets={[referencedDisplaySet, pmapDisplaySet]}
|
displaySets={[referencedDisplaySet, pmapDisplaySet]}
|
||||||
viewportOptions={{
|
viewportOptions={{
|
||||||
viewportType: 'volume',
|
viewportType: 'volume',
|
||||||
toolGroupId: toolGroupId,
|
|
||||||
orientation: viewportOptions.orientation,
|
orientation: viewportOptions.orientation,
|
||||||
viewportId: viewportOptions.viewportId,
|
viewportId: viewportOptions.viewportId,
|
||||||
}}
|
}}
|
||||||
@ -73,7 +106,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
|||||||
displaySetOptions,
|
displaySetOptions,
|
||||||
props,
|
props,
|
||||||
pmapDisplaySet,
|
pmapDisplaySet,
|
||||||
toolGroupId,
|
|
||||||
viewportOptions.orientation,
|
viewportOptions.orientation,
|
||||||
viewportOptions.viewportId,
|
viewportOptions.viewportId,
|
||||||
]);
|
]);
|
||||||
@ -98,23 +130,33 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
|||||||
};
|
};
|
||||||
}, [activeViewportId, displaySetService, viewportGridService, viewports]);
|
}, [activeViewportId, displaySetService, viewportGridService, viewports]);
|
||||||
|
|
||||||
useEffect(() => {
|
let childrenWithProps = null;
|
||||||
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
|
||||||
|
|
||||||
if (toolGroup) {
|
if (children && children.length) {
|
||||||
return;
|
childrenWithProps = children.map((child, index) => {
|
||||||
}
|
return (
|
||||||
|
child &&
|
||||||
// This creates a custom tool group which has the lifetime of this view only
|
React.cloneElement(child, {
|
||||||
toolGroup = createPMAPToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
|
viewportId,
|
||||||
|
key: index,
|
||||||
return () => toolGroupService.destroyToolGroup(toolGroupId);
|
})
|
||||||
}, [customizationService, toolGroupId, toolGroupService]);
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||||
|
{pmapIsLoading && (
|
||||||
|
<LoadingIndicatorTotalPercent
|
||||||
|
className="h-full w-full"
|
||||||
|
totalNumbers={null}
|
||||||
|
percentComplete={null}
|
||||||
|
loadingText="Loading Parametric Map..."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{getCornerstoneViewport()}
|
{getCornerstoneViewport()}
|
||||||
|
{childrenWithProps}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
55
extensions/cornerstone-dicom-rt/src/getCommandsModule.ts
Normal file
55
extensions/cornerstone-dicom-rt/src/getCommandsModule.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||||
|
|
||||||
|
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||||
|
const services = servicesManager.services;
|
||||||
|
const { displaySetService, viewportGridService } = services;
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
hydrateRTSDisplaySet: ({ displaySet, viewportId }) => {
|
||||||
|
if (displaySet.Modality !== 'RTSTRUCT') {
|
||||||
|
throw new Error('Display set is not an RTSTRUCT');
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||||
|
displaySet.referencedDisplaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
// update the previously stored segmentationPresentation with the new viewportId
|
||||||
|
// presentation so that when we put the referencedDisplaySet back in the viewport
|
||||||
|
// it will have the correct segmentation representation hydrated
|
||||||
|
commandsManager.runCommand('updateStoredSegmentationPresentation', {
|
||||||
|
displaySet: displaySet,
|
||||||
|
type: SegmentationRepresentations.Contour,
|
||||||
|
});
|
||||||
|
|
||||||
|
// update the previously stored positionPresentation with the new viewportId
|
||||||
|
// presentation so that when we put the referencedDisplaySet back in the viewport
|
||||||
|
// it will be in the correct position zoom and pan
|
||||||
|
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||||
|
viewportId,
|
||||||
|
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
|
||||||
|
});
|
||||||
|
|
||||||
|
viewportGridService.setDisplaySetsForViewport({
|
||||||
|
viewportId,
|
||||||
|
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const definitions = {
|
||||||
|
hydrateRTSDisplaySet: {
|
||||||
|
commandFn: actions.hydrateRTSDisplaySet,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
actions,
|
||||||
|
definitions,
|
||||||
|
defaultContext: 'cornerstone-dicom-rt',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default commandsModule;
|
||||||
@ -72,22 +72,27 @@ function _getDisplaySetsFromSeries(
|
|||||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||||
|
|
||||||
displaySet.getReferenceDisplaySet = () => {
|
const { displaySetService } = servicesManager.services;
|
||||||
const { displaySetService } = servicesManager.services;
|
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
displaySet.referencedSeriesInstanceUID
|
||||||
displaySet.referencedSeriesInstanceUID
|
);
|
||||||
|
|
||||||
|
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||||
|
// Instead of throwing error, subscribe to display sets added
|
||||||
|
const { unsubscribe } = displaySetService.subscribe(
|
||||||
|
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||||
|
({ displaySetsAdded }) => {
|
||||||
|
const addedDisplaySet = displaySetsAdded[0];
|
||||||
|
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
|
||||||
|
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
|
||||||
throw new Error('Referenced DisplaySet is missing for the RT');
|
|
||||||
}
|
|
||||||
|
|
||||||
const referencedDisplaySet = referencedDisplaySets[0];
|
const referencedDisplaySet = referencedDisplaySets[0];
|
||||||
|
|
||||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||||
|
}
|
||||||
return referencedDisplaySet;
|
|
||||||
};
|
|
||||||
|
|
||||||
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
|
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
|
||||||
|
|
||||||
@ -111,19 +116,13 @@ function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensio
|
|||||||
// and also return the same promise to any other callers.
|
// and also return the same promise to any other callers.
|
||||||
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||||
if (!rtDisplaySet.structureSet) {
|
if (!rtDisplaySet.structureSet) {
|
||||||
const structureSet = await loadRTStruct(
|
const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers);
|
||||||
extensionManager,
|
|
||||||
rtDisplaySet,
|
|
||||||
rtDisplaySet.getReferenceDisplaySet(),
|
|
||||||
headers
|
|
||||||
);
|
|
||||||
|
|
||||||
rtDisplaySet.structureSet = structureSet;
|
rtDisplaySet.structureSet = structureSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
const suppressEvents = true;
|
|
||||||
segmentationService
|
segmentationService
|
||||||
.createSegmentationForRTDisplaySet(rtDisplaySet, null, suppressEvents)
|
.createSegmentationForRTDisplaySet(rtDisplaySet)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
rtDisplaySet.loading = false;
|
rtDisplaySet.loading = false;
|
||||||
resolve();
|
resolve();
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { id } from './id';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||||
|
import getCommandsModule from './getCommandsModule';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
|
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
|
||||||
@ -24,6 +25,7 @@ const extension: Types.Extensions.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,
|
||||||
|
getCommandsModule,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
|||||||
@ -89,12 +89,7 @@ async function checkAndLoadContourData(instance, datasource) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function loadRTStruct(
|
export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) {
|
||||||
extensionManager,
|
|
||||||
rtStructDisplaySet,
|
|
||||||
referencedDisplaySet,
|
|
||||||
headers
|
|
||||||
) {
|
|
||||||
const utilityModule = extensionManager.getModuleEntry(
|
const utilityModule = extensionManager.getModuleEntry(
|
||||||
'@ohif/extension-cornerstone.utilityModule.common'
|
'@ohif/extension-cornerstone.utilityModule.common'
|
||||||
);
|
);
|
||||||
@ -102,8 +97,6 @@ export default async function loadRTStruct(
|
|||||||
const { bulkDataURI } = dataSource.getConfig?.() || {};
|
const { bulkDataURI } = dataSource.getConfig?.() || {};
|
||||||
|
|
||||||
const { dicomLoaderService } = utilityModule.exports;
|
const { dicomLoaderService } = utilityModule.exports;
|
||||||
const imageIdSopInstanceUidPairs =
|
|
||||||
_getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
|
|
||||||
|
|
||||||
// Set here is loading is asynchronous.
|
// Set here is loading is asynchronous.
|
||||||
// If this function throws its set back to false.
|
// If this function throws its set back to false.
|
||||||
@ -149,8 +142,7 @@ export default async function loadRTStruct(
|
|||||||
|
|
||||||
const contourPoints = [];
|
const contourPoints = [];
|
||||||
for (let c = 0; c < ContourSequenceArray.length; c++) {
|
for (let c = 0; c < ContourSequenceArray.length; c++) {
|
||||||
const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } =
|
const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c];
|
||||||
ContourSequenceArray[c];
|
|
||||||
|
|
||||||
let isSupported = false;
|
let isSupported = false;
|
||||||
|
|
||||||
@ -194,24 +186,6 @@ export default async function loadRTStruct(
|
|||||||
return structureSet;
|
return structureSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
|
||||||
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
|
|
||||||
imageIdSopInstanceUidPairsEntry =>
|
|
||||||
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
|
|
||||||
return referencedDisplaySet.images.map(image => {
|
|
||||||
return {
|
|
||||||
imageId: image.imageId,
|
|
||||||
sopInstanceUID: image.SOPInstanceUID,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _setROIContourMetadata(
|
function _setROIContourMetadata(
|
||||||
structureSet,
|
structureSet,
|
||||||
StructureSetROISequence,
|
StructureSetROISequence,
|
||||||
|
|||||||
@ -10,7 +10,6 @@ function promptHydrateRT({
|
|||||||
servicesManager,
|
servicesManager,
|
||||||
rtDisplaySet,
|
rtDisplaySet,
|
||||||
viewportId,
|
viewportId,
|
||||||
toolGroupId = 'default',
|
|
||||||
preHydrateCallbacks,
|
preHydrateCallbacks,
|
||||||
hydrateRTDisplaySet,
|
hydrateRTDisplaySet,
|
||||||
}: withAppTypes) {
|
}: withAppTypes) {
|
||||||
@ -27,16 +26,13 @@ function promptHydrateRT({
|
|||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.setTimeout(async () => {
|
const isHydrated = await hydrateRTDisplaySet({
|
||||||
const isHydrated = await hydrateRTDisplaySet({
|
rtDisplaySet,
|
||||||
rtDisplaySet,
|
viewportId,
|
||||||
viewportId,
|
servicesManager,
|
||||||
toolGroupId,
|
});
|
||||||
servicesManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve(isHydrated);
|
resolve(isHydrated);
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } f
|
|||||||
|
|
||||||
import promptHydrateRT from '../utils/promptHydrateRT';
|
import promptHydrateRT from '../utils/promptHydrateRT';
|
||||||
import _getStatusComponent from './_getStatusComponent';
|
import _getStatusComponent from './_getStatusComponent';
|
||||||
|
|
||||||
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||||
|
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||||
|
|
||||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||||
|
|
||||||
@ -62,7 +64,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
|
|
||||||
const { viewports, activeViewportId } = viewportGrid;
|
const { viewports, activeViewportId } = viewportGrid;
|
||||||
|
|
||||||
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
|
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
|
||||||
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||||
|
referencedDisplaySetInstanceUID
|
||||||
|
);
|
||||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
|
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
|
||||||
|
|
||||||
referencedDisplaySetRef.current = {
|
referencedDisplaySetRef.current = {
|
||||||
@ -91,12 +96,15 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
});
|
});
|
||||||
}, [viewportGrid]);
|
}, [viewportGrid]);
|
||||||
|
|
||||||
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => {
|
const hydrateRTDisplaySet = useCallback(
|
||||||
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
|
({ rtDisplaySet, viewportId }) => {
|
||||||
displaySets: [rtDisplaySet],
|
commandsManager.runCommand('hydrateRTSDisplaySet', {
|
||||||
viewportId,
|
displaySet: rtDisplaySet,
|
||||||
});
|
viewportId,
|
||||||
};
|
});
|
||||||
|
},
|
||||||
|
[commandsManager]
|
||||||
|
);
|
||||||
|
|
||||||
const getCornerstoneViewport = useCallback(() => {
|
const getCornerstoneViewport = useCallback(() => {
|
||||||
const { component: Component } = extensionManager.getModuleEntry(
|
const { component: Component } = extensionManager.getModuleEntry(
|
||||||
@ -143,10 +151,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
|
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
|
||||||
setSelectedSegment(newSelectedSegmentIndex);
|
setSelectedSegment(newSelectedSegmentIndex);
|
||||||
},
|
},
|
||||||
[selectedSegment]
|
[selectedSegment, segmentationService]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -243,7 +251,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// remove the segmentation representations if seg displayset changed
|
// remove the segmentation representations if seg displayset changed
|
||||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
segmentationService.removeSegmentationRepresentations(viewportId);
|
||||||
|
|
||||||
toolGroupService.destroyToolGroup(toolGroupId);
|
toolGroupService.destroyToolGroup(toolGroupId);
|
||||||
};
|
};
|
||||||
@ -254,11 +262,27 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// remove the segmentation representations if seg displayset changed
|
// remove the segmentation representations if seg displayset changed
|
||||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
segmentationService.removeSegmentationRepresentations(viewportId);
|
||||||
referencedDisplaySetRef.current = null;
|
referencedDisplaySetRef.current = null;
|
||||||
};
|
};
|
||||||
}, [rtDisplaySet]);
|
}, [rtDisplaySet]);
|
||||||
|
|
||||||
|
const onStatusClick = useCallback(async () => {
|
||||||
|
// Before hydrating a RT and make it added to all viewports in the grid
|
||||||
|
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||||
|
// presentation state, so that we can restore it after hydrating the RT. This is
|
||||||
|
// required if the user has changed the viewport (other viewport than RT viewport)
|
||||||
|
// presentation state (w/l and invert) and then opens the RT. If we don't store
|
||||||
|
// the presentation state, the viewport will be reset to the default presentation
|
||||||
|
storePresentationState();
|
||||||
|
const isHydrated = await hydrateRTDisplaySet({
|
||||||
|
rtDisplaySet,
|
||||||
|
viewportId,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsHydrated(isHydrated);
|
||||||
|
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
|
||||||
|
|
||||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
let childrenWithProps = null;
|
let childrenWithProps = null;
|
||||||
|
|
||||||
@ -282,24 +306,8 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const onStatusClick = useCallback(async () => {
|
|
||||||
// Before hydrating a RT and make it added to all viewports in the grid
|
|
||||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
|
||||||
// presentation state, so that we can restore it after hydrating the RT. This is
|
|
||||||
// required if the user has changed the viewport (other viewport than RT viewport)
|
|
||||||
// presentation state (w/l and invert) and then opens the RT. If we don't store
|
|
||||||
// the presentation state, the viewport will be reset to the default presentation
|
|
||||||
storePresentationState();
|
|
||||||
const isHydrated = await hydrateRTDisplaySet({
|
|
||||||
rtDisplaySet,
|
|
||||||
viewportId,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsHydrated(isHydrated);
|
|
||||||
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
viewportActionCornersService.setComponents([
|
viewportActionCornersService.addComponents([
|
||||||
{
|
{
|
||||||
viewportId,
|
viewportId,
|
||||||
id: 'viewportStatusComponent',
|
id: 'viewportStatusComponent',
|
||||||
@ -318,7 +326,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
key="actionArrows"
|
key="actionArrows"
|
||||||
onArrowsClick={onSegmentChange}
|
onArrowsClick={onSegmentChange}
|
||||||
className={
|
className={
|
||||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
|
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
|
||||||
}
|
}
|
||||||
></ViewportActionArrows>
|
></ViewportActionArrows>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -46,9 +46,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^1.86.0",
|
"@cornerstonejs/adapters": "^2.1.13",
|
||||||
"@cornerstonejs/core": "^1.86.0",
|
"@cornerstonejs/core": "^2.1.13",
|
||||||
"@kitware/vtk.js": "30.4.1",
|
"@kitware/vtk.js": "32.1.0",
|
||||||
"react-color": "^2.19.3"
|
"react-color": "^2.19.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,18 +14,22 @@ import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchi
|
|||||||
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
||||||
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||||
|
|
||||||
import {
|
|
||||||
updateViewportsForSegmentationRendering,
|
|
||||||
getUpdatedViewportsForSegmentation,
|
|
||||||
getTargetViewport,
|
|
||||||
} from './utils/hydrationUtils';
|
|
||||||
const { segmentation: segmentationUtils } = utilities;
|
const { segmentation: segmentationUtils } = utilities;
|
||||||
|
|
||||||
const { datasetToBlob } = dcmjs.data;
|
const { datasetToBlob } = dcmjs.data;
|
||||||
|
|
||||||
|
const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
||||||
|
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||||
|
const targetViewportId = viewportId || activeViewportId;
|
||||||
|
|
||||||
|
const viewport = viewports.get(targetViewportId);
|
||||||
|
|
||||||
|
return viewport;
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
Cornerstone3D: {
|
Cornerstone3D: {
|
||||||
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation },
|
Segmentation: { generateSegmentation },
|
||||||
},
|
},
|
||||||
} = adaptersSEG;
|
} = adaptersSEG;
|
||||||
|
|
||||||
@ -42,87 +46,14 @@ const commandsModule = ({
|
|||||||
extensionManager,
|
extensionManager,
|
||||||
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
||||||
const {
|
const {
|
||||||
uiNotificationService,
|
|
||||||
segmentationService,
|
segmentationService,
|
||||||
uiDialogService,
|
uiDialogService,
|
||||||
displaySetService,
|
displaySetService,
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
cornerstoneViewportService,
|
} = servicesManager.services as AppTypes.Services;
|
||||||
} = servicesManager.services;
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
/**
|
|
||||||
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
|
|
||||||
* This function evaluates viewports based on their compatibility with the provided segmentation's
|
|
||||||
* frame of reference UID and appends them to the updated list if they should render the segmentation.
|
|
||||||
*
|
|
||||||
* @param {Object} params - Parameters for the function.
|
|
||||||
* @param params.viewportId - the ID of the viewport to be updated.
|
|
||||||
* @param params.servicesManager - The services manager
|
|
||||||
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
|
|
||||||
*
|
|
||||||
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
|
|
||||||
*/
|
|
||||||
getUpdatedViewportsForSegmentation,
|
|
||||||
/**
|
|
||||||
* Creates an empty segmentation for a specified viewport.
|
|
||||||
* It first checks if the display set associated with the viewport is reconstructable.
|
|
||||||
* If not, it raises a notification error. Otherwise, it creates a new segmentation
|
|
||||||
* for the display set after handling the necessary steps for making the viewport
|
|
||||||
* a volume viewport first
|
|
||||||
*
|
|
||||||
* @param {Object} params - Parameters for the function.
|
|
||||||
* @param params.viewportId - the target viewport ID.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
createEmptySegmentationForViewport: async ({ viewportId }) => {
|
|
||||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
|
||||||
// Todo: add support for multiple display sets
|
|
||||||
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
|
||||||
|
|
||||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
|
||||||
|
|
||||||
if (!displaySet.isReconstructable) {
|
|
||||||
uiNotificationService.show({
|
|
||||||
title: 'Segmentation',
|
|
||||||
message: 'Segmentation is not supported for non-reconstructible displaysets yet',
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateViewportsForSegmentationRendering({
|
|
||||||
viewportId,
|
|
||||||
servicesManager,
|
|
||||||
displaySet,
|
|
||||||
loadFn: async () => {
|
|
||||||
const currentSegmentations = segmentationService.getSegmentations();
|
|
||||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
|
||||||
displaySetInstanceUID,
|
|
||||||
{ label: `Segmentation ${currentSegmentations.length + 1}` }
|
|
||||||
);
|
|
||||||
|
|
||||||
const toolGroupId = viewport.viewportOptions.toolGroupId;
|
|
||||||
|
|
||||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
|
||||||
toolGroupId,
|
|
||||||
segmentationId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add only one segment for now
|
|
||||||
segmentationService.addSegment(segmentationId, {
|
|
||||||
toolGroupId,
|
|
||||||
segmentIndex: 1,
|
|
||||||
properties: {
|
|
||||||
label: 'Segment 1',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return segmentationId;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Loads segmentations for a specified viewport.
|
* Loads segmentations for a specified viewport.
|
||||||
* The function prepares the viewport for rendering, then loads the segmentation details.
|
* The function prepares the viewport for rendering, then loads the segmentation details.
|
||||||
@ -134,106 +65,30 @@ const commandsModule = ({
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
|
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
|
||||||
updateViewportsForSegmentationRendering({
|
|
||||||
viewportId,
|
|
||||||
servicesManager,
|
|
||||||
loadFn: async () => {
|
|
||||||
// Todo: handle adding more than one segmentation
|
|
||||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
|
||||||
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
|
||||||
|
|
||||||
const segmentation = segmentations[0];
|
|
||||||
const segmentationId = segmentation.id;
|
|
||||||
const label = segmentation.label;
|
|
||||||
const segments = segmentation.segments;
|
|
||||||
|
|
||||||
delete segmentation.segments;
|
|
||||||
|
|
||||||
await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, {
|
|
||||||
segmentationId,
|
|
||||||
label,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (segmentation.scalarData) {
|
|
||||||
const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId);
|
|
||||||
labelmapVolume.scalarData.set(segmentation.scalarData);
|
|
||||||
}
|
|
||||||
|
|
||||||
segmentationService.addOrUpdateSegmentation(segmentation);
|
|
||||||
|
|
||||||
const toolGroupId = viewport.viewportOptions.toolGroupId;
|
|
||||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
|
||||||
toolGroupId,
|
|
||||||
segmentationId
|
|
||||||
);
|
|
||||||
|
|
||||||
segments.forEach(segment => {
|
|
||||||
if (segment === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
segmentationService.addSegment(segmentationId, {
|
|
||||||
segmentIndex: segment.segmentIndex,
|
|
||||||
toolGroupId,
|
|
||||||
properties: {
|
|
||||||
color: segment.color,
|
|
||||||
label: segment.label,
|
|
||||||
opacity: segment.opacity,
|
|
||||||
isLocked: segment.isLocked,
|
|
||||||
visibility: segment.isVisible,
|
|
||||||
active: segmentation.activeSegmentIndex === segment.segmentIndex,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (segmentation.centroidsIJK) {
|
|
||||||
segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK);
|
|
||||||
}
|
|
||||||
|
|
||||||
return segmentationId;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Loads segmentation display sets for a specified viewport.
|
|
||||||
* Depending on the modality of the display set (SEG or RTSTRUCT),
|
|
||||||
* it chooses the appropriate service function to create
|
|
||||||
* the segmentation for the display set.
|
|
||||||
* The function then prepares the viewport for rendering segmentation.
|
|
||||||
*
|
|
||||||
* @param {Object} params - Parameters for the function.
|
|
||||||
* @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded.
|
|
||||||
* @param params.displaySets - Array of display sets to be loaded for segmentation.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
|
|
||||||
// Todo: handle adding more than one segmentation
|
// Todo: handle adding more than one segmentation
|
||||||
const displaySet = displaySets[0];
|
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
||||||
displaySet.referencedDisplaySetInstanceUID
|
|
||||||
);
|
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
|
||||||
const initialSliceIndex = viewport.getSliceIndex();
|
|
||||||
|
|
||||||
updateViewportsForSegmentationRendering({
|
const segmentation = segmentations[0];
|
||||||
viewportId,
|
const segmentationId = segmentation.segmentationId;
|
||||||
servicesManager,
|
const label = segmentation.config.label;
|
||||||
displaySet,
|
const segments = segmentation.config.segments;
|
||||||
loadFn: async () => {
|
|
||||||
const segDisplaySet = displaySet;
|
|
||||||
const suppressEvents = false;
|
|
||||||
const serviceFunction =
|
|
||||||
segDisplaySet.Modality === 'SEG'
|
|
||||||
? 'createSegmentationForSEGDisplaySet'
|
|
||||||
: 'createSegmentationForRTDisplaySet';
|
|
||||||
|
|
||||||
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||||
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
|
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
await segmentationService.createLabelmapForDisplaySet(displaySet, {
|
||||||
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
|
segmentationId,
|
||||||
return segmentationId;
|
segments,
|
||||||
},
|
label,
|
||||||
initialSliceIndex,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
segmentationService.addOrUpdateSegmentation(segmentation);
|
||||||
|
|
||||||
|
await segmentationService.addSegmentationRepresentation(viewport.viewportId, {
|
||||||
|
segmentationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return segmentationId;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Generates a segmentation from a given segmentation ID.
|
* Generates a segmentation from a given segmentation ID.
|
||||||
@ -251,25 +106,61 @@ const commandsModule = ({
|
|||||||
generateSegmentation: ({ segmentationId, options = {} }) => {
|
generateSegmentation: ({ segmentationId, options = {} }) => {
|
||||||
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
|
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
|
||||||
|
|
||||||
const { referencedVolumeId } = segmentation.representationData.LABELMAP;
|
const { imageIds } = segmentation.representationData.Labelmap;
|
||||||
|
|
||||||
const segmentationVolume = cache.getVolume(segmentationId);
|
const segImages = imageIds.map(imageId => cache.getImage(imageId));
|
||||||
const referencedVolume = cache.getVolume(referencedVolumeId);
|
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
|
||||||
const referencedImages = referencedVolume.getCornerstoneImages();
|
|
||||||
|
|
||||||
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume);
|
const labelmaps2D = [];
|
||||||
|
|
||||||
// Generate fake metadata as an example
|
let z = 0;
|
||||||
labelmapObj.metadata = [];
|
|
||||||
|
for (const segImage of segImages) {
|
||||||
|
const segmentsOnLabelmap = new Set();
|
||||||
|
const pixelData = segImage.getPixelData();
|
||||||
|
const { rows, columns } = segImage;
|
||||||
|
|
||||||
|
// Use a single pass through the pixel data
|
||||||
|
for (let i = 0; i < pixelData.length; i++) {
|
||||||
|
const segment = pixelData[i];
|
||||||
|
if (segment !== 0) {
|
||||||
|
segmentsOnLabelmap.add(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labelmaps2D[z++] = {
|
||||||
|
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
|
||||||
|
pixelData,
|
||||||
|
rows,
|
||||||
|
columns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap);
|
||||||
|
|
||||||
|
const labelmap3D = {
|
||||||
|
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
|
||||||
|
metadata: [],
|
||||||
|
labelmaps2D,
|
||||||
|
};
|
||||||
|
|
||||||
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
|
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
|
||||||
segmentationInOHIF.segments.forEach(segment => {
|
const representations = segmentationService.getRepresentationsForSegmentation(segmentationId);
|
||||||
|
|
||||||
|
Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => {
|
||||||
// segmentation service already has a color for each segment
|
// segmentation service already has a color for each segment
|
||||||
if (!segment) {
|
if (!segment) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const segmentIndex = segment.segmentIndex;
|
|
||||||
const { label, color } = segment;
|
const { label } = segment;
|
||||||
|
|
||||||
|
const firstRepresentation = representations[0];
|
||||||
|
const color = segmentationService.getSegmentColor(
|
||||||
|
firstRepresentation.viewportId,
|
||||||
|
segmentationId,
|
||||||
|
segment.segmentIndex
|
||||||
|
);
|
||||||
|
|
||||||
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
|
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
|
||||||
color.slice(0, 3).map(value => value / 255)
|
color.slice(0, 3).map(value => value / 255)
|
||||||
@ -292,12 +183,12 @@ const commandsModule = ({
|
|||||||
CodeMeaning: 'Tissue',
|
CodeMeaning: 'Tissue',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
labelmapObj.metadata[segmentIndex] = segmentMetadata;
|
labelmap3D.metadata[segmentIndex] = segmentMetadata;
|
||||||
});
|
});
|
||||||
|
|
||||||
const generatedSegmentation = generateSegmentation(
|
const generatedSegmentation = generateSegmentation(
|
||||||
referencedImages,
|
referencedImages,
|
||||||
labelmapObj,
|
labelmap3D,
|
||||||
metaData,
|
metaData,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
@ -445,18 +336,19 @@ const commandsModule = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
getUpdatedViewportsForSegmentation: {
|
/**
|
||||||
commandFn: actions.getUpdatedViewportsForSegmentation,
|
* Obsolete?
|
||||||
},
|
*/
|
||||||
loadSegmentationDisplaySetsForViewport: {
|
loadSegmentationDisplaySetsForViewport: {
|
||||||
commandFn: actions.loadSegmentationDisplaySetsForViewport,
|
commandFn: actions.loadSegmentationDisplaySetsForViewport,
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Obsolete?
|
||||||
|
*/
|
||||||
loadSegmentationsForViewport: {
|
loadSegmentationsForViewport: {
|
||||||
commandFn: actions.loadSegmentationsForViewport,
|
commandFn: actions.loadSegmentationsForViewport,
|
||||||
},
|
},
|
||||||
createEmptySegmentationForViewport: {
|
|
||||||
commandFn: actions.createEmptySegmentationForViewport,
|
|
||||||
},
|
|
||||||
generateSegmentation: {
|
generateSegmentation: {
|
||||||
commandFn: actions.generateSegmentation,
|
commandFn: actions.generateSegmentation,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,6 +20,17 @@ const segProtocol: Types.HangingProtocol.Protocol = {
|
|||||||
viewportType: 'stack',
|
viewportType: 'stack',
|
||||||
toolGroupId: 'default',
|
toolGroupId: 'default',
|
||||||
allowUnmatchedView: true,
|
allowUnmatchedView: true,
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'hydrateseg',
|
||||||
|
id: 'sameFORId',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
// options: {
|
||||||
|
// matchingRules: ['sameFOR'],
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
@ -52,7 +63,20 @@ const segProtocol: Types.HangingProtocol.Protocol = {
|
|||||||
},
|
},
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
viewportOptions: { allowUnmatchedView: true },
|
viewportOptions: {
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'hydrateseg',
|
||||||
|
id: 'sameFORId',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
// options: {
|
||||||
|
// matchingRules: ['sameFOR'],
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
id: 'segDisplaySetId',
|
id: 'segDisplaySetId',
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
||||||
import { CONSTANTS } from '@cornerstonejs/tools';
|
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
|
||||||
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
||||||
|
|
||||||
import { SOPClassHandlerId } from './id';
|
import { SOPClassHandlerId } from './id';
|
||||||
@ -70,28 +70,32 @@ function _getDisplaySetsFromSeries(
|
|||||||
|
|
||||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||||
|
const { displaySetService } = servicesManager.services;
|
||||||
|
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||||
|
displaySet.referencedSeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
displaySet.getReferenceDisplaySet = () => {
|
const referencedDisplaySet = referencedDisplaySets[0];
|
||||||
const { displaySetService } = servicesManager.services;
|
|
||||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
if (!referencedDisplaySet) {
|
||||||
displaySet.referencedSeriesInstanceUID
|
// subscribe to display sets added which means at some point it will be available
|
||||||
|
const { unsubscribe } = displaySetService.subscribe(
|
||||||
|
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||||
|
({ displaySetsAdded }) => {
|
||||||
|
// here we can also do a little bit of search, since sometimes DICOM SEG
|
||||||
|
// does not contain the referenced display set uid , and we can just
|
||||||
|
// see which of the display sets added is more similar and assign it
|
||||||
|
// to the referencedDisplaySet
|
||||||
|
const addedDisplaySet = displaySetsAdded[0];
|
||||||
|
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
|
||||||
|
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
|
||||||
throw new Error('Referenced DisplaySet is missing for the SEG');
|
|
||||||
}
|
|
||||||
|
|
||||||
const referencedDisplaySet = referencedDisplaySets[0];
|
|
||||||
|
|
||||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||||
|
}
|
||||||
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
|
|
||||||
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
|
|
||||||
const referencedVolumeId = `cornerstoneStreamingImageVolume:${displaySet.referencedVolumeURI}`;
|
|
||||||
displaySet.referencedVolumeId = referencedVolumeId;
|
|
||||||
|
|
||||||
return referencedDisplaySet;
|
|
||||||
};
|
|
||||||
|
|
||||||
displaySet.load = async ({ headers }) =>
|
displaySet.load = async ({ headers }) =>
|
||||||
await _load(displaySet, servicesManager, extensionManager, headers);
|
await _load(displaySet, servicesManager, extensionManager, headers);
|
||||||
@ -111,7 +115,7 @@ function _load(
|
|||||||
if (
|
if (
|
||||||
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
|
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
|
||||||
loadPromises[SOPInstanceUID] &&
|
loadPromises[SOPInstanceUID] &&
|
||||||
_segmentationExists(segDisplaySet, segmentationService)
|
_segmentationExists(segDisplaySet)
|
||||||
) {
|
) {
|
||||||
return loadPromises[SOPInstanceUID];
|
return loadPromises[SOPInstanceUID];
|
||||||
}
|
}
|
||||||
@ -135,9 +139,8 @@ function _load(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const suppressEvents = true;
|
|
||||||
segmentationService
|
segmentationService
|
||||||
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents)
|
.createSegmentationForSEGDisplaySet(segDisplaySet)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
segDisplaySet.loading = false;
|
segDisplaySet.loading = false;
|
||||||
resolve();
|
resolve();
|
||||||
@ -166,20 +169,20 @@ async function _loadSegments({
|
|||||||
const { dicomLoaderService } = utilityModule.exports;
|
const { dicomLoaderService } = utilityModule.exports;
|
||||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
|
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
|
||||||
|
|
||||||
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
|
const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
|
||||||
|
segDisplaySet.referencedDisplaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
if (!cachedReferencedVolume) {
|
if (!referencedDisplaySet) {
|
||||||
throw new Error(
|
throw new Error('referencedDisplaySet is missing for SEG');
|
||||||
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { imageIds } = cachedReferencedVolume;
|
const { instances: images } = referencedDisplaySet;
|
||||||
|
const imageIds = images.map(({ imageId }) => imageId);
|
||||||
|
|
||||||
// Todo: what should be defaults here
|
// Todo: what should be defaults here
|
||||||
const tolerance = 0.001;
|
const tolerance = 0.001;
|
||||||
const skipOverlapping = true;
|
const skipOverlapping = true;
|
||||||
|
|
||||||
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
|
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
|
||||||
const { percentComplete } = evt.detail;
|
const { percentComplete } = evt.detail;
|
||||||
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
|
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
|
||||||
@ -231,9 +234,8 @@ async function _loadSegments({
|
|||||||
Object.assign(segDisplaySet, results);
|
Object.assign(segDisplaySet, results);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _segmentationExists(segDisplaySet, segmentationService: AppTypes.SegmentationService) {
|
function _segmentationExists(segDisplaySet) {
|
||||||
// This should be abstracted with the CornerstoneCacheService
|
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||||
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
|
|||||||
// forcing the button to have black background since initially
|
// forcing the button to have black background since initially
|
||||||
// it is designed for the toolbox not the toolbar on top
|
// it is designed for the toolbox not the toolbar on top
|
||||||
// we should then branch the buttonSectionId to have different styles
|
// we should then branch the buttonSectionId to have different styles
|
||||||
const segmentations = segmentationService.getSegmentations();
|
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
|
||||||
if (!segmentations?.length) {
|
if (!segmentations?.length) {
|
||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import React from 'react';
|
|||||||
|
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import getPanelModule from './getPanelModule';
|
|
||||||
import getCommandsModule from './commandsModule';
|
import getCommandsModule from './commandsModule';
|
||||||
import { getToolbarModule } from './getToolbarModule';
|
import { getToolbarModule } from './getToolbarModule';
|
||||||
|
|
||||||
@ -28,13 +27,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,
|
||||||
/**
|
|
||||||
* PanelModule should provide a list of panels that will be available in OHIF
|
|
||||||
* for Modes to consume and render. Each panel is defined by a {name,
|
|
||||||
* iconName, iconLabel, label, component} object. Example of a panel module
|
|
||||||
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
|
||||||
*/
|
|
||||||
getPanelModule,
|
|
||||||
getCommandsModule,
|
getCommandsModule,
|
||||||
getToolbarModule,
|
getToolbarModule,
|
||||||
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||||
|
|||||||
@ -1,393 +0,0 @@
|
|||||||
import { createReportAsync } from '@ohif/extension-default';
|
|
||||||
import React, { useEffect, useState, useCallback } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui';
|
|
||||||
import { SegmentationPanelMode } from '../types/segmentation';
|
|
||||||
import callInputDialog from './callInputDialog';
|
|
||||||
import callColorPickerDialog from './colorPickerDialog';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Separator } from '@ohif/ui-next';
|
|
||||||
|
|
||||||
const components = {
|
|
||||||
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
|
|
||||||
[SegmentationPanelMode.Dropdown]: SegmentationGroupTable,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PanelSegmentation({
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
extensionManager,
|
|
||||||
configuration,
|
|
||||||
}: withAppTypes) {
|
|
||||||
const {
|
|
||||||
segmentationService,
|
|
||||||
viewportGridService,
|
|
||||||
uiDialogService,
|
|
||||||
displaySetService,
|
|
||||||
cornerstoneViewportService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
|
|
||||||
const { t } = useTranslation('PanelSegmentation');
|
|
||||||
|
|
||||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
|
||||||
const [addSegmentationClassName, setAddSegmentationClassName] = useState('');
|
|
||||||
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
|
|
||||||
segmentationService.getConfiguration()
|
|
||||||
);
|
|
||||||
|
|
||||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// ~~ Subscription
|
|
||||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
|
||||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
|
||||||
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
|
||||||
const subscriptions = [];
|
|
||||||
|
|
||||||
[added, updated, removed].forEach(evt => {
|
|
||||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
|
||||||
const segmentations = segmentationService.getSegmentations();
|
|
||||||
setSegmentations(segmentations);
|
|
||||||
setSegmentationConfiguration(segmentationService.getConfiguration());
|
|
||||||
});
|
|
||||||
subscriptions.push(unsubscribe);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
subscriptions.forEach(unsub => {
|
|
||||||
unsub();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// temporary measure to not allow add segmentation when the selected viewport
|
|
||||||
// is stack viewport
|
|
||||||
useEffect(() => {
|
|
||||||
const handleActiveViewportChange = viewportId => {
|
|
||||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
|
|
||||||
viewportId || viewportGridService.getActiveViewportId()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!displaySetUIDs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isReconstructable =
|
|
||||||
displaySetUIDs?.some(displaySetUID => {
|
|
||||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
|
|
||||||
return displaySet?.isReconstructable;
|
|
||||||
}) || false;
|
|
||||||
|
|
||||||
if (isReconstructable) {
|
|
||||||
setAddSegmentationClassName('');
|
|
||||||
} else {
|
|
||||||
setAddSegmentationClassName('ohif-disabled');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle initial state
|
|
||||||
handleActiveViewportChange();
|
|
||||||
|
|
||||||
const changedGrid = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED;
|
|
||||||
const ready = viewportGridService.EVENTS.VIEWPORTS_READY;
|
|
||||||
|
|
||||||
const subsGrid = [];
|
|
||||||
[ready, changedGrid].forEach(evt => {
|
|
||||||
const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => {
|
|
||||||
handleActiveViewportChange(viewportId);
|
|
||||||
});
|
|
||||||
|
|
||||||
subsGrid.push(unsubscribe);
|
|
||||||
});
|
|
||||||
|
|
||||||
const changedData = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
|
||||||
|
|
||||||
const subsData = [];
|
|
||||||
[changedData].forEach(evt => {
|
|
||||||
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, () => {
|
|
||||||
handleActiveViewportChange();
|
|
||||||
});
|
|
||||||
|
|
||||||
subsData.push(unsubscribe);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
return () => {
|
|
||||||
subsGrid.forEach(unsub => unsub());
|
|
||||||
subsData.forEach(unsub => unsub());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getToolGroupIds = segmentationId => {
|
|
||||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
|
||||||
|
|
||||||
return toolGroupIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentationAdd = async () => {
|
|
||||||
commandsManager.runCommand('createEmptySegmentationForViewport', {
|
|
||||||
viewportId: viewportGridService.getActiveViewportId(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentationClick = (segmentationId: string) => {
|
|
||||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentationDelete = (segmentationId: string) => {
|
|
||||||
segmentationService.remove(segmentationId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentAdd = segmentationId => {
|
|
||||||
segmentationService.addSegment(segmentationId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentClick = (segmentationId, segmentIndex) => {
|
|
||||||
segmentationService.setActiveSegment(segmentationId, segmentIndex);
|
|
||||||
|
|
||||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
|
||||||
|
|
||||||
toolGroupIds.forEach(toolGroupId => {
|
|
||||||
// const toolGroupId =
|
|
||||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
|
||||||
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentEdit = (segmentationId, segmentIndex) => {
|
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
||||||
|
|
||||||
const segment = segmentation.segments[segmentIndex];
|
|
||||||
const { label } = segment;
|
|
||||||
|
|
||||||
callInputDialog(uiDialogService, label, (label, actionId) => {
|
|
||||||
if (label === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentationEdit = segmentationId => {
|
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
||||||
const { label } = segmentation;
|
|
||||||
|
|
||||||
callInputDialog(uiDialogService, label, (label, actionId) => {
|
|
||||||
if (label === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
segmentationService.addOrUpdateSegmentation(
|
|
||||||
{
|
|
||||||
id: segmentationId,
|
|
||||||
label,
|
|
||||||
},
|
|
||||||
false, // suppress event
|
|
||||||
true // notYetUpdatedAtSource
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentColorClick = (segmentationId, segmentIndex) => {
|
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
||||||
|
|
||||||
const segment = segmentation.segments[segmentIndex];
|
|
||||||
const { color, opacity } = segment;
|
|
||||||
|
|
||||||
const rgbaColor = {
|
|
||||||
r: color[0],
|
|
||||||
g: color[1],
|
|
||||||
b: color[2],
|
|
||||||
a: opacity / 255.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
|
||||||
if (actionId === 'cancel') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
|
|
||||||
newRgbaColor.r,
|
|
||||||
newRgbaColor.g,
|
|
||||||
newRgbaColor.b,
|
|
||||||
newRgbaColor.a * 255.0,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentDelete = (segmentationId, segmentIndex) => {
|
|
||||||
segmentationService.removeSegment(segmentationId, segmentIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
// segment hide
|
|
||||||
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
||||||
const segmentInfo = segmentation.segments[segmentIndex];
|
|
||||||
const isVisible = !segmentInfo.isVisible;
|
|
||||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
|
||||||
|
|
||||||
// Todo: right now we apply the visibility to all tool groups
|
|
||||||
toolGroupIds.forEach(toolGroupId => {
|
|
||||||
segmentationService.setSegmentVisibility(
|
|
||||||
segmentationId,
|
|
||||||
segmentIndex,
|
|
||||||
isVisible,
|
|
||||||
toolGroupId
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
|
|
||||||
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleSegmentationVisibility = segmentationId => {
|
|
||||||
segmentationService.toggleSegmentationVisibility(segmentationId);
|
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
||||||
const isVisible = segmentation.isVisible;
|
|
||||||
const segments = segmentation.segments;
|
|
||||||
|
|
||||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
|
||||||
|
|
||||||
toolGroupIds.forEach(toolGroupId => {
|
|
||||||
segments.forEach((segment, segmentIndex) => {
|
|
||||||
segmentationService.setSegmentVisibility(
|
|
||||||
segmentationId,
|
|
||||||
segmentIndex,
|
|
||||||
isVisible,
|
|
||||||
toolGroupId
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const _setSegmentationConfiguration = useCallback(
|
|
||||||
(segmentationId, key, value) => {
|
|
||||||
segmentationService.setConfiguration({
|
|
||||||
segmentationId,
|
|
||||||
[key]: value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[segmentationService]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSegmentationDownload = segmentationId => {
|
|
||||||
commandsManager.runCommand('downloadSegmentation', {
|
|
||||||
segmentationId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const storeSegmentation = async segmentationId => {
|
|
||||||
const datasources = extensionManager.getActiveDataSource();
|
|
||||||
|
|
||||||
const displaySetInstanceUIDs = await createReportAsync({
|
|
||||||
servicesManager,
|
|
||||||
getReport: () =>
|
|
||||||
commandsManager.runCommand('storeSegmentation', {
|
|
||||||
segmentationId,
|
|
||||||
dataSource: datasources[0],
|
|
||||||
}),
|
|
||||||
reportType: 'Segmentation',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show the exported report in the active viewport as read only (similar to SR)
|
|
||||||
if (displaySetInstanceUIDs) {
|
|
||||||
// clear the segmentation that we exported, similar to the storeMeasurement
|
|
||||||
// where we remove the measurements and prompt again the user if they would like
|
|
||||||
// to re-read the measurements in a SR read only viewport
|
|
||||||
segmentationService.remove(segmentationId);
|
|
||||||
|
|
||||||
viewportGridService.setDisplaySetsForViewport({
|
|
||||||
viewportId: viewportGridService.getActiveViewportId(),
|
|
||||||
displaySetInstanceUIDs,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentationDownloadRTSS = segmentationId => {
|
|
||||||
commandsManager.runCommand('downloadRTSS', {
|
|
||||||
segmentationId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const SegmentationGroupTableComponent =
|
|
||||||
components[configuration?.segmentationPanelMode] || SegmentationGroupTable;
|
|
||||||
const allowAddSegment = configuration?.addSegment;
|
|
||||||
const onSegmentationAddWrapper =
|
|
||||||
configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function'
|
|
||||||
? configuration?.onSegmentationAdd
|
|
||||||
: onSegmentationAdd;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SegmentationGroupTableComponent
|
|
||||||
title={t('Segmentations')}
|
|
||||||
segmentations={segmentations}
|
|
||||||
disableEditing={configuration.disableEditing}
|
|
||||||
activeSegmentationId={selectedSegmentationId || ''}
|
|
||||||
onSegmentationAdd={onSegmentationAddWrapper}
|
|
||||||
addSegmentationClassName={addSegmentationClassName}
|
|
||||||
showAddSegment={allowAddSegment}
|
|
||||||
onSegmentationClick={onSegmentationClick}
|
|
||||||
onSegmentationDelete={onSegmentationDelete}
|
|
||||||
onSegmentationDownload={onSegmentationDownload}
|
|
||||||
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
|
||||||
storeSegmentation={storeSegmentation}
|
|
||||||
onSegmentationEdit={onSegmentationEdit}
|
|
||||||
onSegmentClick={onSegmentClick}
|
|
||||||
onSegmentEdit={onSegmentEdit}
|
|
||||||
onSegmentAdd={onSegmentAdd}
|
|
||||||
onSegmentColorClick={onSegmentColorClick}
|
|
||||||
onSegmentDelete={onSegmentDelete}
|
|
||||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
|
||||||
onToggleSegmentLock={onToggleSegmentLock}
|
|
||||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
|
||||||
showDeleteSegment={true}
|
|
||||||
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
|
||||||
setRenderOutline={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
|
||||||
}
|
|
||||||
setOutlineOpacityActive={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
|
||||||
}
|
|
||||||
setRenderFill={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
|
||||||
}
|
|
||||||
setRenderInactiveSegmentations={value =>
|
|
||||||
_setSegmentationConfiguration(
|
|
||||||
selectedSegmentationId,
|
|
||||||
'renderInactiveSegmentations',
|
|
||||||
value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setOutlineWidthActive={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
|
||||||
}
|
|
||||||
setFillAlpha={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
|
||||||
}
|
|
||||||
setFillAlphaInactive={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelSegmentation.propTypes = {
|
|
||||||
commandsManager: PropTypes.shape({
|
|
||||||
runCommand: PropTypes.func.isRequired,
|
|
||||||
}),
|
|
||||||
servicesManager: PropTypes.shape({
|
|
||||||
services: PropTypes.shape({
|
|
||||||
segmentationService: PropTypes.shape({
|
|
||||||
getSegmentation: PropTypes.func.isRequired,
|
|
||||||
getSegmentations: PropTypes.func.isRequired,
|
|
||||||
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
|
||||||
subscribe: PropTypes.func.isRequired,
|
|
||||||
EVENTS: PropTypes.object.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
|
|
||||||
|
|
||||||
function callInputDialog(uiDialogService, label, callback) {
|
|
||||||
const dialogId = 'enter-segment-label';
|
|
||||||
|
|
||||||
const onSubmitHandler = ({ action, value }) => {
|
|
||||||
switch (action.id) {
|
|
||||||
case 'save':
|
|
||||||
callback(value.label, action.id);
|
|
||||||
break;
|
|
||||||
case 'cancel':
|
|
||||||
callback('', action.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
uiDialogService.dismiss({ id: dialogId });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (uiDialogService) {
|
|
||||||
uiDialogService.create({
|
|
||||||
id: dialogId,
|
|
||||||
centralize: true,
|
|
||||||
isDraggable: false,
|
|
||||||
showOverlay: true,
|
|
||||||
content: Dialog,
|
|
||||||
contentProps: {
|
|
||||||
title: 'Segment',
|
|
||||||
value: { label },
|
|
||||||
noCloseButton: true,
|
|
||||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
|
||||||
actions: [
|
|
||||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
|
||||||
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
|
|
||||||
],
|
|
||||||
onSubmit: onSubmitHandler,
|
|
||||||
body: ({ value, setValue }) => {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
label="Enter the segment label"
|
|
||||||
labelClassName="text-white text-[14px] leading-[1.2]"
|
|
||||||
autoFocus
|
|
||||||
className="border-primary-main bg-black"
|
|
||||||
type="text"
|
|
||||||
value={value.label}
|
|
||||||
onChange={event => {
|
|
||||||
event.persist();
|
|
||||||
setValue(value => ({ ...value, label: event.target.value }));
|
|
||||||
}}
|
|
||||||
onKeyPress={event => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
onSubmitHandler({ value, action: { id: 'save' } });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default callInputDialog;
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
// Todo: use defaults in cs3d
|
|
||||||
const initialState = {
|
|
||||||
renderOutline: true,
|
|
||||||
renderFill: true,
|
|
||||||
outlineOpacity: 0.9,
|
|
||||||
outlineWidth: 3,
|
|
||||||
fillOpacity: 0.9,
|
|
||||||
fillOpacityInactive: 0.8,
|
|
||||||
renderInactiveSegmentations: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const reducer = (state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'RENDER_OUTLINE':
|
|
||||||
return { ...state, renderOutline: action.payload.value };
|
|
||||||
case 'RENDER_FILL':
|
|
||||||
return { ...state, renderFill: action.payload.value };
|
|
||||||
case 'SET_OUTLINE_OPACITY':
|
|
||||||
return { ...state, outlineOpacity: action.payload.value };
|
|
||||||
case 'SET_OUTLINE_WIDTH':
|
|
||||||
return { ...state, outlineWidth: action.payload.value };
|
|
||||||
case 'SET_FILL_OPACITY':
|
|
||||||
return { ...state, fillOpacity: action.payload.value };
|
|
||||||
case 'SET_FILL_OPACITY_INACTIVE':
|
|
||||||
return { ...state, fillOpacityInactive: action.payload.value };
|
|
||||||
case 'RENDER_INACTIVE_SEGMENTATIONS':
|
|
||||||
return { ...state, renderInactiveSegmentations: action.payload.value };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export { initialState, reducer };
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
import { Enums, cache } from '@cornerstonejs/core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the viewports in preparation for rendering segmentations.
|
|
||||||
* Evaluates each viewport to determine which need modifications,
|
|
||||||
* then for those viewports, changes them to a volume type and ensures
|
|
||||||
* they are ready for segmentation rendering.
|
|
||||||
*
|
|
||||||
* @param {Object} params - Parameters for the function.
|
|
||||||
* @param params.viewportId - ID of the viewport to be updated.
|
|
||||||
* @param params.loadFn - Function to load the segmentation data.
|
|
||||||
* @param params.servicesManager - The services manager.
|
|
||||||
* @param params.displaySet - the display set.
|
|
||||||
* @param params.initialSliceIndex - The initial slice index.
|
|
||||||
*
|
|
||||||
* @returns Returns true upon successful update of viewports for segmentation rendering.
|
|
||||||
*/
|
|
||||||
async function updateViewportsForSegmentationRendering({
|
|
||||||
viewportId,
|
|
||||||
loadFn,
|
|
||||||
servicesManager,
|
|
||||||
displaySet,
|
|
||||||
initialSliceIndex = null,
|
|
||||||
}: {
|
|
||||||
viewportId: string;
|
|
||||||
loadFn: () => Promise<string>;
|
|
||||||
servicesManager: AppTypes.ServicesManager;
|
|
||||||
displaySet?: any;
|
|
||||||
initialSliceIndex?: number;
|
|
||||||
}) {
|
|
||||||
const { cornerstoneViewportService, segmentationService, viewportGridService } =
|
|
||||||
servicesManager.services;
|
|
||||||
|
|
||||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
|
||||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
|
||||||
|
|
||||||
const referencedDisplaySetInstanceUID =
|
|
||||||
displaySet?.referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
|
|
||||||
|
|
||||||
const updatedViewports = getUpdatedViewportsForSegmentation({
|
|
||||||
viewportId,
|
|
||||||
servicesManager,
|
|
||||||
displaySet,
|
|
||||||
});
|
|
||||||
|
|
||||||
// create Segmentation callback which needs to be waited until
|
|
||||||
// the volume is created (if coming from stack)
|
|
||||||
const createSegmentationForVolume = async () => {
|
|
||||||
const segmentationId = await loadFn();
|
|
||||||
segmentationService.hydrateSegmentation(segmentationId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// the reference volume that is used to draw the segmentation. so check if the
|
|
||||||
// volume exists in the cache (the target Viewport is already a volume viewport)
|
|
||||||
const volumeExists = Array.from(cache._volumeCache.keys()).some(volumeId =>
|
|
||||||
volumeId.includes(referencedDisplaySetInstanceUID)
|
|
||||||
);
|
|
||||||
|
|
||||||
updatedViewports.forEach(async viewport => {
|
|
||||||
viewport.viewportOptions = {
|
|
||||||
...viewport.viewportOptions,
|
|
||||||
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
|
|
||||||
needsRerendering: true,
|
|
||||||
};
|
|
||||||
const viewportId = viewport.viewportId;
|
|
||||||
|
|
||||||
// maintain the prehydration slice on the target viewport only
|
|
||||||
if (viewportId === targetViewportId) {
|
|
||||||
viewport.viewportOptions.initialImageOptions = {
|
|
||||||
index: initialSliceIndex,
|
|
||||||
useOnce: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
|
||||||
const prevCamera = csViewport.getCamera();
|
|
||||||
|
|
||||||
// only run the createSegmentationForVolume for the targetViewportId
|
|
||||||
// since the rest will get handled by cornerstoneViewportService
|
|
||||||
if ((volumeExists || displaySet.Modality === 'RTSTRUCT') && viewportId === targetViewportId) {
|
|
||||||
await createSegmentationForVolume();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createNewSegmentationWhenVolumeMounts = async evt => {
|
|
||||||
const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac =>
|
|
||||||
ac.uid.includes(referencedDisplaySetInstanceUID)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note: make sure to re-grab the viewport since it might have changed
|
|
||||||
// during the time it took for the volume to be mounted, for instance
|
|
||||||
// the stack viewport has been changed to a volume viewport
|
|
||||||
const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
|
||||||
volumeViewport.setCamera(prevCamera);
|
|
||||||
|
|
||||||
volumeViewport.element.removeEventListener(
|
|
||||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
|
||||||
createNewSegmentationWhenVolumeMounts
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isTheActiveViewportVolumeMounted) {
|
|
||||||
// it means it is one of those other updated viewports so just update the camera
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewportId === targetViewportId) {
|
|
||||||
await createSegmentationForVolume();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
csViewport.element.addEventListener(
|
|
||||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
|
||||||
createNewSegmentationWhenVolumeMounts
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set the displaySets for the viewports that require to be updated
|
|
||||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
|
||||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
|
||||||
const targetViewportId = viewportId || activeViewportId;
|
|
||||||
|
|
||||||
const viewport = viewports.get(targetViewportId);
|
|
||||||
|
|
||||||
return viewport;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
|
|
||||||
* This function evaluates viewports based on their compatibility with the provided segmentation's
|
|
||||||
* frame of reference UID and appends them to the updated list if they should render the segmentation.
|
|
||||||
*
|
|
||||||
* @param {Object} params - Parameters for the function.
|
|
||||||
* @param params.viewportId - the ID of the viewport to be updated.
|
|
||||||
* @param params.servicesManager - The services manager
|
|
||||||
* @param params.displaySet - the display set.
|
|
||||||
*
|
|
||||||
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
|
|
||||||
*/
|
|
||||||
function getUpdatedViewportsForSegmentation({
|
|
||||||
viewportId,
|
|
||||||
servicesManager,
|
|
||||||
displaySet,
|
|
||||||
}: withAppTypes) {
|
|
||||||
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
|
|
||||||
servicesManager.services;
|
|
||||||
|
|
||||||
const { viewports, isHangingProtocolLayout } = viewportGridService.getState();
|
|
||||||
|
|
||||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
|
||||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
|
||||||
|
|
||||||
const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs;
|
|
||||||
|
|
||||||
const referenceDisplaySetInstanceUID =
|
|
||||||
displaySet?.referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
|
|
||||||
|
|
||||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID);
|
|
||||||
const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID;
|
|
||||||
|
|
||||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
|
||||||
targetViewportId,
|
|
||||||
referenceDisplaySetInstanceUID,
|
|
||||||
isHangingProtocolLayout
|
|
||||||
);
|
|
||||||
|
|
||||||
viewports.forEach((viewport, viewportId) => {
|
|
||||||
if (
|
|
||||||
targetViewportId === viewportId ||
|
|
||||||
updatedViewports.find(v => v.viewportId === viewportId)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
|
||||||
viewport.displaySetInstanceUIDs,
|
|
||||||
segmentationFrameOfReferenceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldDisplaySeg) {
|
|
||||||
updatedViewports.push({
|
|
||||||
viewportId,
|
|
||||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
|
||||||
viewportOptions: {
|
|
||||||
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
|
|
||||||
needsRerendering: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
updateViewportsForSegmentationRendering,
|
|
||||||
getUpdatedViewportsForSegmentation,
|
|
||||||
getTargetViewport,
|
|
||||||
};
|
|
||||||
@ -11,7 +11,7 @@ function promptHydrateSEG({
|
|||||||
segDisplaySet,
|
segDisplaySet,
|
||||||
viewportId,
|
viewportId,
|
||||||
preHydrateCallbacks,
|
preHydrateCallbacks,
|
||||||
hydrateSEGDisplaySet,
|
hydrateCallback,
|
||||||
}: withAppTypes) {
|
}: withAppTypes) {
|
||||||
const { uiViewportDialogService } = servicesManager.services;
|
const { uiViewportDialogService } = servicesManager.services;
|
||||||
const extensionManager = servicesManager._extensionManager;
|
const extensionManager = servicesManager._extensionManager;
|
||||||
@ -28,7 +28,7 @@ function promptHydrateSEG({
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.setTimeout(async () => {
|
window.setTimeout(async () => {
|
||||||
const isHydrated = await hydrateSEGDisplaySet({
|
const isHydrated = await hydrateCallback({
|
||||||
segDisplaySet,
|
segDisplaySet,
|
||||||
viewportId,
|
viewportId,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
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 { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } 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';
|
||||||
|
import { useSegmentationPresentationStore } from '@ohif/extension-cornerstone';
|
||||||
|
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||||
|
|
||||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||||
|
|
||||||
@ -37,7 +38,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const segDisplaySet = displaySets[0];
|
const segDisplaySet = displaySets[0];
|
||||||
|
|
||||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
|
|
||||||
// States
|
// States
|
||||||
@ -61,7 +61,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
|
|
||||||
const { viewports, activeViewportId } = viewportGrid;
|
const { viewports, activeViewportId } = viewportGrid;
|
||||||
|
|
||||||
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
|
const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
|
||||||
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||||
|
referencedDisplaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||||
referencedDisplaySet,
|
referencedDisplaySet,
|
||||||
segDisplaySet
|
segDisplaySet
|
||||||
@ -98,18 +102,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||||
);
|
);
|
||||||
|
|
||||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
|
||||||
|
|
||||||
// Todo: jump to the center of the first segment
|
// Todo: jump to the center of the first segment
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
{...props}
|
{...props}
|
||||||
displaySets={[referencedDisplaySet, segDisplaySet]}
|
displaySets={[segDisplaySet]}
|
||||||
viewportOptions={{
|
viewportOptions={{
|
||||||
viewportType: 'volume',
|
viewportType: viewportOptions.viewportType,
|
||||||
toolGroupId: toolGroupId,
|
toolGroupId: toolGroupId,
|
||||||
orientation: viewportOptions.orientation,
|
orientation: viewportOptions.orientation,
|
||||||
viewportId: viewportOptions.viewportId,
|
viewportId: viewportOptions.viewportId,
|
||||||
|
presentationIds: viewportOptions.presentationIds,
|
||||||
}}
|
}}
|
||||||
onElementEnabled={evt => {
|
onElementEnabled={evt => {
|
||||||
props.onElementEnabled?.(evt);
|
props.onElementEnabled?.(evt);
|
||||||
@ -139,12 +142,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
|
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
|
||||||
setSelectedSegment(newSelectedSegmentIndex);
|
setSelectedSegment(newSelectedSegmentIndex);
|
||||||
},
|
},
|
||||||
[selectedSegment]
|
[selectedSegment]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hydrateSEG = useCallback(() => {
|
||||||
|
// update the previously stored segmentationPresentation with the new viewportId
|
||||||
|
// presentation so that when we put the referencedDisplaySet back in the viewport
|
||||||
|
// it will have the correct segmentation representation hydrated
|
||||||
|
commandsManager.runCommand('updateStoredSegmentationPresentation', {
|
||||||
|
displaySet: segDisplaySet,
|
||||||
|
type: SegmentationRepresentations.Labelmap,
|
||||||
|
});
|
||||||
|
|
||||||
|
// update the previously stored positionPresentation with the new viewportId
|
||||||
|
// presentation so that when we put the referencedDisplaySet back in the viewport
|
||||||
|
// it will be in the correct position zoom and pan
|
||||||
|
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||||
|
viewportId,
|
||||||
|
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
|
||||||
|
});
|
||||||
|
|
||||||
|
viewportGridService.setDisplaySetsForViewport({
|
||||||
|
viewportId,
|
||||||
|
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
|
||||||
|
});
|
||||||
|
}, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (segIsLoading) {
|
if (segIsLoading) {
|
||||||
return;
|
return;
|
||||||
@ -155,15 +181,18 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
viewportId,
|
viewportId,
|
||||||
segDisplaySet,
|
segDisplaySet,
|
||||||
preHydrateCallbacks: [storePresentationState],
|
preHydrateCallbacks: [storePresentationState],
|
||||||
hydrateSEGDisplaySet,
|
hydrateCallback: hydrateSEG,
|
||||||
}).then(isHydrated => {
|
}).then(isHydrated => {
|
||||||
if (isHydrated) {
|
if (isHydrated) {
|
||||||
setIsHydrated(true);
|
setIsHydrated(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [servicesManager, viewportId, segDisplaySet, segIsLoading]);
|
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// on new seg display set, remove all segmentations from all viewports
|
||||||
|
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||||
|
|
||||||
const { unsubscribe } = segmentationService.subscribe(
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||||
evt => {
|
evt => {
|
||||||
@ -222,6 +251,9 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
if (toolGroup) {
|
if (toolGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// always start fresh for this viewport since it is special type of viewport
|
||||||
|
// that should only show one segmentation at a time.
|
||||||
|
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||||
|
|
||||||
// This creates a custom tool group which has the lifetime of this view
|
// This creates a custom tool group which has the lifetime of this view
|
||||||
// only, and does NOT interfere with currently displayed segmentations.
|
// only, and does NOT interfere with currently displayed segmentations.
|
||||||
@ -229,33 +261,14 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// remove the segmentation representations if seg displayset changed
|
// remove the segmentation representations if seg displayset changed
|
||||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
// e.g., another seg displayset is dragged into the viewport
|
||||||
|
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||||
|
|
||||||
// Only destroy the viewport specific implementation
|
// Only destroy the viewport specific implementation
|
||||||
toolGroupService.destroyToolGroup(toolGroupId);
|
toolGroupService.destroyToolGroup(toolGroupId);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsHydrated(segDisplaySet.isHydrated);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// remove the segmentation representations if seg displayset changed
|
|
||||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
|
||||||
referencedDisplaySetRef.current = null;
|
|
||||||
};
|
|
||||||
}, [segDisplaySet]);
|
|
||||||
|
|
||||||
const hydrateSEGDisplaySet = useCallback(
|
|
||||||
({ segDisplaySet, viewportId }) => {
|
|
||||||
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
|
|
||||||
displaySets: [segDisplaySet],
|
|
||||||
viewportId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[commandsManager]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onStatusClick = useCallback(async () => {
|
const onStatusClick = useCallback(async () => {
|
||||||
// Before hydrating a SEG and make it added to all viewports in the grid
|
// 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
|
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||||
@ -264,16 +277,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
// presentation state (w/l and invert) and then opens the SEG. If we don't store
|
// 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
|
// the presentation state, the viewport will be reset to the default presentation
|
||||||
storePresentationState();
|
storePresentationState();
|
||||||
const isHydrated = await hydrateSEGDisplaySet({
|
hydrateSEG();
|
||||||
segDisplaySet,
|
}, [storePresentationState, hydrateSEG]);
|
||||||
viewportId,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsHydrated(isHydrated);
|
|
||||||
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
viewportActionCornersService.setComponents([
|
viewportActionCornersService.addComponents([
|
||||||
{
|
{
|
||||||
viewportId,
|
viewportId,
|
||||||
id: 'viewportStatusComponent',
|
id: 'viewportStatusComponent',
|
||||||
@ -292,7 +300,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
key="actionArrows"
|
key="actionArrows"
|
||||||
onArrowsClick={onSegmentChange}
|
onArrowsClick={onSegmentChange}
|
||||||
className={
|
className={
|
||||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
|
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
|
||||||
}
|
}
|
||||||
></ViewportActionArrows>
|
></ViewportActionArrows>
|
||||||
),
|
),
|
||||||
@ -332,18 +340,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
|
||||||
PatientID,
|
|
||||||
PatientName,
|
|
||||||
PatientSex,
|
|
||||||
PatientAge,
|
|
||||||
SliceThickness,
|
|
||||||
ManufacturerModelName,
|
|
||||||
StudyDate,
|
|
||||||
SeriesDescription,
|
|
||||||
SpacingBetweenSlices,
|
|
||||||
} = referencedDisplaySetRef.current.metadata;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||||
@ -362,13 +358,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
OHIFCornerstoneSEGViewport.propTypes = {
|
|
||||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
viewportId: PropTypes.string.isRequired,
|
|
||||||
dataSource: PropTypes.object,
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
|
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
|
||||||
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
|
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
|
||||||
|
|
||||||
|
|||||||
@ -46,9 +46,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^1.86.0",
|
"@cornerstonejs/adapters": "^2.1.13",
|
||||||
"@cornerstonejs/core": "^1.86.0",
|
"@cornerstonejs/core": "^2.1.13",
|
||||||
"@cornerstonejs/tools": "^1.86.0",
|
"@cornerstonejs/tools": "^2.1.13",
|
||||||
"classnames": "^2.3.2"
|
"classnames": "^2.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,15 @@ const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'
|
|||||||
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||||
|
|
||||||
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||||
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
|
const {
|
||||||
props;
|
commandsManager,
|
||||||
|
children,
|
||||||
|
dataSource,
|
||||||
|
displaySets,
|
||||||
|
viewportOptions,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
@ -63,6 +70,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
|||||||
trackedMeasurements = tracked?.[0];
|
trackedMeasurements = tracked?.[0];
|
||||||
sendTrackedMeasurementsEvent = tracked?.[1];
|
sendTrackedMeasurementsEvent = tracked?.[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sendTrackedMeasurementsEvent) {
|
if (!sendTrackedMeasurementsEvent) {
|
||||||
// if no panels from measurement-tracking extension is used, this code will run
|
// if no panels from measurement-tracking extension is used, this code will run
|
||||||
trackedMeasurements = null;
|
trackedMeasurements = null;
|
||||||
@ -72,6 +80,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
|||||||
{ servicesManager, extensionManager, appConfig },
|
{ servicesManager, extensionManager, appConfig },
|
||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
|
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
|
||||||
if (displaySets.length) {
|
if (displaySets.length) {
|
||||||
viewportGridService.setDisplaySetsForViewports([
|
viewportGridService.setDisplaySetsForViewports([
|
||||||
@ -84,6 +93,11 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Todo: what is this, not sure what it does regarding the react aspect,
|
||||||
|
* it is updating a local variable? which is not state.
|
||||||
|
*/
|
||||||
|
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||||
/**
|
/**
|
||||||
* Store the tracking identifiers per viewport in order to be able to
|
* Store the tracking identifiers per viewport in order to be able to
|
||||||
* show the SR measurements on the referenced image on the correct viewport,
|
* show the SR measurements on the referenced image on the correct viewport,
|
||||||
@ -297,17 +311,12 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
|||||||
updateSR();
|
updateSR();
|
||||||
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
|
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Todo: what is this, not sure what it does regarding the react aspect,
|
|
||||||
* it is updating a local variable? which is not state.
|
|
||||||
*/
|
|
||||||
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||||
}, [trackedMeasurements]);
|
}, [trackedMeasurements]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
viewportActionCornersService.setComponents([
|
viewportActionCornersService.addComponents([
|
||||||
{
|
{
|
||||||
viewportId,
|
viewportId,
|
||||||
id: 'viewportStatusComponent',
|
id: 'viewportStatusComponent',
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
utilities as csToolsUtils,
|
utilities as csToolsUtils,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
import { Types, MeasurementService } from '@ohif/core';
|
import { Types, MeasurementService } from '@ohif/core';
|
||||||
import { StackViewport } from '@cornerstonejs/core';
|
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
|
||||||
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
||||||
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||||
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
|
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
|
||||||
@ -95,7 +95,7 @@ export default function init({
|
|||||||
const imageIndex = (currentViewport as StackViewport)
|
const imageIndex = (currentViewport as StackViewport)
|
||||||
.getImageIds()
|
.getImageIds()
|
||||||
.indexOf(referencedImageId);
|
.indexOf(referencedImageId);
|
||||||
csToolsUtils.jumpToSlice(currentViewport.element, { imageIndex });
|
csUtils.jumpToSlice(currentViewport.element, { imageIndex });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Unable to jump to image based on measurement coordinate', error);
|
console.warn('Unable to jump to image based on measurement coordinate', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,11 +45,19 @@ export default function hydrateStructuredReport(
|
|||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
) {
|
) {
|
||||||
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
||||||
const disableEditing = appConfig?.disableEditing;
|
|
||||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||||
const { measurementService, displaySetService, customizationService } = servicesManager.services;
|
const { measurementService, displaySetService, customizationService } = servicesManager.services;
|
||||||
|
|
||||||
const codingValues = customizationService.getCustomization('codingValues', {});
|
const codingValues = customizationService.getCustomization('codingValues', {});
|
||||||
|
|
||||||
|
const { disableEditing } = customizationService.getCustomization(
|
||||||
|
'PanelMeasurement.disableEditing',
|
||||||
|
{
|
||||||
|
id: 'default.disableEditing',
|
||||||
|
disableEditing: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||||
|
|
||||||
// TODO -> We should define a strict versioning somewhere.
|
// TODO -> We should define a strict versioning somewhere.
|
||||||
|
|||||||
@ -42,9 +42,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/core": "^1.86.0",
|
"@cornerstonejs/core": "^2.1.13",
|
||||||
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
|
"@cornerstonejs/tools": "^2.1.13",
|
||||||
"@cornerstonejs/tools": "^1.86.0",
|
|
||||||
"classnames": "^2.3.2"
|
"classnames": "^2.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -110,7 +110,12 @@ function _getTimePointsData(volume) {
|
|||||||
return { timePoints, timePointsUnit };
|
return { timePoints, timePointsUnit };
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
|
function _getSegmentationData(
|
||||||
|
segmentation,
|
||||||
|
volumesTimePointsCache,
|
||||||
|
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
|
||||||
|
) {
|
||||||
|
const { displaySetService, segmentationService, viewportGridService } = servicesManager.services;
|
||||||
const displaySets = displaySetService.getActiveDisplaySets();
|
const displaySets = displaySetService.getActiveDisplaySets();
|
||||||
|
|
||||||
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||||
@ -145,8 +150,11 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
|
|||||||
referencedDynamicVolume.imageIds[0]
|
referencedDynamicVolume.imageIds[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
|
||||||
|
const maskVolumeId = segmentationVolume?.volumeId;
|
||||||
|
|
||||||
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
|
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
|
||||||
maskVolumeId: segmentation.id,
|
maskVolumeId,
|
||||||
}) as number[][];
|
}) as number[][];
|
||||||
|
|
||||||
const pixelCount = timeData.length;
|
const pixelCount = timeData.length;
|
||||||
@ -155,27 +163,15 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
|
|||||||
return [];
|
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
|
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
|
||||||
const color = csTools.segmentation.config.color.getColorForSegmentIndex(
|
// const color = csTools.segmentation.config.color.getSegmentIndexColor(
|
||||||
toolGroupId,
|
// segmentationRepresentationUID,
|
||||||
segmentationRepresentationUID,
|
// 1 // segmentIndex
|
||||||
1 // segmentIndex
|
// );
|
||||||
);
|
const viewportId = viewportGridService.getActiveViewportId();
|
||||||
|
const color = segmentationService.getSegmentColor(viewportId, segmentation.segmentationId, 1);
|
||||||
|
|
||||||
const hexColor = cs.utilities.color.rgbToHex(...color);
|
const hexColor = cs.utilities.color.rgbToHex(color[0], color[1], color[2]);
|
||||||
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
|
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
|
||||||
|
|
||||||
if (!timePointsData) {
|
if (!timePointsData) {
|
||||||
@ -219,14 +215,14 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getInstanceFromSegmentations(segmentations, displaySetService) {
|
function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
|
||||||
if (!segmentations.length) {
|
if (!segmentations.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const volumesTimePointsCache = new WeakMap();
|
const volumesTimePointsCache = new WeakMap();
|
||||||
const segmentationsData = segmentations.map(segmentation =>
|
const segmentationsData = segmentations.map(segmentation =>
|
||||||
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
|
_getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager })
|
||||||
);
|
);
|
||||||
|
|
||||||
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
|
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
|
||||||
@ -266,10 +262,11 @@ function _getInstanceFromSegmentations(segmentations, displaySetService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
||||||
const { segmentationService, displaySetService } = servicesManager.services;
|
debugger;
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
const segmentations = segmentationService.getSegmentations();
|
const segmentations = segmentationService.getSegmentations();
|
||||||
const { seriesMetadata, instance } =
|
const { seriesMetadata, instance } =
|
||||||
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
|
_getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {};
|
||||||
|
|
||||||
if (seriesMetadata && instance) {
|
if (seriesMetadata && instance) {
|
||||||
// An event is triggered after adding the instance and the displaySet is created
|
// An event is triggered after adding the instance and the displaySet is created
|
||||||
|
|||||||
@ -84,11 +84,11 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
|||||||
const stdDeviation = Math.sqrt(variance);
|
const stdDeviation = Math.sqrt(variance);
|
||||||
return stdDeviation;
|
return stdDeviation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate through each segmentation to get the timeData and ijkCoords
|
// Iterate through each segmentation to get the timeData and ijkCoords
|
||||||
segmentations.forEach((segmentation, segmentationIndex) => {
|
segmentations.forEach(segmentation => {
|
||||||
|
const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
|
||||||
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
|
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
|
||||||
maskVolumeId: segmentation.id,
|
maskVolumeId: volume.volumeId,
|
||||||
}) as number[][];
|
}) as number[][];
|
||||||
|
|
||||||
if (summaryStats) {
|
if (summaryStats) {
|
||||||
@ -113,17 +113,33 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
|||||||
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
|
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
|
||||||
// for each voxel in the ROI, get the value at the current time point
|
// for each voxel in the ROI, get the value at the current time point
|
||||||
const voxelValues = [];
|
const voxelValues = [];
|
||||||
|
let sum = 0;
|
||||||
|
let minValue = Infinity;
|
||||||
|
let maxValue = -Infinity;
|
||||||
|
let minIndex = 0;
|
||||||
|
let maxIndex = 0;
|
||||||
|
|
||||||
|
// Single pass through the data to collect all needed values
|
||||||
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
|
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
|
||||||
voxelValues.push(timeData[voxelIndex][timeIndex]);
|
const value = timeData[voxelIndex][timeIndex];
|
||||||
|
voxelValues.push(value);
|
||||||
|
sum += value;
|
||||||
|
|
||||||
|
if (value < minValue) {
|
||||||
|
minValue = value;
|
||||||
|
minIndex = voxelIndex;
|
||||||
|
}
|
||||||
|
if (value > maxValue) {
|
||||||
|
maxValue = value;
|
||||||
|
maxIndex = voxelIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mean.push(voxelValues.reduce((acc, value) => acc + value, 0) / numVoxels);
|
mean.push(sum / numVoxels);
|
||||||
const minimum = Math.min(...voxelValues);
|
min.push(minValue);
|
||||||
min.push(minimum);
|
minIJK.push(ijkCoords[minIndex]);
|
||||||
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]);
|
max.push(maxValue);
|
||||||
const maximum = Math.max(...voxelValues);
|
maxIJK.push(ijkCoords[maxIndex]);
|
||||||
max.push(maximum);
|
|
||||||
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
|
|
||||||
std.push(calculateStandardDeviation(voxelValues));
|
std.push(calculateStandardDeviation(voxelValues));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,30 +356,19 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
|||||||
throw new Error('No reference display set found based on the dynamic data');
|
throw new Error('No reference display set found based on the dynamic data');
|
||||||
}
|
}
|
||||||
|
|
||||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
const displaySet = displaySetService.getDisplaySetByUID(
|
||||||
referenceDisplaySet.displaySetInstanceUID,
|
referenceDisplaySet.displaySetInstanceUID
|
||||||
{ label }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add Segmentation to all toolGroupIds in the viewer
|
const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
|
||||||
const toolGroupIds = Array.from(
|
label,
|
||||||
viewports.values(),
|
});
|
||||||
viewport => viewport.viewportOptions.toolGroupId
|
|
||||||
);
|
|
||||||
|
|
||||||
const representationType = LABELMAP;
|
const firstViewport = viewports.values().next().value;
|
||||||
|
|
||||||
for (const toolGroupId of toolGroupIds) {
|
await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
|
||||||
const hydrateSegmentation = true;
|
segmentationId,
|
||||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
});
|
||||||
toolGroupId,
|
|
||||||
segmentationId,
|
|
||||||
hydrateSegmentation,
|
|
||||||
representationType
|
|
||||||
);
|
|
||||||
|
|
||||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return segmentationId;
|
return segmentationId;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -147,8 +147,8 @@ function getFusionViewports() {
|
|||||||
name: DEFAULT_COLORMAP,
|
name: DEFAULT_COLORMAP,
|
||||||
opacity: [
|
opacity: [
|
||||||
{ value: 0, opacity: 0 },
|
{ value: 0, opacity: 0 },
|
||||||
{ value: 0.1, opacity: 0.3 },
|
{ value: 0.1, opacity: 0.8 },
|
||||||
{ value: 1, opacity: 0.3 },
|
{ value: 1, opacity: 0.8 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -191,6 +191,15 @@ function getFusionViewports() {
|
|||||||
syncInvertState: false,
|
syncInvertState: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'hydrateseg',
|
||||||
|
id: 'sameFORId',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
options: {
|
||||||
|
matchingRules: ['sameFOR'],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
displaySets: [
|
displaySets: [
|
||||||
@ -240,6 +249,15 @@ function getFusionViewports() {
|
|||||||
syncInvertState: false,
|
syncInvertState: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'hydrateseg',
|
||||||
|
id: 'sameFORId',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
options: {
|
||||||
|
matchingRules: ['sameFOR'],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
displaySets: [
|
displaySets: [
|
||||||
@ -289,6 +307,15 @@ function getFusionViewports() {
|
|||||||
syncInvertState: false,
|
syncInvertState: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'hydrateseg',
|
||||||
|
id: 'sameFORId',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
options: {
|
||||||
|
matchingRules: ['sameFOR'],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
displaySets: [
|
displaySets: [
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DynamicDataPanel } from './panels';
|
import { DynamicDataPanel } from './panels';
|
||||||
import { Toolbox } from '@ohif/ui-next';
|
import { Toolbox } from '@ohif/ui-next';
|
||||||
|
import { PanelSegmentation } from '@ohif/extension-cornerstone';
|
||||||
import DynamicExport from './panels/DynamicExport';
|
import DynamicExport from './panels/DynamicExport';
|
||||||
|
|
||||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) {
|
||||||
const wrappedDynamicDataPanel = ({}) => {
|
const wrappedDynamicDataPanel = () => {
|
||||||
return (
|
return (
|
||||||
<DynamicDataPanel
|
<DynamicDataPanel
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
@ -14,7 +15,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrappedDynamicToolbox = ({}) => {
|
const wrappedDynamicSegmentation = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbox
|
<Toolbox
|
||||||
@ -24,18 +25,17 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
|||||||
buttonSectionId="dynamic-toolbox"
|
buttonSectionId="dynamic-toolbox"
|
||||||
title="Threshold Tools"
|
title="Threshold Tools"
|
||||||
/>
|
/>
|
||||||
</>
|
<PanelSegmentation
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrappedDynamicExport = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DynamicExport
|
|
||||||
commandsManager={commandsManager}
|
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
|
commandsManager={commandsManager}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
/>
|
configuration={configuration}
|
||||||
|
>
|
||||||
|
<DynamicExport
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
/>
|
||||||
|
</PanelSegmentation>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -49,18 +49,11 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
|||||||
component: wrappedDynamicDataPanel,
|
component: wrappedDynamicDataPanel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dynamic-toolbox',
|
name: 'dynamic-segmentation',
|
||||||
iconName: 'tab-4d',
|
iconName: 'tab-segmentation',
|
||||||
iconLabel: '4D Workflow',
|
iconLabel: 'Segmentation',
|
||||||
label: 'Dynamic Toolbox',
|
label: 'Segmentation',
|
||||||
component: wrappedDynamicToolbox,
|
component: wrappedDynamicSegmentation,
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'dynamic-export',
|
|
||||||
iconName: 'tab-4d',
|
|
||||||
iconLabel: '4D Workflow',
|
|
||||||
label: '4D Workflow',
|
|
||||||
component: wrappedDynamicExport,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PanelGenerateImage from './PanelGenerateImage';
|
import PanelGenerateImage from './PanelGenerateImage';
|
||||||
import { Separator } from '@ohif/ui-next';
|
|
||||||
|
|
||||||
function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) {
|
function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,73 +1,52 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { ActionButtons } from '@ohif/ui';
|
import { Button, Icons } from '@ohif/ui-next';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useSegmentations } from '@ohif/extension-cornerstone';
|
||||||
|
|
||||||
function DynamicExport({ commandsManager, servicesManager, extensionManager }: withAppTypes) {
|
function DynamicExport({ commandsManager, servicesManager }: withAppTypes) {
|
||||||
const { segmentationService } = servicesManager.services;
|
const segmentations = useSegmentations({ servicesManager });
|
||||||
const { t } = useTranslation('dynamicExport');
|
|
||||||
|
|
||||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
if (!segmentations?.length) {
|
||||||
|
return null;
|
||||||
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 (
|
return (
|
||||||
<div>
|
<div className="flex gap-2">
|
||||||
<div className="mt-3 flex justify-center px-2">
|
<div className="flex h-8 w-full items-center rounded pr-0.5">
|
||||||
<ActionButtons
|
<Button
|
||||||
actions={actions}
|
size="sm"
|
||||||
t={t}
|
variant="ghost"
|
||||||
/>
|
className="pl-1.5"
|
||||||
|
onClick={() => {
|
||||||
|
commandsManager.runCommand('exportTimeReportCSV', {
|
||||||
|
segmentations,
|
||||||
|
options: {
|
||||||
|
filename: 'TimeData.csv',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icons.Export />
|
||||||
|
<span className="pl-1">Time Data</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-8 w-full items-center rounded pr-0.5">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="pl-1.5"
|
||||||
|
onClick={() => {
|
||||||
|
commandsManager.runCommand('exportTimeReportCSV', {
|
||||||
|
segmentations,
|
||||||
|
summaryStats: true,
|
||||||
|
options: {
|
||||||
|
filename: 'ROIStats.csv',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icons.Export />
|
||||||
|
<span className="pl-1">ROI Stats</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
InputDoubleRange,
|
|
||||||
Button,
|
Button,
|
||||||
PanelSection,
|
PanelSection,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
@ -10,6 +9,8 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
|
|
||||||
|
import { DoubleSlider } from '@ohif/ui-next';
|
||||||
|
|
||||||
import { Enums } from '@cornerstonejs/core';
|
import { Enums } from '@cornerstonejs/core';
|
||||||
|
|
||||||
const controlClassNames = {
|
const controlClassNames = {
|
||||||
@ -55,21 +56,15 @@ const DynamicVolumeControls = ({
|
|||||||
|
|
||||||
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
||||||
|
|
||||||
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
|
const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSliderRangeValues([framesLength / 4, framesLength / 2]);
|
|
||||||
}, [framesLength]);
|
|
||||||
|
|
||||||
const handleSliderChange = newValues => {
|
const handleSliderChange = newValues => {
|
||||||
onDoubleRangeChange(newValues);
|
onDoubleRangeChange(newValues);
|
||||||
|
|
||||||
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSliderRangeValues(newValues);
|
setSliderRangeValues(newValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatLabel = value => Math.round(value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex select-none flex-col">
|
<div className="flex select-none flex-col">
|
||||||
<PanelSection
|
<PanelSection
|
||||||
@ -134,7 +129,7 @@ const DynamicVolumeControls = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
className={`mt-2 w-full `}
|
className={`mt-2 w-full`}
|
||||||
separated={true}
|
separated={true}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -156,15 +151,15 @@ const DynamicVolumeControls = ({
|
|||||||
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
|
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
|
||||||
</button>
|
</button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<div className="w-ful mt-2">
|
<div className="mt-2 w-full">
|
||||||
<InputDoubleRange
|
<DoubleSlider
|
||||||
values={sliderRangeValues}
|
min={0}
|
||||||
onChange={handleSliderChange}
|
max={framesLength - 1}
|
||||||
minValue={0}
|
|
||||||
showLabel={true}
|
|
||||||
allowNumberEdit={true}
|
|
||||||
maxValue={framesLength}
|
|
||||||
step={1}
|
step={1}
|
||||||
|
defaultValue={sliderRangeValues}
|
||||||
|
onValueChange={handleSliderChange}
|
||||||
|
formatLabel={formatLabel}
|
||||||
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@ -216,14 +211,14 @@ function FrameControls({
|
|||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={getPlayPauseIconName()}
|
name={getPlayPauseIconName()}
|
||||||
className=" active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
className="active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
value={currentFrameIndex}
|
value={currentFrameIndex}
|
||||||
onChange={onFrameChange}
|
onChange={onFrameChange}
|
||||||
minValue={0}
|
minValue={0}
|
||||||
maxValue={framesLength}
|
maxValue={framesLength - 1}
|
||||||
label="Frame"
|
label="Frame"
|
||||||
{...controlClassNames}
|
{...controlClassNames}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useCine, useViewportGrid } from '@ohif/ui';
|
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||||
import { cache, utilities as csUtils, volumeLoader, eventTarget } from '@cornerstonejs/core';
|
import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
|
||||||
import { Enums } from '@cornerstonejs/streaming-image-volume-loader';
|
|
||||||
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||||
import DynamicVolumeControls from './DynamicVolumeControls';
|
import DynamicVolumeControls from './DynamicVolumeControls';
|
||||||
|
|
||||||
@ -16,8 +14,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
|||||||
const [{ activeViewportId }] = useViewportGrid();
|
const [{ activeViewportId }] = useViewportGrid();
|
||||||
|
|
||||||
//
|
//
|
||||||
const [timePointsRange, setTimePointsRange] = useState([]);
|
const [timePointsRange, setTimePointsRange] = useState([0, 0]);
|
||||||
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
|
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([0, 0]);
|
||||||
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
|
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
|
||||||
const [dynamicVolume, setDynamicVolume] = useState(null);
|
const [dynamicVolume, setDynamicVolume] = useState(null);
|
||||||
const [frameRate, setFrameRate] = useState(20);
|
const [frameRate, setFrameRate] = useState(20);
|
||||||
@ -31,47 +29,62 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
|||||||
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
|
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const evt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
const viewportDataChangedEvt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||||
|
const cineStateChangedEvt = servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED;
|
||||||
|
|
||||||
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, evtDetails => {
|
const viewportDataChangedCallback = evtDetails => {
|
||||||
evtDetails.viewportData.data.forEach(volumeData => {
|
evtDetails.viewportData.data.forEach(volumeData => {
|
||||||
if (volumeData.volume.isDynamicVolume()) {
|
if (volumeData.volume?.isDynamicVolume()) {
|
||||||
setDynamicVolume(volumeData.volume);
|
setDynamicVolume(volumeData.volume);
|
||||||
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
|
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
|
||||||
setTimePointsRange([1, volumeData.volume.numTimePoints]);
|
const newRange = [1, volumeData.volume.numTimePoints];
|
||||||
|
setTimePointsRange(newRange);
|
||||||
|
setTimePointsRangeToUseForGenerate(newRange);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const cineStateChangedCallback = evt => {
|
||||||
|
setIsPlaying(evt.isPlaying);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { unsubscribe: unsubscribeViewportData } = cornerstoneViewportService.subscribe(
|
||||||
|
viewportDataChangedEvt,
|
||||||
|
viewportDataChangedCallback
|
||||||
|
);
|
||||||
|
const { unsubscribe: unsubscribeCineState } = servicesManager.services.cineService.subscribe(
|
||||||
|
cineStateChangedEvt,
|
||||||
|
cineStateChangedCallback
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribeViewportData();
|
||||||
|
unsubscribeCineState();
|
||||||
|
};
|
||||||
|
}, [cornerstoneViewportService, cineService, servicesManager.services.cineService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
||||||
|
|
||||||
|
const callback = evt => {
|
||||||
|
setTimePointRendered(evt.detail.timePointIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventTarget.addEventListener(evt, callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventTarget.removeEventListener(evt, callback);
|
||||||
};
|
};
|
||||||
}, [cornerstoneViewportService]);
|
}, [cornerstoneViewportService]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { unsubscribe } = servicesManager.services.cineService.subscribe(
|
|
||||||
servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED,
|
|
||||||
evt => {
|
|
||||||
setIsPlaying(evt.isPlaying);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, [cineService]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
||||||
|
|
||||||
if (!displaySetUIDs || displaySetUIDs.length === 0) {
|
if (!displaySetUIDs?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displaySets = displaySetUIDs.map(displaySetUID =>
|
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
|
||||||
displaySetService.getDisplaySetByUID(displaySetUID)
|
|
||||||
);
|
|
||||||
|
|
||||||
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
|
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
|
||||||
|
|
||||||
if (!dynamicVolumeDisplaySet) {
|
if (!dynamicVolumeDisplaySet) {
|
||||||
@ -88,23 +101,16 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
|||||||
|
|
||||||
setDynamicVolume(dynamicVolume);
|
setDynamicVolume(dynamicVolume);
|
||||||
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
|
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
|
||||||
setTimePointsRange([1, dynamicVolume.numTimePoints]);
|
const newRange = [1, dynamicVolume.numTimePoints];
|
||||||
}, [activeViewportId, cornerstoneViewportService]);
|
setTimePointsRange(newRange);
|
||||||
|
setTimePointsRangeToUseForGenerate(newRange);
|
||||||
useEffect(() => {
|
}, [
|
||||||
// ~~ Subscription
|
activeViewportId,
|
||||||
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
viewportGridService,
|
||||||
|
displaySetService,
|
||||||
const callback = evt => {
|
cornerstoneViewportService,
|
||||||
setTimePointRendered(evt.detail.timePointIndex);
|
cineService,
|
||||||
};
|
]);
|
||||||
|
|
||||||
eventTarget.addEventListener(evt, callback);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
eventTarget.removeEventListener(evt, callback);
|
|
||||||
};
|
|
||||||
}, [cornerstoneViewportService]);
|
|
||||||
|
|
||||||
function renderGeneratedImage(displaySet) {
|
function renderGeneratedImage(displaySet) {
|
||||||
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
|
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
|
||||||
@ -130,26 +136,19 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
|||||||
let computedVolume = cache.getVolume(computedVolumeId);
|
let computedVolume = cache.getVolume(computedVolumeId);
|
||||||
|
|
||||||
if (!computedVolume) {
|
if (!computedVolume) {
|
||||||
await createComputedVolume(dynamicVolumeId, computedVolumeId);
|
computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
||||||
computedVolume = cache.getVolume(computedVolumeId);
|
volumeId: computedVolumeId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
const [start, end] = timePointsRangeToUseForGenerate;
|
||||||
|
const frameNumbers = Array.from({ length: end - start + 1 }, (_, i) => i + start - 1);
|
||||||
|
|
||||||
const vals = timePointsRangeToUseForGenerate;
|
const options = {
|
||||||
|
frameNumbers: operationName === 'SUBTRACT' ? [start, end - 1] : frameNumbers,
|
||||||
|
targetVolume: computedVolume,
|
||||||
|
};
|
||||||
|
|
||||||
const targets = Array.from({ length: vals[1] - vals[0] + 1 }, (_, i) => i + vals[0]);
|
cstUtils.dynamicVolume.updateVolumeFromTimeData(dynamicVolume, operationName, options);
|
||||||
|
|
||||||
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
|
// 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
|
// the displaySet. If it does exist, update the image data and vtkTexture
|
||||||
@ -166,6 +165,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
|||||||
madeInClient: true,
|
madeInClient: true,
|
||||||
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
|
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
|
||||||
isDerived: true,
|
isDerived: true,
|
||||||
|
imageIds: computedVolume.imageIds,
|
||||||
};
|
};
|
||||||
setComputedDisplaySet(displaySet);
|
setComputedDisplaySet(displaySet);
|
||||||
renderGeneratedImage(displaySet);
|
renderGeneratedImage(displaySet);
|
||||||
@ -173,19 +173,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
|||||||
commandsManager.runCommand('updateVolumeData', {
|
commandsManager.runCommand('updateVolumeData', {
|
||||||
volume: computedVolume,
|
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();
|
cornerstoneViewportService.getRenderingEngine().render();
|
||||||
renderGeneratedImage(computedDisplaySet);
|
renderGeneratedImage(computedDisplaySet);
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,28 +237,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
|||||||
onGenerate={onGenerateImage}
|
onGenerate={onGenerateImage}
|
||||||
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
|
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
|
||||||
onDoubleRangeChange={handleSliderChange}
|
onDoubleRangeChange={handleSliderChange}
|
||||||
|
initialRangeValues={timePointsRangeToUseForGenerate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createComputedVolume(dynamicVolumeId, computedVolumeId) {
|
|
||||||
if (!cache.getVolume(computedVolumeId)) {
|
|
||||||
const computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
|
||||||
volumeId: computedVolumeId,
|
|
||||||
});
|
|
||||||
return computedVolume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelGenerateImage.propTypes = {
|
|
||||||
servicesManager: PropTypes.shape({
|
|
||||||
services: PropTypes.shape({
|
|
||||||
measurementService: PropTypes.shape({
|
|
||||||
getMeasurements: PropTypes.func.isRequired,
|
|
||||||
subscribe: PropTypes.func.isRequired,
|
|
||||||
EVENTS: PropTypes.object.isRequired,
|
|
||||||
VALUE_TYPES: PropTypes.object.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||||
"@cornerstonejs/dicom-image-loader": "^1.86.0",
|
"@cornerstonejs/dicom-image-loader": "^2.1.13",
|
||||||
"@icr/polyseg-wasm": "^0.4.0",
|
"@icr/polyseg-wasm": "^0.4.0",
|
||||||
"@ohif/core": "3.9.0-beta.105",
|
"@ohif/core": "3.9.0-beta.105",
|
||||||
"@ohif/ui": "3.9.0-beta.105",
|
"@ohif/ui": "3.9.0-beta.105",
|
||||||
@ -55,12 +55,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^1.86.0",
|
"@cornerstonejs/adapters": "^2.1.13",
|
||||||
"@cornerstonejs/core": "^1.86.0",
|
"@cornerstonejs/core": "^2.1.13",
|
||||||
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
|
"@cornerstonejs/tools": "^2.1.13",
|
||||||
"@cornerstonejs/tools": "^1.86.0",
|
|
||||||
"@icr/polyseg-wasm": "^0.4.0",
|
"@icr/polyseg-wasm": "^0.4.0",
|
||||||
"@kitware/vtk.js": "30.4.1",
|
"@kitware/vtk.js": "32.1.0",
|
||||||
"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",
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import { useResizeDetector } from 'react-resize-detector';
|
import { useResizeDetector } from 'react-resize-detector';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as cs3DTools from '@cornerstonejs/tools';
|
import * as cs3DTools from '@cornerstonejs/tools';
|
||||||
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
||||||
import { MeasurementService } from '@ohif/core';
|
import { MeasurementService } from '@ohif/core';
|
||||||
import { Notification, useViewportDialog, AllInOneMenu } from '@ohif/ui';
|
import { AllInOneMenu, Notification, useViewportDialog } from '@ohif/ui';
|
||||||
import type { Types as csTypes } from '@cornerstonejs/core';
|
import type { Types as csTypes } from '@cornerstonejs/core';
|
||||||
|
|
||||||
import { setEnabledElement } from '../state';
|
import { setEnabledElement } from '../state';
|
||||||
@ -17,8 +16,10 @@ import type { Types } from '@ohif/core';
|
|||||||
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
|
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
|
||||||
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
|
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
|
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
|
||||||
import { LutPresentation, PositionPresentation } from '../types/Presentation';
|
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
|
||||||
|
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
|
||||||
|
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
|
||||||
|
|
||||||
const STACK = 'stack';
|
const STACK = 'stack';
|
||||||
|
|
||||||
@ -28,428 +29,401 @@ const STACK = 'stack';
|
|||||||
*/
|
*/
|
||||||
let cacheJumpToMeasurementEvent;
|
let cacheJumpToMeasurementEvent;
|
||||||
|
|
||||||
function areEqual(prevProps, nextProps) {
|
|
||||||
if (nextProps.needsRerendering) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.viewportOptions.needsRerendering) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevDisplaySets = prevProps.displaySets;
|
|
||||||
const nextDisplaySets = nextProps.displaySets;
|
|
||||||
|
|
||||||
if (prevDisplaySets.length !== nextDisplaySets.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < prevDisplaySets.length; i++) {
|
|
||||||
const prevDisplaySet = prevDisplaySets[i];
|
|
||||||
|
|
||||||
const foundDisplaySet = nextDisplaySets.find(
|
|
||||||
nextDisplaySet =>
|
|
||||||
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!foundDisplaySet) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check they contain the same image
|
|
||||||
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if their imageIds are the same
|
|
||||||
if (foundDisplaySet.images?.length) {
|
|
||||||
for (let j = 0; j < foundDisplaySet.images.length; j++) {
|
|
||||||
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
|
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
|
||||||
// Then we don't need to worry about the re-renders if the props change.
|
// Then we don't need to worry about the re-renders if the props change.
|
||||||
const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
|
const OHIFCornerstoneViewport = React.memo(
|
||||||
const {
|
(
|
||||||
displaySets,
|
props: withAppTypes<{
|
||||||
dataSource,
|
viewportId: string;
|
||||||
viewportOptions,
|
displaySets: AppTypes.DisplaySet[];
|
||||||
displaySetOptions,
|
viewportOptions: AppTypes.ViewportGrid.GridViewportOptions;
|
||||||
servicesManager,
|
initialImageIndex: number;
|
||||||
commandsManager,
|
}>
|
||||||
onElementEnabled,
|
) => {
|
||||||
// eslint-disable-next-line react/prop-types
|
const {
|
||||||
onElementDisabled,
|
displaySets,
|
||||||
isJumpToMeasurementDisabled = false,
|
dataSource,
|
||||||
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
viewportOptions,
|
||||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
displaySetOptions,
|
||||||
// to set the initial state of the viewport's first image to render
|
servicesManager,
|
||||||
// eslint-disable-next-line react/prop-types
|
commandsManager,
|
||||||
initialImageIndex,
|
onElementEnabled,
|
||||||
// if the viewport is part of a hanging protocol layout
|
// eslint-disable-next-line react/prop-types
|
||||||
// we should not really rely on the old synchronizers and
|
onElementDisabled,
|
||||||
// you see below we only rehydrate the synchronizers if the viewport
|
isJumpToMeasurementDisabled = false,
|
||||||
// is not part of the hanging protocol layout. HPs should
|
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||||
// define their own synchronizers. Since the synchronizers are
|
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||||
// viewportId dependent and
|
// to set the initial state of the viewport's first image to render
|
||||||
// eslint-disable-next-line react/prop-types
|
// eslint-disable-next-line react/prop-types
|
||||||
isHangingProtocolLayout,
|
initialImageIndex,
|
||||||
} = props;
|
// 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
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
isHangingProtocolLayout,
|
||||||
|
} = props;
|
||||||
|
const viewportId = viewportOptions.viewportId;
|
||||||
|
|
||||||
const viewportId = viewportOptions.viewportId;
|
if (!viewportId) {
|
||||||
|
throw new Error('Viewport ID is required');
|
||||||
if (!viewportId) {
|
|
||||||
throw new Error('Viewport ID is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure displaySetOptions has one object per displaySet
|
|
||||||
while (displaySetOptions.length < displaySets.length) {
|
|
||||||
displaySetOptions.push({});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we only have support for dynamic data in volume viewports, we should
|
|
||||||
// handle this case here and set the viewportType to volume if any of the
|
|
||||||
// displaySets are dynamic volumes
|
|
||||||
viewportOptions.viewportType = displaySets.some(ds => ds.isDynamicVolume && ds.isReconstructable)
|
|
||||||
? 'volume'
|
|
||||||
: viewportOptions.viewportType;
|
|
||||||
|
|
||||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
|
||||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
|
||||||
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
|
|
||||||
const [appConfig] = useAppConfig();
|
|
||||||
|
|
||||||
const {
|
|
||||||
displaySetService,
|
|
||||||
toolbarService,
|
|
||||||
toolGroupService,
|
|
||||||
syncGroupService,
|
|
||||||
cornerstoneViewportService,
|
|
||||||
cornerstoneCacheService,
|
|
||||||
stateSyncService,
|
|
||||||
viewportActionCornersService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
|
|
||||||
const [viewportDialogState] = useViewportDialog();
|
|
||||||
// useCallback for scroll bar height calculation
|
|
||||||
const setImageScrollBarHeight = useCallback(() => {
|
|
||||||
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
|
|
||||||
setScrollbarHeight(scrollbarHeight);
|
|
||||||
}, [elementRef]);
|
|
||||||
|
|
||||||
// useCallback for onResize
|
|
||||||
const onResize = useCallback(() => {
|
|
||||||
if (elementRef.current) {
|
|
||||||
cornerstoneViewportService.resize();
|
|
||||||
setImageScrollBarHeight();
|
|
||||||
}
|
}
|
||||||
}, [elementRef]);
|
|
||||||
|
|
||||||
const cleanUpServices = useCallback(
|
// Make sure displaySetOptions has one object per displaySet
|
||||||
viewportInfo => {
|
while (displaySetOptions.length < displaySets.length) {
|
||||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
displaySetOptions.push({});
|
||||||
const syncGroups = viewportInfo.getSyncGroups();
|
}
|
||||||
|
|
||||||
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
// 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;
|
||||||
|
|
||||||
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||||
|
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||||
|
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
|
||||||
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
viewportActionCornersService.clear(viewportId);
|
const {
|
||||||
},
|
displaySetService,
|
||||||
[viewportId]
|
toolbarService,
|
||||||
);
|
toolGroupService,
|
||||||
|
syncGroupService,
|
||||||
|
cornerstoneViewportService,
|
||||||
|
segmentationService,
|
||||||
|
cornerstoneCacheService,
|
||||||
|
viewportActionCornersService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
const elementEnabledHandler = useCallback(
|
const [viewportDialogState] = useViewportDialog();
|
||||||
evt => {
|
// useCallback for scroll bar height calculation
|
||||||
// check this is this element reference and return early if doesn't match
|
const setImageScrollBarHeight = useCallback(() => {
|
||||||
if (evt.detail.element !== elementRef.current) {
|
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
|
||||||
return;
|
setScrollbarHeight(scrollbarHeight);
|
||||||
|
}, [elementRef]);
|
||||||
|
|
||||||
|
// useCallback for onResize
|
||||||
|
const onResize = useCallback(() => {
|
||||||
|
if (elementRef.current) {
|
||||||
|
cornerstoneViewportService.resize();
|
||||||
|
setImageScrollBarHeight();
|
||||||
}
|
}
|
||||||
|
}, [elementRef]);
|
||||||
|
|
||||||
const { viewportId, element } = evt.detail;
|
const cleanUpServices = useCallback(
|
||||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
viewportInfo => {
|
||||||
setEnabledElement(viewportId, element);
|
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||||
setEnabledVPElement(element);
|
const syncGroups = viewportInfo.getSyncGroups();
|
||||||
|
|
||||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
||||||
const toolGroupId = viewportInfo.getToolGroupId();
|
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
||||||
const syncGroups = viewportInfo.getSyncGroups();
|
|
||||||
|
|
||||||
toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
|
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||||
|
|
||||||
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
|
viewportActionCornersService.clear(viewportId);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
viewportId,
|
||||||
|
segmentationService,
|
||||||
|
syncGroupService,
|
||||||
|
toolGroupService,
|
||||||
|
viewportActionCornersService,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const synchronizersStore = stateSyncService.getState().synchronizersStore;
|
const elementEnabledHandler = useCallback(
|
||||||
|
evt => {
|
||||||
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
|
// check this is this element reference and return early if doesn't match
|
||||||
// If the viewport used to have a synchronizer, re apply it again
|
if (evt.detail.element !== elementRef.current) {
|
||||||
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onElementEnabled) {
|
|
||||||
onElementEnabled(evt);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[viewportId, onElementEnabled, toolGroupService]
|
|
||||||
);
|
|
||||||
|
|
||||||
// disable the element upon unmounting
|
|
||||||
useEffect(() => {
|
|
||||||
cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
|
|
||||||
|
|
||||||
eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
|
||||||
|
|
||||||
setImageScrollBarHeight();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
|
||||||
|
|
||||||
if (!viewportInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cornerstoneViewportService.storePresentation({ viewportId });
|
|
||||||
|
|
||||||
// This should be done after the store presentation since synchronizers
|
|
||||||
// will get cleaned up and they need the viewportInfo to be present
|
|
||||||
cleanUpServices(viewportInfo);
|
|
||||||
|
|
||||||
if (onElementDisabled) {
|
|
||||||
onElementDisabled(viewportInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
cornerstoneViewportService.disableElement(viewportId);
|
|
||||||
|
|
||||||
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// subscribe to displaySet metadata invalidation (updates)
|
|
||||||
// Currently, if the metadata changes we need to re-render the display set
|
|
||||||
// for it to take effect in the viewport. As we deal with scaling in the loading,
|
|
||||||
// we need to remove the old volume from the cache, and let the
|
|
||||||
// viewport to re-add it which will use the new metadata. Otherwise, the
|
|
||||||
// viewport will use the cached volume and the new metadata will not be used.
|
|
||||||
// Note: this approach does not actually end of sending network requests
|
|
||||||
// and it uses the network cache
|
|
||||||
useEffect(() => {
|
|
||||||
const { unsubscribe } = displaySetService.subscribe(
|
|
||||||
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
|
|
||||||
async ({
|
|
||||||
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
|
|
||||||
invalidateData,
|
|
||||||
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
|
|
||||||
if (!invalidateData) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { viewportId, element } = evt.detail;
|
||||||
|
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||||
|
setEnabledElement(viewportId, element);
|
||||||
|
setEnabledVPElement(element);
|
||||||
|
|
||||||
|
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||||
|
const toolGroupId = viewportInfo.getToolGroupId();
|
||||||
|
const syncGroups = viewportInfo.getSyncGroups();
|
||||||
|
|
||||||
|
toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
|
||||||
|
|
||||||
|
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
|
||||||
|
|
||||||
|
// we don't need reactivity here so just use state
|
||||||
|
const { synchronizersStore } = useSynchronizersStore.getState();
|
||||||
|
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
|
||||||
|
// If the viewport used to have a synchronizer, re apply it again
|
||||||
|
_rehydrateSynchronizers(viewportId, syncGroupService);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onElementEnabled && typeof onElementEnabled === 'function') {
|
||||||
|
onElementEnabled(evt);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[viewportId, onElementEnabled, toolGroupService]
|
||||||
|
);
|
||||||
|
|
||||||
|
// disable the element upon unmounting
|
||||||
|
useEffect(() => {
|
||||||
|
cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
|
||||||
|
|
||||||
|
eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
||||||
|
|
||||||
|
setImageScrollBarHeight();
|
||||||
|
|
||||||
|
return () => {
|
||||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||||
|
|
||||||
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
|
if (!viewportInfo) {
|
||||||
const viewportData = viewportInfo.getViewportData();
|
return;
|
||||||
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
|
|
||||||
viewportData,
|
|
||||||
invalidatedDisplaySetInstanceUID,
|
|
||||||
dataSource,
|
|
||||||
displaySetService
|
|
||||||
);
|
|
||||||
|
|
||||||
const keepCamera = true;
|
|
||||||
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, [viewportId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
cornerstoneViewportService.storePresentation({ viewportId });
|
||||||
// handle the default viewportType to be stack
|
|
||||||
if (!viewportOptions.viewportType) {
|
|
||||||
viewportOptions.viewportType = STACK;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadViewportData = async () => {
|
// This should be done after the store presentation since synchronizers
|
||||||
const viewportData = await cornerstoneCacheService.createViewportData(
|
// will get cleaned up and they need the viewportInfo to be present
|
||||||
displaySets,
|
cleanUpServices(viewportInfo);
|
||||||
viewportOptions,
|
|
||||||
dataSource,
|
|
||||||
initialImageIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
// The presentation state will have been stored previously by closing
|
if (onElementDisabled && typeof onElementDisabled === 'function') {
|
||||||
// a viewport. Otherwise, this viewport will be unchanged and the
|
onElementDisabled(viewportInfo);
|
||||||
// presentation information will be directly carried over.
|
}
|
||||||
const state = stateSyncService.getState();
|
|
||||||
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
|
|
||||||
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
|
|
||||||
|
|
||||||
const { presentationIds } = viewportOptions;
|
cornerstoneViewportService.disableElement(viewportId);
|
||||||
const presentations = {
|
|
||||||
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
||||||
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
|
|
||||||
};
|
};
|
||||||
let measurement;
|
}, []);
|
||||||
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
|
|
||||||
measurement = cacheJumpToMeasurementEvent.measurement;
|
|
||||||
// Delete the position presentation so that viewport navigates direct
|
|
||||||
presentations.positionPresentation = null;
|
|
||||||
cacheJumpToMeasurementEvent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
|
// subscribe to displaySet metadata invalidation (updates)
|
||||||
// Used for segmentation hydration right now, since the logic to decide whether
|
// Currently, if the metadata changes we need to re-render the display set
|
||||||
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
|
// for it to take effect in the viewport. As we deal with scaling in the loading,
|
||||||
// so we need to re-render (force update via change of the needsRerendering) so that React
|
// we need to remove the old volume from the cache, and let the
|
||||||
// does the diffing and decides we should render this again (although the id and element has not changed)
|
// viewport to re-add it which will use the new metadata. Otherwise, the
|
||||||
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
|
// viewport will use the cached volume and the new metadata will not be used.
|
||||||
if (viewportOptions.needsRerendering) {
|
// Note: this approach does not actually end of sending network requests
|
||||||
viewportOptions.needsRerendering = false;
|
// and it uses the network cache
|
||||||
}
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = displaySetService.subscribe(
|
||||||
|
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
|
||||||
|
async ({
|
||||||
|
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
|
||||||
|
invalidateData,
|
||||||
|
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
|
||||||
|
if (!invalidateData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cornerstoneViewportService.setViewportData(
|
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||||
viewportId,
|
|
||||||
viewportData,
|
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
|
||||||
viewportOptions,
|
const viewportData = viewportInfo.getViewportData();
|
||||||
displaySetOptions,
|
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
|
||||||
presentations
|
viewportData,
|
||||||
|
invalidatedDisplaySetInstanceUID,
|
||||||
|
dataSource,
|
||||||
|
displaySetService
|
||||||
|
);
|
||||||
|
|
||||||
|
const keepCamera = true;
|
||||||
|
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (measurement) {
|
return () => {
|
||||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [viewportId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// handle the default viewportType to be stack
|
||||||
|
if (!viewportOptions.viewportType) {
|
||||||
|
viewportOptions.viewportType = STACK;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
loadViewportData();
|
const loadViewportData = async () => {
|
||||||
}, [viewportOptions, displaySets, dataSource]);
|
const viewportData = await cornerstoneCacheService.createViewportData(
|
||||||
|
displaySets,
|
||||||
|
viewportOptions,
|
||||||
|
dataSource,
|
||||||
|
initialImageIndex
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
const presentations = getViewportPresentations(viewportId, viewportOptions);
|
||||||
* There are two scenarios for jump to click
|
|
||||||
* 1. Current viewports contain the displaySet that the annotation was drawn on
|
|
||||||
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
|
|
||||||
* and we need to change the viewports displaySet for jumping.
|
|
||||||
* Since measurement_jump happens via events and listeners, the former case is handled
|
|
||||||
* by the measurement_jump direct callback, but the latter case is handled first by
|
|
||||||
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
|
|
||||||
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (isJumpToMeasurementDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
|
let measurement;
|
||||||
elementRef,
|
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
|
||||||
viewportId,
|
measurement = cacheJumpToMeasurementEvent.measurement;
|
||||||
servicesManager
|
// Delete the position presentation so that viewport navigates direct
|
||||||
);
|
presentations.positionPresentation = null;
|
||||||
|
cacheJumpToMeasurementEvent = null;
|
||||||
|
}
|
||||||
|
|
||||||
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
|
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
|
||||||
|
// Used for segmentation hydration right now, since the logic to decide whether
|
||||||
|
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
|
||||||
|
// so we need to re-render (force update via change of the needsRerendering) so that React
|
||||||
|
// does the diffing and decides we should render this again (although the id and element has not changed)
|
||||||
|
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
|
||||||
|
if (viewportOptions.needsRerendering) {
|
||||||
|
viewportOptions.needsRerendering = false;
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
cornerstoneViewportService.setViewportData(
|
||||||
unsubscribeFromJumpToMeasurementEvents();
|
viewportId,
|
||||||
};
|
viewportData,
|
||||||
}, [displaySets, elementRef, viewportId]);
|
viewportOptions,
|
||||||
|
displaySetOptions,
|
||||||
|
presentations
|
||||||
|
);
|
||||||
|
|
||||||
// Set up the window level action menu in the viewport action corners.
|
if (measurement) {
|
||||||
useEffect(() => {
|
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||||
// 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
|
loadViewportData();
|
||||||
// to determine if and in which corner various action components should go.
|
}, [viewportOptions, displaySets, dataSource]);
|
||||||
const wlActionMenu = getWindowLevelActionMenu({
|
|
||||||
viewportId,
|
/**
|
||||||
element: elementRef.current,
|
* There are two scenarios for jump to click
|
||||||
|
* 1. Current viewports contain the displaySet that the annotation was drawn on
|
||||||
|
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
|
||||||
|
* and we need to change the viewports displaySet for jumping.
|
||||||
|
* Since measurement_jump happens via events and listeners, the former case is handled
|
||||||
|
* by the measurement_jump direct callback, but the latter case is handled first by
|
||||||
|
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
|
||||||
|
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (isJumpToMeasurementDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
|
||||||
|
elementRef,
|
||||||
|
viewportId,
|
||||||
|
servicesManager
|
||||||
|
);
|
||||||
|
|
||||||
|
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribeFromJumpToMeasurementEvents();
|
||||||
|
};
|
||||||
|
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const location = viewportActionCornersService.LOCATIONS.topRight;
|
||||||
|
|
||||||
|
// TODO: In the future we should consider using the customization service
|
||||||
|
// to determine if and in which corner various action components should go.
|
||||||
|
viewportActionCornersService.addComponent({
|
||||||
|
viewportId,
|
||||||
|
id: 'windowLevelActionMenu',
|
||||||
|
component: getWindowLevelActionMenu({
|
||||||
|
viewportId,
|
||||||
|
element: elementRef.current,
|
||||||
|
displaySets,
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
location,
|
||||||
|
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
|
||||||
|
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
|
||||||
|
}),
|
||||||
|
location,
|
||||||
|
});
|
||||||
|
|
||||||
|
viewportActionCornersService.addComponent({
|
||||||
|
viewportId,
|
||||||
|
id: 'segmentation',
|
||||||
|
component: getViewportDataOverlaySettingsMenu({
|
||||||
|
viewportId,
|
||||||
|
element: elementRef.current,
|
||||||
|
displaySets,
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
location,
|
||||||
|
}),
|
||||||
|
location,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
displaySets,
|
displaySets,
|
||||||
|
viewportId,
|
||||||
|
viewportActionCornersService,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
|
appConfig,
|
||||||
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
|
]);
|
||||||
|
|
||||||
|
const { ref: resizeRef } = useResizeDetector({
|
||||||
|
onResize,
|
||||||
});
|
});
|
||||||
|
|
||||||
viewportActionCornersService.setComponent({
|
return (
|
||||||
viewportId,
|
<React.Fragment>
|
||||||
id: 'windowLevelActionMenu',
|
<div className="viewport-wrapper">
|
||||||
component: wlActionMenu,
|
<div
|
||||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
className="cornerstone-viewport-element"
|
||||||
indexPriority: -100,
|
style={{ height: '100%', width: '100%' }}
|
||||||
});
|
onContextMenu={e => e.preventDefault()}
|
||||||
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
|
onMouseDown={e => e.preventDefault()}
|
||||||
|
ref={el => {
|
||||||
const { ref: resizeRef } = useResizeDetector({
|
resizeRef.current = el;
|
||||||
onResize,
|
elementRef.current = el;
|
||||||
});
|
}}
|
||||||
return (
|
></div>
|
||||||
<React.Fragment>
|
<CornerstoneOverlays
|
||||||
<div className="viewport-wrapper">
|
viewportId={viewportId}
|
||||||
<div
|
toolBarService={toolbarService}
|
||||||
className="cornerstone-viewport-element"
|
element={elementRef.current}
|
||||||
style={{ height: '100%', width: '100%' }}
|
scrollbarHeight={scrollbarHeight}
|
||||||
onContextMenu={e => e.preventDefault()}
|
servicesManager={servicesManager}
|
||||||
onMouseDown={e => e.preventDefault()}
|
|
||||||
ref={el => {
|
|
||||||
resizeRef.current = el;
|
|
||||||
elementRef.current = el;
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
<CornerstoneOverlays
|
|
||||||
viewportId={viewportId}
|
|
||||||
toolBarService={toolbarService}
|
|
||||||
element={elementRef.current}
|
|
||||||
scrollbarHeight={scrollbarHeight}
|
|
||||||
servicesManager={servicesManager}
|
|
||||||
/>
|
|
||||||
<CinePlayer
|
|
||||||
enabledVPElement={enabledVPElement}
|
|
||||||
viewportId={viewportId}
|
|
||||||
servicesManager={servicesManager}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* top offset of 24px to account for ViewportActionCorners. */}
|
|
||||||
<div className="absolute top-[24px] w-full">
|
|
||||||
{viewportDialogState.viewportId === viewportId && (
|
|
||||||
<Notification
|
|
||||||
id="viewport-notification"
|
|
||||||
message={viewportDialogState.message}
|
|
||||||
type={viewportDialogState.type}
|
|
||||||
actions={viewportDialogState.actions}
|
|
||||||
onSubmit={viewportDialogState.onSubmit}
|
|
||||||
onOutsideClick={viewportDialogState.onOutsideClick}
|
|
||||||
onKeyPress={viewportDialogState.onKeyPress}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<CinePlayer
|
||||||
</div>
|
enabledVPElement={enabledVPElement}
|
||||||
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
|
viewportId={viewportId}
|
||||||
<OHIFViewportActionCorners viewportId={viewportId} />
|
servicesManager={servicesManager}
|
||||||
</React.Fragment>
|
/>
|
||||||
);
|
<ActiveViewportBehavior
|
||||||
}, areEqual);
|
viewportId={viewportId}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* top offset of 24px to account for ViewportActionCorners. */}
|
||||||
|
<div className="absolute top-[24px] w-full">
|
||||||
|
{viewportDialogState.viewportId === viewportId && (
|
||||||
|
<Notification
|
||||||
|
id="viewport-notification"
|
||||||
|
message={viewportDialogState.message}
|
||||||
|
type={viewportDialogState.type}
|
||||||
|
actions={viewportDialogState.actions}
|
||||||
|
onSubmit={viewportDialogState.onSubmit}
|
||||||
|
onOutsideClick={viewportDialogState.onOutsideClick}
|
||||||
|
onKeyPress={viewportDialogState.onKeyPress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
|
||||||
|
<OHIFViewportActionCorners viewportId={viewportId} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
areEqual
|
||||||
|
);
|
||||||
|
|
||||||
function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
|
function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
|
||||||
const { measurementService, cornerstoneViewportService } = servicesManager.services;
|
const { measurementService, cornerstoneViewportService } = servicesManager.services;
|
||||||
@ -542,12 +516,15 @@ function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _rehydrateSynchronizers(
|
function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) {
|
||||||
synchronizersStore: { [key: string]: unknown },
|
const { synchronizersStore } = useSynchronizersStore.getState();
|
||||||
viewportId: string,
|
const synchronizers = synchronizersStore[viewportId];
|
||||||
syncGroupService: any
|
|
||||||
) {
|
if (!synchronizers) {
|
||||||
synchronizersStore[viewportId].forEach(synchronizerObj => {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronizers.forEach(synchronizerObj => {
|
||||||
if (!synchronizerObj.id) {
|
if (!synchronizerObj.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -597,17 +574,91 @@ function _rehydrateSynchronizers(
|
|||||||
// Component displayName
|
// Component displayName
|
||||||
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
|
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
|
||||||
|
|
||||||
OHIFCornerstoneViewport.propTypes = {
|
function areEqual(prevProps, nextProps) {
|
||||||
displaySets: PropTypes.array.isRequired,
|
if (nextProps.needsRerendering) {
|
||||||
dataSource: PropTypes.object.isRequired,
|
console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering');
|
||||||
viewportOptions: PropTypes.object,
|
return false;
|
||||||
displaySetOptions: PropTypes.arrayOf(PropTypes.any),
|
}
|
||||||
servicesManager: PropTypes.object.isRequired,
|
|
||||||
onElementEnabled: PropTypes.func,
|
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
|
||||||
isJumpToMeasurementDisabled: PropTypes.bool,
|
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change');
|
||||||
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
return false;
|
||||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
}
|
||||||
// to set the initial state of the viewport's first image to render
|
|
||||||
initialImageIdOrIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
|
||||||
};
|
console.debug('OHIFCornerstoneViewport: Rerender caused by: orientation change');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
|
||||||
|
console.debug('OHIFCornerstoneViewport: Rerender caused by: toolGroupId change');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextProps.viewportOptions.viewportType &&
|
||||||
|
prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType
|
||||||
|
) {
|
||||||
|
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportType change');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextProps.viewportOptions.needsRerendering) {
|
||||||
|
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportOptions.needsRerendering');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevDisplaySets = prevProps.displaySets;
|
||||||
|
const nextDisplaySets = nextProps.displaySets;
|
||||||
|
|
||||||
|
if (prevDisplaySets.length !== nextDisplaySets.length) {
|
||||||
|
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length mismatch');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < prevDisplaySets.length; i++) {
|
||||||
|
const prevDisplaySet = prevDisplaySets[i];
|
||||||
|
|
||||||
|
const foundDisplaySet = nextDisplaySets.find(
|
||||||
|
nextDisplaySet =>
|
||||||
|
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!foundDisplaySet) {
|
||||||
|
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySet not found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check they contain the same image
|
||||||
|
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
|
||||||
|
console.debug('OHIFCornerstoneViewport: Rerender caused by: images length mismatch');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if their imageIds are the same
|
||||||
|
if (foundDisplaySet.images?.length) {
|
||||||
|
for (let j = 0; j < foundDisplaySet.images.length; j++) {
|
||||||
|
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
|
||||||
|
console.debug('OHIFCornerstoneViewport: Rerender caused by: imageId mismatch');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if display sets have changed
|
||||||
|
function haveDisplaySetsChanged(prevDisplaySets, currentDisplaySets) {
|
||||||
|
if (prevDisplaySets.length !== currentDisplaySets.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentDisplaySets.some((currentDS, index) => {
|
||||||
|
const prevDS = prevDisplaySets[index];
|
||||||
|
return currentDS.displaySetInstanceUID !== prevDS.displaySetInstanceUID;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default OHIFCornerstoneViewport;
|
export default OHIFCornerstoneViewport;
|
||||||
|
|||||||
@ -2,9 +2,9 @@ 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 { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
|
import type { ImageSliceData } from '@cornerstonejs/core/types';
|
||||||
import { ViewportOverlay } from '@ohif/ui';
|
import { ViewportOverlay } from '@ohif/ui';
|
||||||
import { InstanceMetadata } from '@ohif/core/src/types';
|
import type { InstanceMetadata } from '@ohif/core/src/types';
|
||||||
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||||
|
|
||||||
@ -45,14 +45,14 @@ const OverlayItemComponents = {
|
|||||||
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
|
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const studyDateItem = {
|
const studyDateItem = {
|
||||||
id: 'StudyDate',
|
id: 'StudyDate',
|
||||||
customizationType: 'ohif.overlayItem',
|
customizationType: 'ohif.overlayItem',
|
||||||
label: '',
|
label: '',
|
||||||
title: 'Study date',
|
title: 'Study date',
|
||||||
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
|
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
|
||||||
contentF: ({ referenceInstance, formatters: { formatDate } }) => formatDate(referenceInstance.StudyDate),
|
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
|
||||||
|
formatDate(referenceInstance.StudyDate),
|
||||||
};
|
};
|
||||||
|
|
||||||
const seriesDescriptionItem = {
|
const seriesDescriptionItem = {
|
||||||
@ -63,15 +63,19 @@ const seriesDescriptionItem = {
|
|||||||
condition: ({ referenceInstance }) => {
|
condition: ({ referenceInstance }) => {
|
||||||
return referenceInstance && referenceInstance.SeriesDescription;
|
return referenceInstance && referenceInstance.SeriesDescription;
|
||||||
},
|
},
|
||||||
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription
|
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
|
||||||
};
|
};
|
||||||
|
|
||||||
const topLeftItems = { id: 'cornerstoneOverlayTopLeft', items: [studyDateItem, seriesDescriptionItem] };
|
const topLeftItems = {
|
||||||
|
id: 'cornerstoneOverlayTopLeft',
|
||||||
|
items: [studyDateItem, seriesDescriptionItem],
|
||||||
|
};
|
||||||
|
|
||||||
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
|
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
|
||||||
|
|
||||||
const bottomLeftItems = {
|
const bottomLeftItems = {
|
||||||
id: 'cornerstoneOverlayBottomLeft', items: [
|
id: 'cornerstoneOverlayBottomLeft',
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
id: 'WindowLevel',
|
id: 'WindowLevel',
|
||||||
customizationType: 'ohif.overlayItem.windowLevel',
|
customizationType: 'ohif.overlayItem.windowLevel',
|
||||||
@ -79,12 +83,12 @@ const bottomLeftItems = {
|
|||||||
{
|
{
|
||||||
id: 'ZoomLevel',
|
id: 'ZoomLevel',
|
||||||
customizationType: 'ohif.overlayItem.zoomLevel',
|
customizationType: 'ohif.overlayItem.zoomLevel',
|
||||||
condition: (props) => {
|
condition: props => {
|
||||||
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
|
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
|
||||||
return activeToolName === 'Zoom';
|
return activeToolName === 'Zoom';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const bottomRightItems = {
|
const bottomRightItems = {
|
||||||
@ -94,7 +98,7 @@ const bottomRightItems = {
|
|||||||
id: 'InstanceNumber',
|
id: 'InstanceNumber',
|
||||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -145,19 +149,18 @@ function CustomizableViewportOverlay({
|
|||||||
// append functionality. This code enables the historical usage, but
|
// append functionality. This code enables the historical usage, but
|
||||||
// the recommended functionality is to append to the default values in
|
// the recommended functionality is to append to the default values in
|
||||||
// cornerstoneOverlay rather than defining individual items.
|
// cornerstoneOverlay rather than defining individual items.
|
||||||
const topLeftCustomization = customizationService.getCustomization(
|
const topLeftCustomization =
|
||||||
'cornerstoneOverlayTopLeft'
|
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
|
||||||
) || cornerstoneOverlay?.topLeftItems;
|
cornerstoneOverlay?.topLeftItems;
|
||||||
const topRightCustomization = customizationService.getCustomization(
|
const topRightCustomization =
|
||||||
'cornerstoneOverlayTopRight'
|
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
|
||||||
) || cornerstoneOverlay?.topRightItems;
|
cornerstoneOverlay?.topRightItems;
|
||||||
const bottomLeftCustomization = customizationService.getCustomization(
|
const bottomLeftCustomization =
|
||||||
'cornerstoneOverlayBottomLeft'
|
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
|
||||||
) || cornerstoneOverlay?.bottomLeftItems;
|
cornerstoneOverlay?.bottomLeftItems;
|
||||||
const bottomRightCustomization = customizationService.getCustomization(
|
const bottomRightCustomization =
|
||||||
'cornerstoneOverlayBottomRight'
|
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
|
||||||
) || cornerstoneOverlay?.bottomRightItems;
|
cornerstoneOverlay?.bottomRightItems;
|
||||||
|
|
||||||
|
|
||||||
const instanceNumber = useMemo(
|
const instanceNumber = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -177,13 +180,12 @@ function CustomizableViewportOverlay({
|
|||||||
return {
|
return {
|
||||||
displaySets,
|
displaySets,
|
||||||
displaySet,
|
displaySet,
|
||||||
instance: instances[imageIndex],
|
instance: instances?.[imageIndex],
|
||||||
instances,
|
instances,
|
||||||
referenceInstance,
|
referenceInstance,
|
||||||
};
|
};
|
||||||
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
|
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updating the VOI when the viewport changes its voi
|
* Updating the VOI when the viewport changes its voi
|
||||||
*/
|
*/
|
||||||
@ -307,7 +309,8 @@ function CustomizableViewportOverlay({
|
|||||||
<>
|
<>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<div key={`${keyPrefix}_${index}`}>
|
<div key={`${keyPrefix}_${index}`}>
|
||||||
{(!item?.condition || item.condition(props)) && _renderOverlayItem(item, props) || null}
|
{((!item?.condition || item.condition(props)) && _renderOverlayItem(item, props)) ||
|
||||||
|
null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@ -316,7 +319,6 @@ function CustomizableViewportOverlay({
|
|||||||
[_renderOverlayItem]
|
[_renderOverlayItem]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewportOverlay
|
<ViewportOverlay
|
||||||
topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
|
topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
|
||||||
@ -335,7 +337,9 @@ function getDisplaySets(viewportData, displaySetService) {
|
|||||||
if (!viewportData?.data?.length) {
|
if (!viewportData?.data?.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const displaySets = viewportData.data.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID)).filter(it => !!it);
|
const displaySets = viewportData.data
|
||||||
|
.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID))
|
||||||
|
.filter(it => !!it);
|
||||||
if (!displaySets.length) {
|
if (!displaySets.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -382,7 +386,6 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
|||||||
return parseInt(instanceNumber);
|
return parseInt(instanceNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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
|
||||||
@ -400,6 +403,11 @@ function _getInstanceNumberFromVolume(
|
|||||||
|
|
||||||
// Todo: support fusion of acquisition plane which has instanceNumber
|
// Todo: support fusion of acquisition plane which has instanceNumber
|
||||||
const { volume } = volumes[0];
|
const { volume } = volumes[0];
|
||||||
|
|
||||||
|
if (!volume) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { direction, imageIds } = volume;
|
const { direction, imageIds } = volume;
|
||||||
|
|
||||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
@ -443,9 +451,10 @@ function OverlayItem(props) {
|
|||||||
style={{ color, background }}
|
style={{ color, background }}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{label ? (<span className="mr-1 shrink-0">{label}</span>) : null}
|
{label ? <span className="mr-1 shrink-0">{label}</span> : null}
|
||||||
<span className="ml-1 mr-2 shrink-0">{value}</span>
|
<span className="ml-1 mr-2 shrink-0">{value}</span>
|
||||||
</div>);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Enums, VolumeViewport3D } from '@cornerstonejs/core';
|
import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
|
||||||
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
|
||||||
import { ImageScrollbar } from '@ohif/ui';
|
import { ImageScrollbar } from '@ohif/ui';
|
||||||
|
|
||||||
function CornerstoneImageScrollbar({
|
function CornerstoneImageScrollbar({
|
||||||
@ -12,7 +11,9 @@ function CornerstoneImageScrollbar({
|
|||||||
setImageSliceData,
|
setImageSliceData,
|
||||||
scrollbarHeight,
|
scrollbarHeight,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
}: withAppTypes) {
|
}: withAppTypes<{
|
||||||
|
element: HTMLElement;
|
||||||
|
}>) {
|
||||||
const { cineService, cornerstoneViewportService } = servicesManager.services;
|
const { cineService, cornerstoneViewportService } = servicesManager.services;
|
||||||
|
|
||||||
const onImageScrollbarChange = (imageIndex, viewportId) => {
|
const onImageScrollbarChange = (imageIndex, viewportId) => {
|
||||||
@ -26,7 +27,7 @@ function CornerstoneImageScrollbar({
|
|||||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
csToolsUtils.jumpToSlice(viewport.element, {
|
csUtils.jumpToSlice(viewport.element, {
|
||||||
imageIndex,
|
imageIndex,
|
||||||
debounceLoading: true,
|
debounceLoading: true,
|
||||||
});
|
});
|
||||||
@ -64,7 +65,9 @@ function CornerstoneImageScrollbar({
|
|||||||
|
|
||||||
const updateIndex = event => {
|
const updateIndex = event => {
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
if(!viewport || viewport instanceof VolumeViewport3D) return
|
if (!viewport || viewport instanceof VolumeViewport3D) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
|
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
|
||||||
const numberOfSlices = viewport.getNumberOfSlices();
|
const numberOfSlices = viewport.getNumberOfSlices();
|
||||||
// find the index of imageId in the imageIds
|
// find the index of imageId in the imageIds
|
||||||
|
|||||||
@ -1,14 +1,7 @@
|
|||||||
import React, { useEffect, useState, useMemo } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core';
|
||||||
metaData,
|
|
||||||
Enums,
|
|
||||||
Types,
|
|
||||||
getEnabledElement,
|
|
||||||
utilities as csUtils,
|
|
||||||
} from '@cornerstonejs/core';
|
|
||||||
import { utilities } from '@cornerstonejs/tools';
|
import { utilities } from '@cornerstonejs/tools';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { vec3 } from 'gl-matrix';
|
import { vec3 } from 'gl-matrix';
|
||||||
|
|
||||||
import './ViewportOrientationMarkers.css';
|
import './ViewportOrientationMarkers.css';
|
||||||
@ -31,8 +24,9 @@ function ViewportOrientationMarkers({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
|
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
|
||||||
const { rotation, previousCamera, camera } = evt.detail;
|
const { previousCamera, camera } = evt.detail;
|
||||||
|
|
||||||
|
const { rotation } = camera;
|
||||||
if (rotation !== undefined) {
|
if (rotation !== undefined) {
|
||||||
setRotation(rotation);
|
setRotation(rotation);
|
||||||
}
|
}
|
||||||
@ -74,7 +68,12 @@ function ViewportOrientationMarkers({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
({ rowCosines, columnCosines, isDefaultValueSetForColumnCosine, isDefaultValueSetForColumnCosine } = metaData.get('imagePlaneModule', imageId) || {});
|
({
|
||||||
|
rowCosines,
|
||||||
|
columnCosines,
|
||||||
|
isDefaultValueSetForColumnCosine,
|
||||||
|
isDefaultValueSetForColumnCosine,
|
||||||
|
} = metaData.get('imagePlaneModule', imageId) || {});
|
||||||
} else {
|
} else {
|
||||||
if (!element || !getEnabledElement(element)) {
|
if (!element || !getEnabledElement(element)) {
|
||||||
return '';
|
return '';
|
||||||
@ -90,7 +89,13 @@ function ViewportOrientationMarkers({
|
|||||||
rowCosines = viewRight;
|
rowCosines = viewRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rowCosines || !columnCosines || rotation === undefined || isDefaultValueSetForRowCosine || isDefaultValueSetForColumnCosine) {
|
if (
|
||||||
|
!rowCosines ||
|
||||||
|
!columnCosines ||
|
||||||
|
rotation === undefined ||
|
||||||
|
isDefaultValueSetForRowCosine ||
|
||||||
|
isDefaultValueSetForColumnCosine
|
||||||
|
) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,15 +12,23 @@ import {
|
|||||||
utilities as cstUtils,
|
utilities as cstUtils,
|
||||||
ReferenceLinesTool,
|
ReferenceLinesTool,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
|
|
||||||
import { Types as OhifTypes } from '@ohif/core';
|
import { Types as OhifTypes } from '@ohif/core';
|
||||||
|
import {
|
||||||
|
callLabelAutocompleteDialog,
|
||||||
|
showLabelAnnotationPopup,
|
||||||
|
createReportAsync,
|
||||||
|
callInputDialog,
|
||||||
|
colorPickerDialog,
|
||||||
|
} from '@ohif/extension-default';
|
||||||
import { vec3, mat4 } from 'gl-matrix';
|
import { vec3, mat4 } from 'gl-matrix';
|
||||||
|
|
||||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||||
import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/callInputDialog';
|
|
||||||
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
|
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 toggleVOISliceSync from './utils/toggleVOISliceSync';
|
import toggleVOISliceSync from './utils/toggleVOISliceSync';
|
||||||
|
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
|
||||||
|
|
||||||
const toggleSyncFunctions = {
|
const toggleSyncFunctions = {
|
||||||
imageSlice: toggleImageSliceSync,
|
imageSlice: toggleImageSliceSync,
|
||||||
@ -29,6 +37,7 @@ const toggleSyncFunctions = {
|
|||||||
|
|
||||||
function commandsModule({
|
function commandsModule({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
|
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
|
||||||
const {
|
const {
|
||||||
@ -101,7 +110,49 @@ function commandsModule({
|
|||||||
|
|
||||||
commandsManager.run(options, optionsToUse);
|
commandsManager.run(options, optionsToUse);
|
||||||
},
|
},
|
||||||
|
updateStoredSegmentationPresentation: ({ displaySet, type }) => {
|
||||||
|
const { setSegmentationPresentation } = useSegmentationPresentationStore.getState();
|
||||||
|
|
||||||
|
const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID;
|
||||||
|
setSegmentationPresentation(referencedDisplaySetInstanceUID, [
|
||||||
|
{
|
||||||
|
segmentationId: displaySet.displaySetInstanceUID,
|
||||||
|
hydrated: true,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
updateStoredPositionPresentation: ({ viewportId, displaySetInstanceUID }) => {
|
||||||
|
const presentations = cornerstoneViewportService.getPresentations(viewportId);
|
||||||
|
const { positionPresentationStore, setPositionPresentation, getPositionPresentationId } =
|
||||||
|
usePositionPresentationStore.getState();
|
||||||
|
|
||||||
|
// Look inside positionPresentationStore and find the key that includes the displaySetInstanceUID
|
||||||
|
// and the value has viewportId as activeViewportId.
|
||||||
|
const previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find(
|
||||||
|
([key, value]) => key.includes(displaySetInstanceUID) && value.viewportId === viewportId
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
if (previousReferencedDisplaySetStoreKey) {
|
||||||
|
setPositionPresentation(
|
||||||
|
previousReferencedDisplaySetStoreKey,
|
||||||
|
presentations.positionPresentation
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not found means we have not visited that referencedDisplaySetInstanceUID before
|
||||||
|
// so we need to grab the positionPresentationId directly from the store,
|
||||||
|
// Todo: this is really hacky, we should have a better way for this
|
||||||
|
|
||||||
|
const positionPresentationId = getPositionPresentationId({
|
||||||
|
displaySetInstanceUIDs: [displaySetInstanceUID],
|
||||||
|
viewportId,
|
||||||
|
});
|
||||||
|
|
||||||
|
setPositionPresentation(positionPresentationId, presentations.positionPresentation);
|
||||||
|
},
|
||||||
getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
|
getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
|
||||||
return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates);
|
return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates);
|
||||||
},
|
},
|
||||||
@ -435,9 +486,10 @@ function commandsModule({
|
|||||||
viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 });
|
viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 });
|
||||||
viewport.render();
|
viewport.render();
|
||||||
} else if (viewport.getRotation !== undefined) {
|
} else if (viewport.getRotation !== undefined) {
|
||||||
const currentRotation = viewport.getRotation();
|
const presentation = viewport.getViewPresentation();
|
||||||
|
const { rotation: currentRotation } = presentation;
|
||||||
const newRotation = (currentRotation + rotation) % 360;
|
const newRotation = (currentRotation + rotation) % 360;
|
||||||
viewport.setProperties({ rotation: newRotation });
|
viewport.setViewPresentation({ rotation: newRotation });
|
||||||
viewport.render();
|
viewport.render();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -554,7 +606,7 @@ function commandsModule({
|
|||||||
|
|
||||||
// Set slice to last slice
|
// Set slice to last slice
|
||||||
const options = { imageIndex: jumpIndex };
|
const options = { imageIndex: jumpIndex };
|
||||||
cstUtils.jumpToSlice(viewport.element, options);
|
csUtils.jumpToSlice(viewport.element, options);
|
||||||
},
|
},
|
||||||
scroll: ({ direction }) => {
|
scroll: ({ direction }) => {
|
||||||
const enabledElement = _getActiveViewportEnabledElement();
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
@ -566,7 +618,7 @@ function commandsModule({
|
|||||||
const { viewport } = enabledElement;
|
const { viewport } = enabledElement;
|
||||||
const options = { delta: direction };
|
const options = { delta: direction };
|
||||||
|
|
||||||
cstUtils.scroll(viewport, options);
|
csUtils.scroll(viewport, options);
|
||||||
},
|
},
|
||||||
setViewportColormap: ({
|
setViewportColormap: ({
|
||||||
viewportId,
|
viewportId,
|
||||||
@ -577,7 +629,6 @@ function commandsModule({
|
|||||||
}) => {
|
}) => {
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
const actorEntries = viewport.getActors();
|
|
||||||
let hpOpacity;
|
let hpOpacity;
|
||||||
// Retrieve active protocol's viewport match details
|
// Retrieve active protocol's viewport match details
|
||||||
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
|
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
|
||||||
@ -596,14 +647,8 @@ function commandsModule({
|
|||||||
// HP takes priority over the default opacity
|
// HP takes priority over the default opacity
|
||||||
colormap = { ...colormap, opacity: hpOpacity || opacity };
|
colormap = { ...colormap, opacity: hpOpacity || opacity };
|
||||||
|
|
||||||
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) {
|
if (viewport instanceof StackViewport) {
|
||||||
setViewportProperties(viewport, viewportId);
|
viewport.setProperties({ colormap });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewport instanceof VolumeViewport) {
|
if (viewport instanceof VolumeViewport) {
|
||||||
@ -611,7 +656,9 @@ function commandsModule({
|
|||||||
const { viewports } = viewportGridService.getState();
|
const { viewports } = viewportGridService.getState();
|
||||||
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
|
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
|
||||||
}
|
}
|
||||||
setViewportProperties(viewport, displaySetInstanceUID);
|
|
||||||
|
const volumeId = viewport.getVolumeId();
|
||||||
|
viewport.setProperties({ colormap }, volumeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
@ -827,6 +874,339 @@ function commandsModule({
|
|||||||
ins?.resetCrosshairs();
|
ins?.resetCrosshairs();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Creates a labelmap for the active viewport
|
||||||
|
*/
|
||||||
|
createLabelmapForViewport: async ({ viewportId, options = {} }) => {
|
||||||
|
const { viewportGridService, displaySetService, segmentationService } =
|
||||||
|
servicesManager.services;
|
||||||
|
const { viewports } = viewportGridService.getState();
|
||||||
|
const targetViewportId = viewportId;
|
||||||
|
|
||||||
|
const viewport = viewports.get(targetViewportId);
|
||||||
|
|
||||||
|
// Todo: add support for multiple display sets
|
||||||
|
const displaySetInstanceUID =
|
||||||
|
options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0];
|
||||||
|
|
||||||
|
const segs = segmentationService.getSegmentations();
|
||||||
|
|
||||||
|
const label = options.label || `Segmentation ${segs.length + 1}`;
|
||||||
|
const segmentationId = options.segmentationId || `${csUtils.uuidv4()}`;
|
||||||
|
|
||||||
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||||
|
|
||||||
|
const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet(
|
||||||
|
displaySet,
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
segmentationId,
|
||||||
|
segments: options.createInitialSegment
|
||||||
|
? {
|
||||||
|
1: {
|
||||||
|
label: 'Segment 1',
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await segmentationService.addSegmentationRepresentation(viewportId, {
|
||||||
|
segmentationId,
|
||||||
|
type: Enums.SegmentationRepresentations.Labelmap,
|
||||||
|
});
|
||||||
|
|
||||||
|
return generatedSegmentationId;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active segmentation for a viewport
|
||||||
|
* @param props.segmentationId - The ID of the segmentation to set as active
|
||||||
|
*/
|
||||||
|
setActiveSegmentation: ({ segmentationId }) => {
|
||||||
|
const { viewportGridService, segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.setActiveSegmentation(
|
||||||
|
viewportGridService.getActiveViewportId(),
|
||||||
|
segmentationId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new segment to a segmentation
|
||||||
|
* @param props.segmentationId - The ID of the segmentation to add the segment to
|
||||||
|
*/
|
||||||
|
addSegmentCommand: ({ segmentationId }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.addSegment(segmentationId);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active segment and jumps to its center
|
||||||
|
* @param props.segmentationId - The ID of the segmentation
|
||||||
|
* @param props.segmentIndex - The index of the segment to activate
|
||||||
|
*/
|
||||||
|
setActiveSegmentAndCenterCommand: ({ segmentationId, segmentIndex }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.setActiveSegment(segmentationId, segmentIndex);
|
||||||
|
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of a segment
|
||||||
|
* @param props.segmentationId - The ID of the segmentation
|
||||||
|
* @param props.segmentIndex - The index of the segment
|
||||||
|
* @param props.type - The type of visibility to toggle
|
||||||
|
*/
|
||||||
|
toggleSegmentVisibilityCommand: ({ segmentationId, segmentIndex, type }) => {
|
||||||
|
const { segmentationService, viewportGridService } = servicesManager.services;
|
||||||
|
segmentationService.toggleSegmentVisibility(
|
||||||
|
viewportGridService.getActiveViewportId(),
|
||||||
|
segmentationId,
|
||||||
|
segmentIndex,
|
||||||
|
type
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the lock state of a segment
|
||||||
|
* @param props.segmentationId - The ID of the segmentation
|
||||||
|
* @param props.segmentIndex - The index of the segment
|
||||||
|
*/
|
||||||
|
toggleSegmentLockCommand: ({ segmentationId, segmentIndex }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of a segmentation representation
|
||||||
|
* @param props.segmentationId - The ID of the segmentation
|
||||||
|
* @param props.type - The type of representation
|
||||||
|
*/
|
||||||
|
toggleSegmentationVisibilityCommand: ({ segmentationId, type }) => {
|
||||||
|
const { segmentationService, viewportGridService } = servicesManager.services;
|
||||||
|
segmentationService.toggleSegmentationRepresentationVisibility(
|
||||||
|
viewportGridService.getActiveViewportId(),
|
||||||
|
{ segmentationId, type }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a segmentation
|
||||||
|
* @param props.segmentationId - The ID of the segmentation to download
|
||||||
|
*/
|
||||||
|
downloadSegmentationCommand: ({ segmentationId }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.downloadSegmentation(segmentationId);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a segmentation and shows it in the viewport
|
||||||
|
* @param props.segmentationId - The ID of the segmentation to store
|
||||||
|
*/
|
||||||
|
storeSegmentationCommand: async ({ segmentationId }) => {
|
||||||
|
const { segmentationService, viewportGridService } = servicesManager.services;
|
||||||
|
const datasources = extensionManager.getActiveDataSource();
|
||||||
|
|
||||||
|
const displaySetInstanceUIDs = await createReportAsync({
|
||||||
|
servicesManager,
|
||||||
|
getReport: () =>
|
||||||
|
commandsManager.runCommand('storeSegmentation', {
|
||||||
|
segmentationId,
|
||||||
|
dataSource: datasources[0],
|
||||||
|
}),
|
||||||
|
reportType: 'Segmentation',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (displaySetInstanceUIDs) {
|
||||||
|
segmentationService.remove(segmentationId);
|
||||||
|
viewportGridService.setDisplaySetsForViewport({
|
||||||
|
viewportId: viewportGridService.getActiveViewportId(),
|
||||||
|
displaySetInstanceUIDs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a segmentation as RTSS
|
||||||
|
* @param props.segmentationId - The ID of the segmentation
|
||||||
|
*/
|
||||||
|
downloadRTSSCommand: ({ segmentationId }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.downloadRTSS(segmentationId);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the style for a segmentation
|
||||||
|
* @param props.segmentationId - The ID of the segmentation
|
||||||
|
* @param props.type - The type of style
|
||||||
|
* @param props.key - The style key to set
|
||||||
|
* @param props.value - The style value
|
||||||
|
*/
|
||||||
|
setSegmentationStyleCommand: ({ segmentationId, type, key, value }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.setStyle({ type }, { [key]: value });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a segment from a segmentation
|
||||||
|
* @param props.segmentationId - The ID of the segmentation
|
||||||
|
* @param props.segmentIndex - The index of the segment to delete
|
||||||
|
*/
|
||||||
|
deleteSegmentCommand: ({ segmentationId, segmentIndex }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.removeSegment(segmentationId, segmentIndex);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an entire segmentation
|
||||||
|
* @param props.segmentationId - The ID of the segmentation to delete
|
||||||
|
*/
|
||||||
|
deleteSegmentationCommand: ({ segmentationId }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.remove(segmentationId);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a segmentation from the viewport
|
||||||
|
* @param props.segmentationId - The ID of the segmentation to remove
|
||||||
|
*/
|
||||||
|
removeSegmentationFromViewportCommand: ({ segmentationId }) => {
|
||||||
|
const { segmentationService, viewportGridService } = servicesManager.services;
|
||||||
|
segmentationService.removeSegmentationRepresentations(
|
||||||
|
viewportGridService.getActiveViewportId(),
|
||||||
|
{ segmentationId }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles rendering of inactive segmentations
|
||||||
|
*/
|
||||||
|
toggleRenderInactiveSegmentationsCommand: () => {
|
||||||
|
const { segmentationService, viewportGridService } = servicesManager.services;
|
||||||
|
const viewportId = viewportGridService.getActiveViewportId();
|
||||||
|
const renderInactive = segmentationService.getRenderInactiveSegmentations(viewportId);
|
||||||
|
segmentationService.setRenderInactiveSegmentations(viewportId, !renderInactive);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the fill alpha value for a segmentation type
|
||||||
|
* @param props.type - The type of segmentation
|
||||||
|
* @param props.value - The alpha value to set
|
||||||
|
*/
|
||||||
|
setFillAlphaCommand: ({ type, value }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.setStyle({ type }, { fillAlpha: value });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the outline width for a segmentation type
|
||||||
|
* @param props.type - The type of segmentation
|
||||||
|
* @param props.value - The width value to set
|
||||||
|
*/
|
||||||
|
setOutlineWidthCommand: ({ type, value }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.setStyle({ type }, { outlineWidth: value });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether to render fill for a segmentation type
|
||||||
|
* @param props.type - The type of segmentation
|
||||||
|
* @param props.value - Whether to render fill
|
||||||
|
*/
|
||||||
|
setRenderFillCommand: ({ type, value }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.setStyle({ type }, { renderFill: value });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether to render outline for a segmentation type
|
||||||
|
* @param props.type - The type of segmentation
|
||||||
|
* @param props.value - Whether to render outline
|
||||||
|
*/
|
||||||
|
setRenderOutlineCommand: ({ type, value }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.setStyle({ type }, { renderOutline: value });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the fill alpha for inactive segmentations
|
||||||
|
* @param props.type - The type of segmentation
|
||||||
|
* @param props.value - The alpha value to set
|
||||||
|
*/
|
||||||
|
setFillAlphaInactiveCommand: ({ type, value }) => {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
segmentationService.setStyle({ type }, { fillAlphaInactive: value });
|
||||||
|
},
|
||||||
|
|
||||||
|
editSegmentLabel: ({ segmentationId, segmentIndex }) => {
|
||||||
|
const { segmentationService, uiDialogService } = servicesManager.services;
|
||||||
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
|
||||||
|
if (!segmentation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segment = segmentation.segments[segmentIndex];
|
||||||
|
const { label } = segment;
|
||||||
|
|
||||||
|
callInputDialog(uiDialogService, label, (label, actionId) => {
|
||||||
|
if (label === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
editSegmentationLabel: ({ segmentationId }) => {
|
||||||
|
const { segmentationService, uiDialogService } = servicesManager.services;
|
||||||
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
|
||||||
|
if (!segmentation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { label } = segmentation;
|
||||||
|
|
||||||
|
callInputDialog(uiDialogService, label, (label, actionId) => {
|
||||||
|
if (label === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentationService.addOrUpdateSegmentation({ segmentationId, label });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
editSegmentColor: ({ segmentationId, segmentIndex }) => {
|
||||||
|
const { segmentationService, uiDialogService, viewportGridService } =
|
||||||
|
servicesManager.services;
|
||||||
|
const viewportId = viewportGridService.getActiveViewportId();
|
||||||
|
const color = segmentationService.getSegmentColor(viewportId, segmentationId, segmentIndex);
|
||||||
|
|
||||||
|
const rgbaColor = {
|
||||||
|
r: color[0],
|
||||||
|
g: color[1],
|
||||||
|
b: color[2],
|
||||||
|
a: color[3] / 255.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
||||||
|
if (actionId === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
|
||||||
|
segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getRenderInactiveSegmentations: () => {
|
||||||
|
const { segmentationService, viewportGridService } = servicesManager.services;
|
||||||
|
return segmentationService.getRenderInactiveSegmentations(
|
||||||
|
viewportGridService.getActiveViewportId()
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
@ -988,6 +1368,84 @@ function commandsModule({
|
|||||||
toggleActiveDisabledToolbar: {
|
toggleActiveDisabledToolbar: {
|
||||||
commandFn: actions.toggleActiveDisabledToolbar,
|
commandFn: actions.toggleActiveDisabledToolbar,
|
||||||
},
|
},
|
||||||
|
updateStoredPositionPresentation: {
|
||||||
|
commandFn: actions.updateStoredPositionPresentation,
|
||||||
|
},
|
||||||
|
updateStoredSegmentationPresentation: {
|
||||||
|
commandFn: actions.updateStoredSegmentationPresentation,
|
||||||
|
},
|
||||||
|
createLabelmapForViewport: {
|
||||||
|
commandFn: actions.createLabelmapForViewport,
|
||||||
|
},
|
||||||
|
setActiveSegmentation: {
|
||||||
|
commandFn: actions.setActiveSegmentation,
|
||||||
|
},
|
||||||
|
addSegment: {
|
||||||
|
commandFn: actions.addSegmentCommand,
|
||||||
|
},
|
||||||
|
setActiveSegmentAndCenter: {
|
||||||
|
commandFn: actions.setActiveSegmentAndCenterCommand,
|
||||||
|
},
|
||||||
|
toggleSegmentVisibility: {
|
||||||
|
commandFn: actions.toggleSegmentVisibilityCommand,
|
||||||
|
},
|
||||||
|
toggleSegmentLock: {
|
||||||
|
commandFn: actions.toggleSegmentLockCommand,
|
||||||
|
},
|
||||||
|
toggleSegmentationVisibility: {
|
||||||
|
commandFn: actions.toggleSegmentationVisibilityCommand,
|
||||||
|
},
|
||||||
|
downloadSegmentation: {
|
||||||
|
commandFn: actions.downloadSegmentationCommand,
|
||||||
|
},
|
||||||
|
storeSegmentation: {
|
||||||
|
commandFn: actions.storeSegmentationCommand,
|
||||||
|
},
|
||||||
|
downloadRTSS: {
|
||||||
|
commandFn: actions.downloadRTSSCommand,
|
||||||
|
},
|
||||||
|
setSegmentationStyle: {
|
||||||
|
commandFn: actions.setSegmentationStyleCommand,
|
||||||
|
},
|
||||||
|
deleteSegment: {
|
||||||
|
commandFn: actions.deleteSegmentCommand,
|
||||||
|
},
|
||||||
|
deleteSegmentation: {
|
||||||
|
commandFn: actions.deleteSegmentationCommand,
|
||||||
|
},
|
||||||
|
removeSegmentationFromViewport: {
|
||||||
|
commandFn: actions.removeSegmentationFromViewportCommand,
|
||||||
|
},
|
||||||
|
toggleRenderInactiveSegmentations: {
|
||||||
|
commandFn: actions.toggleRenderInactiveSegmentationsCommand,
|
||||||
|
},
|
||||||
|
setFillAlpha: {
|
||||||
|
commandFn: actions.setFillAlphaCommand,
|
||||||
|
},
|
||||||
|
setOutlineWidth: {
|
||||||
|
commandFn: actions.setOutlineWidthCommand,
|
||||||
|
},
|
||||||
|
setRenderFill: {
|
||||||
|
commandFn: actions.setRenderFillCommand,
|
||||||
|
},
|
||||||
|
setRenderOutline: {
|
||||||
|
commandFn: actions.setRenderOutlineCommand,
|
||||||
|
},
|
||||||
|
setFillAlphaInactive: {
|
||||||
|
commandFn: actions.setFillAlphaInactiveCommand,
|
||||||
|
},
|
||||||
|
editSegmentLabel: {
|
||||||
|
commandFn: actions.editSegmentLabel,
|
||||||
|
},
|
||||||
|
editSegmentationLabel: {
|
||||||
|
commandFn: actions.editSegmentationLabel,
|
||||||
|
},
|
||||||
|
editSegmentColor: {
|
||||||
|
commandFn: actions.editSegmentColor,
|
||||||
|
},
|
||||||
|
getRenderInactiveSegmentations: {
|
||||||
|
commandFn: actions.getRenderInactiveSegmentations,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||||
import { CinePlayer, useCine } from '@ohif/ui';
|
import { CinePlayer, useCine } from '@ohif/ui';
|
||||||
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
|
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
|
||||||
import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
|
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
|
|
||||||
function WrappedCinePlayer({
|
function WrappedCinePlayer({
|
||||||
@ -101,7 +100,7 @@ function WrappedCinePlayer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
|
enabledVPElement.addEventListener(Enums.Events.VIEWPORT_NEW_IMAGE_SET, newDisplaySetHandler);
|
||||||
// this doesn't makes sense that we are listening to this event on viewport element
|
// this doesn't makes sense that we are listening to this event on viewport element
|
||||||
enabledVPElement.addEventListener(
|
enabledVPElement.addEventListener(
|
||||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||||
@ -111,7 +110,10 @@ function WrappedCinePlayer({
|
|||||||
return () => {
|
return () => {
|
||||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||||
|
|
||||||
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
|
enabledVPElement.removeEventListener(
|
||||||
|
Enums.Events.VIEWPORT_NEW_IMAGE_SET,
|
||||||
|
newDisplaySetHandler
|
||||||
|
);
|
||||||
enabledVPElement.removeEventListener(
|
enabledVPElement.removeEventListener(
|
||||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||||
newDisplaySetHandler
|
newDisplaySetHandler
|
||||||
@ -181,13 +183,13 @@ function RenderCinePlayer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
eventTarget.addEventListener(
|
eventTarget.addEventListener(
|
||||||
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||||
handleTimePointIndexChange
|
handleTimePointIndexChange
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
eventTarget.removeEventListener(
|
eventTarget.removeEventListener(
|
||||||
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||||
handleTimePointIndexChange
|
handleTimePointIndexChange
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -199,7 +201,7 @@ function RenderCinePlayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
|
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
|
||||||
const volume = cache.getVolume(volumeId);
|
const volume = cache.getVolume(volumeId, true);
|
||||||
volume.timePointIndex = timePointIndex;
|
volume.timePointIndex = timePointIndex;
|
||||||
|
|
||||||
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
||||||
@ -207,7 +209,7 @@ function RenderCinePlayer({
|
|||||||
|
|
||||||
const updateDynamicInfo = useCallback(props => {
|
const updateDynamicInfo = useCallback(props => {
|
||||||
const { volumeId, timePointIndex } = props;
|
const { volumeId, timePointIndex } = props;
|
||||||
const volume = cache.getVolume(volumeId);
|
const volume = cache.getVolume(volumeId, true);
|
||||||
volume.timePointIndex = timePointIndex;
|
volume.timePointIndex = timePointIndex;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -223,6 +225,7 @@ function RenderCinePlayer({
|
|||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
});
|
});
|
||||||
cineService.setIsCineEnabled(false);
|
cineService.setIsCineEnabled(false);
|
||||||
|
cineService.setViewportCineClosed(viewportId);
|
||||||
}}
|
}}
|
||||||
onPlayPauseChange={isPlaying => {
|
onPlayPauseChange={isPlaying => {
|
||||||
cineService.setCine({
|
cineService.setCine({
|
||||||
|
|||||||
@ -0,0 +1,138 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Button, Icons, Separator } from '@ohif/ui-next';
|
||||||
|
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||||
|
|
||||||
|
function ViewportSegmentationMenu({
|
||||||
|
viewportId,
|
||||||
|
servicesManager,
|
||||||
|
}: withAppTypes<{ viewportId: string }>) {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
const [activeSegmentations, setActiveSegmentations] = useState([]);
|
||||||
|
const [availableSegmentations, setAvailableSegmentations] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateSegmentations = () => {
|
||||||
|
const active = segmentationService.getSegmentationRepresentations(viewportId);
|
||||||
|
setActiveSegmentations(active);
|
||||||
|
|
||||||
|
const all = segmentationService.getSegmentations();
|
||||||
|
const available = all.filter(
|
||||||
|
seg => !active.some(activeSeg => activeSeg.segmentationId === seg.segmentationId)
|
||||||
|
);
|
||||||
|
setAvailableSegmentations(available);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSegmentations();
|
||||||
|
|
||||||
|
const subscriptions = [
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
|
||||||
|
].map(event => segmentationService.subscribe(event, updateSegmentations));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
};
|
||||||
|
}, [segmentationService, viewportId]);
|
||||||
|
|
||||||
|
const toggleSegmentationRepresentationVisibility = (
|
||||||
|
segmentationId,
|
||||||
|
type = SegmentationRepresentations.Labelmap
|
||||||
|
) => {
|
||||||
|
segmentationService.toggleSegmentationRepresentationVisibility(viewportId, {
|
||||||
|
segmentationId,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSegmentationToViewport = segmentationId => {
|
||||||
|
segmentationService.addSegmentationRepresentation(viewportId, { segmentationId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSegmentationFromViewport = segmentationId => {
|
||||||
|
segmentationService.removeSegmentationRepresentations(viewportId, {
|
||||||
|
segmentationId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-muted flex h-full w-[262px] flex-col rounded p-3">
|
||||||
|
<span className="text-muted-foreground mb-2 text-xs font-semibold">Current Viewport</span>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{activeSegmentations.map(segmentation => (
|
||||||
|
<li
|
||||||
|
key={segmentation.id}
|
||||||
|
className="flex items-center text-sm"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-muted-foreground mr-2"
|
||||||
|
onClick={() => removeSegmentationFromViewport(segmentation.segmentationId)}
|
||||||
|
>
|
||||||
|
<Icons.Minus className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
<span className="text-foreground flex-grow">{segmentation.label}</span>
|
||||||
|
{segmentation.visible ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-muted-foreground"
|
||||||
|
onClick={() =>
|
||||||
|
toggleSegmentationRepresentationVisibility(
|
||||||
|
segmentation.segmentationId,
|
||||||
|
segmentation.type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icons.Hide className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-muted-foreground"
|
||||||
|
onClick={() =>
|
||||||
|
toggleSegmentationRepresentationVisibility(
|
||||||
|
segmentation.segmentationId,
|
||||||
|
segmentation.type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icons.Show className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{availableSegmentations.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Separator className="bg-input mb-3" />
|
||||||
|
<span className="text-muted-foreground mb-2 text-xs font-semibold">Available</span>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{availableSegmentations.map(segmentation => (
|
||||||
|
<li
|
||||||
|
key={segmentation.segmentation.segmentationId}
|
||||||
|
className="flex items-center text-sm"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-muted-foreground mr-2"
|
||||||
|
onClick={() =>
|
||||||
|
addSegmentationToViewport(segmentation.segmentation.segmentationId)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icons.Plus className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
<span className="text-foreground/60">{segmentation.segmentation.label}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ViewportSegmentationMenu;
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
import React, { ReactNode, useEffect, useState } from 'react';
|
||||||
|
import { Button, Icons, Popover, PopoverContent, PopoverTrigger } from '@ohif/ui-next';
|
||||||
|
import ViewportSegmentationMenu from './ViewportSegmentationMenu';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export function ViewportSegmentationMenuWrapper({
|
||||||
|
viewportId,
|
||||||
|
displaySets,
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
location,
|
||||||
|
}: withAppTypes<{
|
||||||
|
viewportId: string;
|
||||||
|
element: HTMLElement;
|
||||||
|
}>): ReactNode {
|
||||||
|
const { segmentationService, viewportActionCornersService, viewportGridService } =
|
||||||
|
servicesManager.services;
|
||||||
|
const [representations, setRepresentations] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const representations = segmentationService.getSegmentationRepresentations(viewportId);
|
||||||
|
setRepresentations(representations);
|
||||||
|
}, [viewportId, segmentationService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
|
||||||
|
() => {
|
||||||
|
const representations = segmentationService.getSegmentationRepresentations(viewportId);
|
||||||
|
setRepresentations(representations);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [viewportId, segmentationService]);
|
||||||
|
|
||||||
|
const activeViewportId = viewportGridService.getActiveViewportId();
|
||||||
|
const isActiveViewport = viewportId === activeViewportId;
|
||||||
|
|
||||||
|
const { align, side } = getAlignAndSide(viewportActionCornersService, location);
|
||||||
|
|
||||||
|
if (!representations?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger
|
||||||
|
asChild
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<Icons.ViewportViews
|
||||||
|
className={classNames(
|
||||||
|
'text-highlight',
|
||||||
|
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="border-none bg-transparent p-0 shadow-none"
|
||||||
|
side={side}
|
||||||
|
align={align}
|
||||||
|
alignOffset={-15}
|
||||||
|
sideOffset={5}
|
||||||
|
>
|
||||||
|
<ViewportSegmentationMenu
|
||||||
|
className="w-full"
|
||||||
|
viewportId={viewportId}
|
||||||
|
displaySets={displaySets}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAlignAndSide = (viewportActionCornersService, location) => {
|
||||||
|
const ViewportActionCornersLocations = viewportActionCornersService.LOCATIONS;
|
||||||
|
|
||||||
|
switch (location) {
|
||||||
|
case ViewportActionCornersLocations.topLeft:
|
||||||
|
return { align: 'start', side: 'bottom' };
|
||||||
|
case ViewportActionCornersLocations.topRight:
|
||||||
|
return { align: 'end', side: 'bottom' };
|
||||||
|
case ViewportActionCornersLocations.bottomLeft:
|
||||||
|
return { align: 'start', side: 'top' };
|
||||||
|
case ViewportActionCornersLocations.bottomRight:
|
||||||
|
return { align: 'end', side: 'top' };
|
||||||
|
default:
|
||||||
|
console.debug('Unknown location, defaulting to bottom-start');
|
||||||
|
return { align: 'start', side: 'bottom' };
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper';
|
||||||
|
|
||||||
|
export function getViewportDataOverlaySettingsMenu(
|
||||||
|
props: withAppTypes<{
|
||||||
|
viewportId: string;
|
||||||
|
element: HTMLElement;
|
||||||
|
}>
|
||||||
|
): ReactNode {
|
||||||
|
return <ViewportSegmentationMenuWrapper {...props} />;
|
||||||
|
}
|
||||||
@ -1,10 +1,15 @@
|
|||||||
import React, { useEffect, useCallback, useState, ReactElement } from 'react';
|
import React, { useEffect, useCallback, useState, ReactElement, useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
import { PanelSection, WindowLevel } from '@ohif/ui';
|
import { PanelSection, WindowLevel } from '@ohif/ui';
|
||||||
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
|
import { Enums, eventTarget } from '@cornerstonejs/core';
|
||||||
import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core';
|
import { useActiveViewportDisplaySets } from '@ohif/core';
|
||||||
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
|
import {
|
||||||
|
getNodeOpacity,
|
||||||
|
isPetVolumeWithDefaultOpacity,
|
||||||
|
isVolumeWithConstantOpacity,
|
||||||
|
getWindowLevelsData,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
const { Events } = Enums;
|
const { Events } = Enums;
|
||||||
|
|
||||||
@ -16,19 +21,16 @@ const ViewportWindowLevel = ({
|
|||||||
}>): ReactElement => {
|
}>): ReactElement => {
|
||||||
const { cornerstoneViewportService } = servicesManager.services;
|
const { cornerstoneViewportService } = servicesManager.services;
|
||||||
const [windowLevels, setWindowLevels] = useState([]);
|
const [windowLevels, setWindowLevels] = useState([]);
|
||||||
const [cachedHistograms, setCachedHistograms] = useState({});
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const displaySets = useActiveViewportDisplaySets({ servicesManager });
|
||||||
|
|
||||||
/**
|
|
||||||
* Looks for all viewports that has exactly all volumeIds passed as parameter.
|
|
||||||
*/
|
|
||||||
const getViewportsWithVolumeIds = useCallback(
|
const getViewportsWithVolumeIds = useCallback(
|
||||||
(volumeIds: string[]) => {
|
(volumeIds: string[]) => {
|
||||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||||
const viewports = renderingEngine.getVolumeViewports();
|
const viewports = renderingEngine.getVolumeViewports();
|
||||||
|
|
||||||
return viewports.filter(vp => {
|
return viewports.filter(vp => {
|
||||||
const viewportVolumeIds = vp.getActors().map(actor => actor.uid);
|
const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
volumeIds.length === viewportVolumeIds.length &&
|
volumeIds.length === viewportVolumeIds.length &&
|
||||||
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
|
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
|
||||||
@ -38,97 +40,10 @@ const ViewportWindowLevel = ({
|
|||||||
[cornerstoneViewportService]
|
[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 getVolumeOpacity = useCallback((viewport, volumeId) => {
|
||||||
const volumeActor = viewport.getActor(volumeId).actor;
|
const volumeActor = viewport.getActors().find(actor => actor.referencedId === volumeId)?.actor;
|
||||||
|
|
||||||
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
|
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
|
||||||
// Get the opacity from the second node at 0.1
|
|
||||||
return getNodeOpacity(volumeActor, 1);
|
return getNodeOpacity(volumeActor, 1);
|
||||||
} else if (isVolumeWithConstantOpacity(volumeActor)) {
|
} else if (isVolumeWithConstantOpacity(volumeActor)) {
|
||||||
return getNodeOpacity(volumeActor, 0);
|
return getNodeOpacity(volumeActor, 0);
|
||||||
@ -137,85 +52,14 @@ const ViewportWindowLevel = ({
|
|||||||
return undefined;
|
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(() => {
|
const updateViewportHistograms = useCallback(() => {
|
||||||
getWindowLevelsData(viewportId).then(windowLevels => {
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
setWindowLevels(windowLevels);
|
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||||
|
|
||||||
|
getWindowLevelsData(viewport, viewportInfo, getVolumeOpacity).then(data => {
|
||||||
|
setWindowLevels(data);
|
||||||
});
|
});
|
||||||
}, [viewportId, getWindowLevelsData]);
|
}, [viewportId, cornerstoneViewportService, getVolumeOpacity]);
|
||||||
|
|
||||||
const handleCornerstoneVOIModified = useCallback(
|
const handleCornerstoneVOIModified = useCallback(
|
||||||
e => {
|
e => {
|
||||||
@ -252,8 +96,8 @@ const ViewportWindowLevel = ({
|
|||||||
[windowLevels]
|
[windowLevels]
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedHandleCornerstoneVOIModified = useCallback(
|
const debouncedHandleCornerstoneVOIModified = useMemo(
|
||||||
debounce(handleCornerstoneVOIModified, 100),
|
() => debounce(handleCornerstoneVOIModified, 100),
|
||||||
[handleCornerstoneVOIModified]
|
[handleCornerstoneVOIModified]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -280,7 +124,7 @@ const ViewportWindowLevel = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewportVolumeIds = viewport.getActors().map(actor => actor.uid);
|
const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId);
|
||||||
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
|
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
|
||||||
|
|
||||||
viewports.forEach(vp => {
|
viewports.forEach(vp => {
|
||||||
@ -291,49 +135,50 @@ const ViewportWindowLevel = ({
|
|||||||
[getViewportsWithVolumeIds, cornerstoneViewportService]
|
[getViewportsWithVolumeIds, cornerstoneViewportService]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Listen to windowLevels changes and caches all the new ones
|
// New function to handle image volume loading completion
|
||||||
|
const handleImageVolumeLoadingCompleted = useCallback(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
updateViewportHistograms();
|
||||||
|
}, [updateViewportHistograms]);
|
||||||
|
|
||||||
|
// Listen to cornerstone events and set up interval for histogram updates
|
||||||
useEffect(() => {
|
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);
|
document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true);
|
||||||
|
eventTarget.addEventListener(
|
||||||
|
Events.IMAGE_VOLUME_LOADING_COMPLETED,
|
||||||
|
handleImageVolumeLoadingCompleted
|
||||||
|
);
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
updateViewportHistograms();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
eventTarget.removeEventListener(
|
|
||||||
Events.IMAGE_VOLUME_LOADING_COMPLETED,
|
|
||||||
updateViewportHistograms
|
|
||||||
);
|
|
||||||
|
|
||||||
document.removeEventListener(
|
document.removeEventListener(
|
||||||
Events.VOI_MODIFIED,
|
Events.VOI_MODIFIED,
|
||||||
debouncedHandleCornerstoneVOIModified,
|
debouncedHandleCornerstoneVOIModified,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
eventTarget.removeEventListener(
|
||||||
|
Events.IMAGE_VOLUME_LOADING_COMPLETED,
|
||||||
|
handleImageVolumeLoadingCompleted
|
||||||
|
);
|
||||||
|
clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, [updateViewportHistograms, debouncedHandleCornerstoneVOIModified]);
|
}, [
|
||||||
|
updateViewportHistograms,
|
||||||
|
debouncedHandleCornerstoneVOIModified,
|
||||||
|
handleImageVolumeLoadingCompleted,
|
||||||
|
isLoading,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a memoized version of displaySet IDs for comparison
|
||||||
|
const displaySetIds = useMemo(() => {
|
||||||
|
return displaySets?.map(ds => ds.displaySetInstanceUID).sort() || [];
|
||||||
|
}, [displaySets]);
|
||||||
|
|
||||||
// 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(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = cornerstoneViewportService.subscribe(
|
const { unsubscribe } = cornerstoneViewportService.subscribe(
|
||||||
cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED,
|
cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED,
|
||||||
@ -344,10 +189,15 @@ const ViewportWindowLevel = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Only update if displaySets actually changed and are loaded
|
||||||
|
if (displaySetIds.length && !isLoading) {
|
||||||
|
updateViewportHistograms();
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [viewportId, cornerstoneViewportService, updateViewportHistograms]);
|
}, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelSection title="Window Level">
|
<PanelSection title="Window Level">
|
||||||
|
|||||||
@ -20,10 +20,6 @@ const workerFn = () => {
|
|||||||
const getViewportVolumeHistogram = async (viewport, volume, options?) => {
|
const getViewportVolumeHistogram = async (viewport, volume, options?) => {
|
||||||
workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions);
|
workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions);
|
||||||
|
|
||||||
if (!volume?.loadStatus.loaded) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const volumeImageData = viewport.getImageData(volume.volumeId);
|
const volumeImageData = viewport.getImageData(volume.volumeId);
|
||||||
|
|
||||||
if (!volumeImageData) {
|
if (!volumeImageData) {
|
||||||
@ -32,12 +28,15 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
|
|||||||
|
|
||||||
let scalarData = volume.scalarData;
|
let scalarData = volume.scalarData;
|
||||||
|
|
||||||
let prevTimePoint;
|
|
||||||
if (volume.numTimePoints > 1) {
|
if (volume.numTimePoints > 1) {
|
||||||
prevTimePoint = volume.timePointIndex;
|
const targetTimePoint = volume.numTimePoints - 1; // or any other time point you need
|
||||||
const middleTimePoint = Math.round(volume.numTimePoints / 2);
|
scalarData = volume.voxelManager.getTimePointScalarData(targetTimePoint);
|
||||||
volume.timePointIndex = middleTimePoint;
|
} else {
|
||||||
scalarData = volume.getScalarData(middleTimePoint);
|
scalarData = volume.voxelManager.getCompleteScalarDataArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scalarData?.length) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { dimensions, origin, direction, spacing } = volume;
|
const { dimensions, origin, direction, spacing } = volume;
|
||||||
@ -50,11 +49,12 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
|
|||||||
scalarData,
|
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 { minimum: min, maximum: max } = range;
|
||||||
|
|
||||||
|
if (min === Infinity || max === -Infinity) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const calcHistOptions = {
|
const calcHistOptions = {
|
||||||
numBins: 256,
|
numBins: 256,
|
||||||
min: Math.max(min, options?.min ?? min),
|
min: Math.max(min, options?.min ?? min),
|
||||||
|
|||||||
@ -0,0 +1,153 @@
|
|||||||
|
import { cache as cs3DCache, Types } from '@cornerstonejs/core';
|
||||||
|
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
|
||||||
|
import { utilities as csUtils } from '@cornerstonejs/core';
|
||||||
|
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets node opacity from volume actor
|
||||||
|
*/
|
||||||
|
export 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 follows a specific pattern
|
||||||
|
*/
|
||||||
|
export const isPetVolumeWithDefaultOpacity = (volumeId: string, volumeActor) => {
|
||||||
|
const volume = cs3DCache.getVolume(volumeId);
|
||||||
|
|
||||||
|
if (!volume || volume.metadata.Modality !== 'PT') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||||
|
|
||||||
|
if (volumeOpacity.getSize() < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node1Value = [];
|
||||||
|
const node2Value = [];
|
||||||
|
|
||||||
|
volumeOpacity.getNodeValue(0, node1Value);
|
||||||
|
volumeOpacity.getNodeValue(1, node2Value);
|
||||||
|
|
||||||
|
if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedOpacity = node2Value[1];
|
||||||
|
const opacitySize = volumeOpacity.getSize();
|
||||||
|
const currentNodeValue = [];
|
||||||
|
|
||||||
|
for (let i = 2; i < opacitySize; i++) {
|
||||||
|
volumeOpacity.getNodeValue(i, currentNodeValue);
|
||||||
|
if (currentNodeValue[1] !== expectedOpacity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if volume has constant opacity
|
||||||
|
*/
|
||||||
|
export 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets window levels data for a viewport
|
||||||
|
*/
|
||||||
|
export const getWindowLevelsData = async (
|
||||||
|
viewport: Types.IStackViewport | Types.IVolumeViewport,
|
||||||
|
viewportInfo: any,
|
||||||
|
getVolumeOpacity: (viewport: any, volumeId: string) => number | undefined
|
||||||
|
) => {
|
||||||
|
if (!viewport) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeIds = (viewport as Types.IBaseVolumeViewport).getAllVolumeIds();
|
||||||
|
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);
|
||||||
|
|
||||||
|
const opacity = getVolumeOpacity(viewport, volumeId);
|
||||||
|
const { metadata, scaling } = volume;
|
||||||
|
const modality = metadata.Modality;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
min: modality === 'PT' ? 0.1 : -999,
|
||||||
|
max: modality === 'PT' ? 5 : 10000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const histogram = await getViewportVolumeHistogram(viewport, volume, options);
|
||||||
|
|
||||||
|
if (!histogram || histogram.range.min === histogram.range.max) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!viewportInfo.displaySetOptions || !viewportInfo.displaySetOptions[volumeIndex]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: viewportInfo.viewportId,
|
||||||
|
modality,
|
||||||
|
volumeId,
|
||||||
|
volumeIndex,
|
||||||
|
voi,
|
||||||
|
histogram,
|
||||||
|
colormap,
|
||||||
|
step: scaling?.PT ? 0.05 : 1,
|
||||||
|
opacity,
|
||||||
|
showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return windowLevels.filter(Boolean);
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
||||||
import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui';
|
import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui';
|
||||||
import { StackViewport } from '@cornerstonejs/core';
|
import { StackViewport, Types } from '@cornerstonejs/core';
|
||||||
import { ColormapProps } from '../../types/Colormap';
|
import { ColormapProps } from '../../types/Colormap';
|
||||||
|
|
||||||
export function Colormap({
|
export function Colormap({
|
||||||
@ -54,8 +54,10 @@ export function Colormap({
|
|||||||
return colormap;
|
return colormap;
|
||||||
}
|
}
|
||||||
const actorEntries = viewport.getActors();
|
const actorEntries = viewport.getActors();
|
||||||
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
|
const actorEntry = actorEntries?.find(entry =>
|
||||||
const { colormap } = viewport.getProperties(actorEntry.uid);
|
entry.referencedId.includes(displaySetInstanceUID)
|
||||||
|
);
|
||||||
|
const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId);
|
||||||
if (!colormap) {
|
if (!colormap) {
|
||||||
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
|
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,12 +15,12 @@ import { ViewportPreset } from '../../types/ViewportPresets';
|
|||||||
import { VolumeViewport3D } from '@cornerstonejs/core';
|
import { VolumeViewport3D } from '@cornerstonejs/core';
|
||||||
import { utilities } from '@cornerstonejs/core';
|
import { utilities } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
|
||||||
|
|
||||||
export type WindowLevelActionMenuProps = {
|
export type WindowLevelActionMenuProps = {
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
||||||
verticalDirection: AllInOneMenu.VerticalDirection;
|
|
||||||
horizontalDirection: AllInOneMenu.HorizontalDirection;
|
|
||||||
colorbarProperties: ColorbarProperties;
|
colorbarProperties: ColorbarProperties;
|
||||||
displaySets: Array<any>;
|
displaySets: Array<any>;
|
||||||
volumeRenderingPresets: Array<ViewportPreset>;
|
volumeRenderingPresets: Array<ViewportPreset>;
|
||||||
@ -53,8 +53,6 @@ export function WindowLevelActionMenu({
|
|||||||
const backgroundColor = viewportInfo.getViewportOptions().background;
|
const backgroundColor = viewportInfo.getViewportOptions().background;
|
||||||
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
|
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
|
||||||
|
|
||||||
const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
|
|
||||||
|
|
||||||
const { t } = useTranslation('WindowLevelActionMenu');
|
const { t } = useTranslation('WindowLevelActionMenu');
|
||||||
|
|
||||||
const [viewportGrid] = useViewportGrid();
|
const [viewportGrid] = useViewportGrid();
|
||||||
@ -119,7 +117,7 @@ export function WindowLevelActionMenu({
|
|||||||
horizontalDirection={horizontalDirection}
|
horizontalDirection={horizontalDirection}
|
||||||
iconClassName={classNames(
|
iconClassName={classNames(
|
||||||
// Visible on hover and for the active viewport
|
// Visible on hover and for the active viewport
|
||||||
activeViewportId === viewportId ? 'visible' : 'invisible group-hover:visible',
|
activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible',
|
||||||
'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light',
|
'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light',
|
||||||
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60'
|
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60'
|
||||||
)}
|
)}
|
||||||
@ -133,7 +131,7 @@ export function WindowLevelActionMenu({
|
|||||||
{!is3DVolume && (
|
{!is3DVolume && (
|
||||||
<Colorbar
|
<Colorbar
|
||||||
viewportId={viewportId}
|
viewportId={viewportId}
|
||||||
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
|
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
colorbarProperties={colorbarProperties}
|
colorbarProperties={colorbarProperties}
|
||||||
@ -149,7 +147,7 @@ export function WindowLevelActionMenu({
|
|||||||
<Colormap
|
<Colormap
|
||||||
colormaps={colormaps}
|
colormaps={colormaps}
|
||||||
viewportId={viewportId}
|
viewportId={viewportId}
|
||||||
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
|
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
|
import { nonWLModalities, WindowLevelActionMenu } from './WindowLevelActionMenu';
|
||||||
|
|
||||||
export function getWindowLevelActionMenu({
|
export function getWindowLevelActionMenu({
|
||||||
viewportId,
|
viewportId,
|
||||||
@ -9,7 +9,11 @@ export function getWindowLevelActionMenu({
|
|||||||
commandsManager,
|
commandsManager,
|
||||||
verticalDirection,
|
verticalDirection,
|
||||||
horizontalDirection,
|
horizontalDirection,
|
||||||
}: withAppTypes): ReactNode {
|
}: withAppTypes<{
|
||||||
|
viewportId: string;
|
||||||
|
element: HTMLElement;
|
||||||
|
displaySets: AppTypes.DisplaySet[];
|
||||||
|
}>): ReactNode {
|
||||||
const { customizationService } = servicesManager.services;
|
const { customizationService } = servicesManager.services;
|
||||||
|
|
||||||
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
|
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
|
||||||
@ -24,6 +28,14 @@ export function getWindowLevelActionMenu({
|
|||||||
return { [displaySet.Modality]: presets[displaySet.Modality] };
|
return { [displaySet.Modality]: presets[displaySet.Modality] };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const modalities = displaySets
|
||||||
|
.map(displaySet => displaySet.Modality)
|
||||||
|
.filter(modality => !nonWLModalities.includes(modality));
|
||||||
|
|
||||||
|
if (modalities.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WindowLevelActionMenu
|
<WindowLevelActionMenu
|
||||||
viewportId={viewportId}
|
viewportId={viewportId}
|
||||||
|
|||||||
@ -35,8 +35,8 @@ export const ViewportActionCornersContext = createContext(DEFAULT_STATE);
|
|||||||
export function ViewportActionCornersProvider({ children, service }) {
|
export function ViewportActionCornersProvider({ children, service }) {
|
||||||
const viewportActionCornersReducer = (state, action) => {
|
const viewportActionCornersReducer = (state, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_ACTION_COMPONENT': {
|
case 'ADD_ACTION_COMPONENT': {
|
||||||
const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
|
const { viewportId, id, component, location, indexPriority } = action.payload;
|
||||||
// Get the components at the specified location of the specified viewport.
|
// Get the components at the specified location of the specified viewport.
|
||||||
let locationComponents = state?.[viewportId]?.[location]
|
let locationComponents = state?.[viewportId]?.[location]
|
||||||
? [...state[viewportId][location]]
|
? [...state[viewportId][location]]
|
||||||
@ -56,27 +56,48 @@ export function ViewportActionCornersProvider({ children, service }) {
|
|||||||
// Insert the component from the payload but
|
// Insert the component from the payload but
|
||||||
// do not insert an undefined or null component.
|
// do not insert an undefined or null component.
|
||||||
if (component) {
|
if (component) {
|
||||||
const insertionIndex = locationComponents.findIndex(
|
let insertionIndex;
|
||||||
component => indexPriority <= component.indexPriority
|
const isRightSide =
|
||||||
);
|
location === ViewportActionCornersLocations.topRight ||
|
||||||
|
location === ViewportActionCornersLocations.bottomRight;
|
||||||
|
|
||||||
|
if (indexPriority === undefined) {
|
||||||
|
// If no indexPriority is provided, add it to the appropriate end
|
||||||
|
insertionIndex = isRightSide ? 0 : locationComponents.length;
|
||||||
|
} else {
|
||||||
|
if (isRightSide) {
|
||||||
|
insertionIndex = locationComponents.findIndex(
|
||||||
|
component => indexPriority > component.indexPriority
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
insertionIndex = locationComponents.findIndex(
|
||||||
|
component => indexPriority <= component.indexPriority
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (insertionIndex === -1) {
|
||||||
|
// If no suitable position found, add to the appropriate end
|
||||||
|
insertionIndex = isRightSide ? 0 : locationComponents.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPriority = isRightSide ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
locationComponents = [
|
locationComponents = [
|
||||||
...locationComponents.slice(0, insertionIndex),
|
...locationComponents.slice(0, insertionIndex),
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
component,
|
component,
|
||||||
indexPriority,
|
indexPriority: indexPriority ?? defaultPriority,
|
||||||
},
|
},
|
||||||
...locationComponents.slice(insertionIndex + 1),
|
...locationComponents.slice(insertionIndex),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...{
|
[viewportId]: {
|
||||||
[viewportId]: {
|
...state[viewportId],
|
||||||
...state[viewportId],
|
[location]: locationComponents,
|
||||||
[location]: locationComponents,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -100,17 +121,17 @@ export function ViewportActionCornersProvider({ children, service }) {
|
|||||||
return viewportActionCornersState;
|
return viewportActionCornersState;
|
||||||
}, [viewportActionCornersState]);
|
}, [viewportActionCornersState]);
|
||||||
|
|
||||||
const setComponent = useCallback(
|
const addComponent = useCallback(
|
||||||
(actionComponentInfo: ActionComponentInfo) => {
|
(actionComponentInfo: ActionComponentInfo) => {
|
||||||
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo });
|
dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo });
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setComponents = useCallback(
|
const addComponents = useCallback(
|
||||||
(actionComponentInfos: Array<ActionComponentInfo>) => {
|
(actionComponentInfos: Array<ActionComponentInfo>) => {
|
||||||
actionComponentInfos.forEach(actionComponentInfo =>
|
actionComponentInfos.forEach(actionComponentInfo =>
|
||||||
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo })
|
dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo })
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
@ -120,28 +141,28 @@ export function ViewportActionCornersProvider({ children, service }) {
|
|||||||
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
|
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (service) {
|
if (service) {
|
||||||
service.setServiceImplementation({
|
service.setServiceImplementation({
|
||||||
getState,
|
getState,
|
||||||
setComponent,
|
addComponent,
|
||||||
setComponents,
|
addComponents,
|
||||||
clear,
|
clear,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [getState, service, setComponent, setComponents]);
|
}, [getState, service, addComponent, addComponents, clear]);
|
||||||
|
|
||||||
// run many of the calls through the service itself since we want to publish events
|
const viewportCornerActions = {
|
||||||
const api = {
|
|
||||||
getState,
|
getState,
|
||||||
setComponent: props => service.setComponent(props),
|
addComponent: props => service.addComponent(props),
|
||||||
setComponents: props => service.setComponents(props),
|
addComponents: props => service.addComponents(props),
|
||||||
clear: props => service.clear(props),
|
clear: props => service.clear(props),
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextValue = useMemo(
|
const contextValue = useMemo(
|
||||||
() => [viewportActionCornersState, api],
|
() => [viewportActionCornersState, viewportCornerActions],
|
||||||
[viewportActionCornersState, api]
|
[viewportActionCornersState, viewportCornerActions]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
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 defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
|
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
|
||||||
import { colormaps } from './utils/colormaps';
|
import { colormaps } from './utils/colormaps';
|
||||||
import { CONSTANTS } from '@cornerstonejs/core';
|
import { CONSTANTS } from '@cornerstonejs/core';
|
||||||
@ -23,10 +22,12 @@ const tools = {
|
|||||||
toolName: toolNames.Zoom,
|
toolName: toolNames.Zoom,
|
||||||
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
|
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
|
||||||
},
|
},
|
||||||
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] },
|
{
|
||||||
|
toolName: toolNames.StackScroll,
|
||||||
|
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
enabled: [
|
enabled: [
|
||||||
{ toolName: toolNames.SegmentationDisplay },
|
|
||||||
{
|
{
|
||||||
toolName: toolNames.PlanarFreehandContourSegmentation,
|
toolName: toolNames.PlanarFreehandContourSegmentation,
|
||||||
configuration: {
|
configuration: {
|
||||||
@ -38,13 +39,6 @@ const tools = {
|
|||||||
|
|
||||||
function getCustomizationModule() {
|
function getCustomizationModule() {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
name: 'cornerstoneDicomUploadComponent',
|
|
||||||
value: {
|
|
||||||
id: 'dicomUploadComponent',
|
|
||||||
component: DicomUpload,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'default',
|
name: 'default',
|
||||||
value: [
|
value: [
|
||||||
@ -120,7 +114,7 @@ function getCustomizationModule() {
|
|||||||
type: 'value',
|
type: 'value',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'areaUnit',
|
value: 'areaUnits',
|
||||||
for: ['area'],
|
for: ['area'],
|
||||||
type: 'unit',
|
type: 'unit',
|
||||||
},
|
},
|
||||||
@ -140,7 +134,7 @@ function getCustomizationModule() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Unit',
|
displayName: 'Unit',
|
||||||
value: 'areaUnit',
|
value: 'areaUnits',
|
||||||
type: 'value',
|
type: 'value',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -170,12 +164,12 @@ function getCustomizationModule() {
|
|||||||
type: 'value',
|
type: 'value',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'modalityUnit',
|
value: 'pixelValueUnits',
|
||||||
for: ['mean', 'max' /** 'stdDev **/],
|
for: ['mean', 'max' /** 'stdDev **/],
|
||||||
type: 'unit',
|
type: 'unit',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'areaUnit',
|
value: 'areaUnits',
|
||||||
for: ['area'],
|
for: ['area'],
|
||||||
type: 'unit',
|
type: 'unit',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useAppConfig } from '@state';
|
|
||||||
import { Toolbox } from '@ohif/ui-next';
|
import { Toolbox } from '@ohif/ui-next';
|
||||||
import PanelSegmentation from './panels/PanelSegmentation';
|
import PanelSegmentation from './panels/PanelSegmentation';
|
||||||
|
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
||||||
|
|
||||||
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
|
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
|
||||||
const { customizationService } = servicesManager.services;
|
|
||||||
|
|
||||||
const wrappedPanelSegmentation = ({ configuration }) => {
|
const wrappedPanelSegmentation = ({ configuration }) => {
|
||||||
const [appConfig] = useAppConfig();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelSegmentation
|
<PanelSegmentation
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
@ -17,16 +13,25 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
|||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
configuration={{
|
configuration={{
|
||||||
...configuration,
|
...configuration,
|
||||||
disableEditing: appConfig.disableEditing,
|
}}
|
||||||
...customizationService.get('segmentation.panel'),
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrappedPanelSegmentationNoHeader = ({ configuration }) => {
|
||||||
|
return (
|
||||||
|
<PanelSegmentation
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
extensionManager={extensionManager}
|
||||||
|
configuration={{
|
||||||
|
...configuration,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrappedPanelSegmentationWithTools = ({ configuration }) => {
|
const wrappedPanelSegmentationWithTools = ({ configuration }) => {
|
||||||
const [appConfig] = useAppConfig();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbox
|
<Toolbox
|
||||||
@ -45,8 +50,6 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
|||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
configuration={{
|
configuration={{
|
||||||
...configuration,
|
...configuration,
|
||||||
disableEditing: appConfig.disableEditing,
|
|
||||||
...customizationService.get('segmentation.panel'),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@ -54,6 +57,12 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
name: 'activeViewportWindowLevel',
|
||||||
|
component: () => {
|
||||||
|
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'panelSegmentation',
|
name: 'panelSegmentation',
|
||||||
iconName: 'tab-segmentation',
|
iconName: 'tab-segmentation',
|
||||||
@ -61,6 +70,13 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
|||||||
label: 'Segmentation',
|
label: 'Segmentation',
|
||||||
component: wrappedPanelSegmentation,
|
component: wrappedPanelSegmentation,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'panelSegmentationNoHeader',
|
||||||
|
iconName: 'tab-segmentation',
|
||||||
|
iconLabel: 'Segmentation',
|
||||||
|
label: 'Segmentation',
|
||||||
|
component: wrappedPanelSegmentationNoHeader,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'panelSegmentationWithTools',
|
name: 'panelSegmentationWithTools',
|
||||||
iconName: 'tab-segmentation',
|
iconName: 'tab-segmentation',
|
||||||
@ -89,7 +89,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
|||||||
const displaySet = {
|
const displaySet = {
|
||||||
plugin: 'microscopy',
|
plugin: 'microscopy',
|
||||||
Modality: 'SM',
|
Modality: 'SM',
|
||||||
viewportType: csEnums.ViewportType.WholeSlide,
|
viewportType: csEnums.ViewportType.WHOLE_SLIDE,
|
||||||
altImageText: 'Microscopy',
|
altImageText: 'Microscopy',
|
||||||
displaySetInstanceUID: utils.guid(),
|
displaySetInstanceUID: utils.guid(),
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
|
|||||||
@ -6,6 +6,12 @@ const getToggledClassName = (isToggled: boolean) => {
|
|||||||
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
|
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDisabledState = (disabledText?: string) => ({
|
||||||
|
disabled: true,
|
||||||
|
className: '!text-common-bright ohif-disabled',
|
||||||
|
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||||
|
});
|
||||||
|
|
||||||
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
|
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
|
||||||
const {
|
const {
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
@ -20,6 +26,52 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
|||||||
return [
|
return [
|
||||||
// functions/helpers to be used by the toolbar buttons to decide if they should
|
// functions/helpers to be used by the toolbar buttons to decide if they should
|
||||||
// enabled or not
|
// enabled or not
|
||||||
|
{
|
||||||
|
name: 'evaluate.viewport.supported',
|
||||||
|
evaluate: ({ viewportId, unsupportedViewportTypes, disabledText }) => {
|
||||||
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
|
if (viewport && unsupportedViewportTypes?.includes(viewport.type)) {
|
||||||
|
return getDisabledState(disabledText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'evaluate.modality.supported',
|
||||||
|
evaluate: ({ viewportId, unsupportedModalities, supportedModalities, disabledText }) => {
|
||||||
|
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||||
|
|
||||||
|
if (!displaySetUIDs?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
|
||||||
|
|
||||||
|
// Check for unsupported modalities (exclusion)
|
||||||
|
if (unsupportedModalities?.length) {
|
||||||
|
const hasUnsupportedModality = displaySets.some(displaySet =>
|
||||||
|
unsupportedModalities.includes(displaySet?.Modality)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasUnsupportedModality) {
|
||||||
|
return getDisabledState(disabledText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for supported modalities (inclusion)
|
||||||
|
if (supportedModalities?.length) {
|
||||||
|
const hasAnySupportedModality = displaySets.some(displaySet =>
|
||||||
|
supportedModalities.includes(displaySet?.Modality)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasAnySupportedModality) {
|
||||||
|
return getDisabledState(disabledText || 'Tool not available for this modality');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'evaluate.cornerstoneTool',
|
name: 'evaluate.cornerstoneTool',
|
||||||
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
|
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
|
||||||
@ -32,11 +84,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
|||||||
const toolName = toolbarService.getToolNameForButton(button);
|
const toolName = toolbarService.getToolNameForButton(button);
|
||||||
|
|
||||||
if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) {
|
if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) {
|
||||||
return {
|
return getDisabledState(disabledText);
|
||||||
disabled: true,
|
|
||||||
className: '!text-common-bright ohif-disabled',
|
|
||||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPrimaryActive = toolNames
|
const isPrimaryActive = toolNames
|
||||||
@ -172,40 +220,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
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',
|
name: 'evaluate.viewportProperties.toggle',
|
||||||
evaluate: ({ viewportId, button }) => {
|
evaluate: ({ viewportId, button }) => {
|
||||||
@ -254,11 +268,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!areReconstructable) {
|
if (!areReconstructable) {
|
||||||
return {
|
return getDisabledState(disabledText);
|
||||||
disabled: true,
|
|
||||||
className: '!text-common-bright ohif-disabled',
|
|
||||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMpr = protocol?.id === 'mpr';
|
const isMpr = protocol?.id === 'mpr';
|
||||||
@ -288,11 +298,7 @@ function _evaluateToggle({
|
|||||||
const toolName = toolbarService.getToolNameForButton(button);
|
const toolName = toolbarService.getToolNameForButton(button);
|
||||||
|
|
||||||
if (!toolGroup.hasTool(toolName)) {
|
if (!toolGroup.hasTool(toolName)) {
|
||||||
return {
|
return getDisabledState(disabledText);
|
||||||
disabled: true,
|
|
||||||
className: '!text-common-bright ohif-disabled',
|
|
||||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
|
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
|
||||||
|
|||||||
@ -0,0 +1,219 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
import { roundNumber } from '@ohif/core/src/utils';
|
||||||
|
import {
|
||||||
|
SegmentationData,
|
||||||
|
SegmentationRepresentation,
|
||||||
|
} from '../services/SegmentationService/SegmentationService';
|
||||||
|
|
||||||
|
const excludedModalities = ['SM', 'OT', 'DOC', 'ECG'];
|
||||||
|
|
||||||
|
function mapSegmentationToDisplay(segmentation, customizationService) {
|
||||||
|
const { label, segments } = segmentation;
|
||||||
|
|
||||||
|
// Get the readable text mapping once
|
||||||
|
const { readableText: readableTextMap } = customizationService.getCustomization(
|
||||||
|
'PanelSegmentation.readableText',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper function to recursively map cachedStats to readable display text
|
||||||
|
function mapStatsToDisplay(stats, indent = 0) {
|
||||||
|
const primary = [];
|
||||||
|
const indentation = ' '.repeat(indent);
|
||||||
|
|
||||||
|
for (const key in stats) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(stats, key)) {
|
||||||
|
const value = stats[key];
|
||||||
|
const readableText = readableTextMap?.[key];
|
||||||
|
|
||||||
|
if (!readableText) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||||
|
// Add empty row before category (except for the first category)
|
||||||
|
if (primary.length > 0) {
|
||||||
|
primary.push('');
|
||||||
|
}
|
||||||
|
// Add category title
|
||||||
|
primary.push(`${indentation}${readableText}`);
|
||||||
|
// Recursively handle nested objects
|
||||||
|
primary.push(...mapStatsToDisplay(value, indent + 1));
|
||||||
|
} else {
|
||||||
|
// For non-nested values, don't add empty rows
|
||||||
|
primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get customization for display text mapping
|
||||||
|
const displayTextMapper = segment => {
|
||||||
|
const defaultDisplay = {
|
||||||
|
primary: [],
|
||||||
|
secondary: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the segment has cachedStats, map it to readable text
|
||||||
|
if (segment.cachedStats) {
|
||||||
|
const primary = mapStatsToDisplay(segment.cachedStats);
|
||||||
|
defaultDisplay.primary = primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultDisplay;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSegments = {};
|
||||||
|
|
||||||
|
Object.entries(segments).forEach(([segmentIndex, segment]) => {
|
||||||
|
updatedSegments[segmentIndex] = {
|
||||||
|
...segment,
|
||||||
|
displayText: displayTextMapper(segment),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map the segments and apply the display text mapper
|
||||||
|
return {
|
||||||
|
...segmentation,
|
||||||
|
label,
|
||||||
|
segments: updatedSegments,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the combination of segmentation data and its representation in a viewport.
|
||||||
|
*/
|
||||||
|
type ViewportSegmentationRepresentation = {
|
||||||
|
segmentationsWithRepresentations: {
|
||||||
|
representation: SegmentationRepresentation;
|
||||||
|
segmentation: SegmentationData;
|
||||||
|
}[];
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook that provides segmentation data and their representations for the active viewport.
|
||||||
|
* @param options - The options object.
|
||||||
|
* @param options.servicesManager - The services manager object.
|
||||||
|
* @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
|
||||||
|
* @param options.debounceTime - Debounce time in milliseconds for updates.
|
||||||
|
* @returns An array of segmentation data and their representations for the active viewport.
|
||||||
|
*/
|
||||||
|
export function useActiveViewportSegmentationRepresentations({
|
||||||
|
servicesManager,
|
||||||
|
subscribeToDataModified = false,
|
||||||
|
debounceTime = 0,
|
||||||
|
}: withAppTypes<{ debounceTime?: number }>): ViewportSegmentationRepresentation {
|
||||||
|
const { segmentationService, viewportGridService, customizationService, displaySetService } =
|
||||||
|
servicesManager.services;
|
||||||
|
|
||||||
|
const [segmentationsWithRepresentations, setSegmentationsWithRepresentations] =
|
||||||
|
useState<ViewportSegmentationRepresentation>({
|
||||||
|
segmentationsWithRepresentations: [],
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const update = () => {
|
||||||
|
const viewportId = viewportGridService.getActiveViewportId();
|
||||||
|
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||||
|
|
||||||
|
if (!displaySetUIDs?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
|
||||||
|
|
||||||
|
if (!displaySet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludedModalities.includes(displaySet.Modality)) {
|
||||||
|
setSegmentationsWithRepresentations(prev => ({
|
||||||
|
segmentationsWithRepresentations: [],
|
||||||
|
disabled: true,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
|
||||||
|
if (!segmentations?.length) {
|
||||||
|
setSegmentationsWithRepresentations(prev => ({
|
||||||
|
segmentationsWithRepresentations: [],
|
||||||
|
disabled: false,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const representations = segmentationService.getSegmentationRepresentations(viewportId);
|
||||||
|
|
||||||
|
const newSegmentationsWithRepresentations = representations.map(representation => {
|
||||||
|
const segmentation = segmentationService.getSegmentation(representation.segmentationId);
|
||||||
|
const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService);
|
||||||
|
return {
|
||||||
|
representation,
|
||||||
|
segmentation: mappedSegmentation,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setSegmentationsWithRepresentations({
|
||||||
|
segmentationsWithRepresentations: newSegmentationsWithRepresentations,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedUpdate =
|
||||||
|
debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
const subscriptions = [
|
||||||
|
segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||||
|
debouncedUpdate
|
||||||
|
),
|
||||||
|
segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||||
|
debouncedUpdate
|
||||||
|
),
|
||||||
|
segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
|
||||||
|
debouncedUpdate
|
||||||
|
),
|
||||||
|
viewportGridService.subscribe(
|
||||||
|
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||||
|
debouncedUpdate
|
||||||
|
),
|
||||||
|
viewportGridService.subscribe(viewportGridService.EVENTS.GRID_STATE_CHANGED, debouncedUpdate),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (subscribeToDataModified) {
|
||||||
|
subscriptions.push(
|
||||||
|
segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
|
||||||
|
debouncedUpdate
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
if (debounceTime > 0) {
|
||||||
|
debouncedUpdate.cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
segmentationService,
|
||||||
|
viewportGridService,
|
||||||
|
customizationService,
|
||||||
|
displaySetService,
|
||||||
|
debounceTime,
|
||||||
|
subscribeToDataModified,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return segmentationsWithRepresentations;
|
||||||
|
}
|
||||||
103
extensions/cornerstone/src/hooks/useMeasurements.ts
Normal file
103
extensions/cornerstone/src/hooks/useMeasurements.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
|
||||||
|
function mapMeasurementToDisplay(measurement, displaySetService) {
|
||||||
|
const { referenceSeriesUID } = measurement;
|
||||||
|
|
||||||
|
const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID);
|
||||||
|
|
||||||
|
if (!displaySets[0]?.instances) {
|
||||||
|
throw new Error('The tracked measurements panel should only be tracking "stack" displaySets.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { findingSites, finding, label: baseLabel, displayText: baseDisplayText } = measurement;
|
||||||
|
|
||||||
|
const firstSite = findingSites?.[0];
|
||||||
|
const label = baseLabel || finding?.text || firstSite?.text || '(empty)';
|
||||||
|
|
||||||
|
// Initialize displayText with the structure used in Length.ts and CobbAngle.ts
|
||||||
|
const displayText = {
|
||||||
|
primary: [],
|
||||||
|
secondary: baseDisplayText?.secondary || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add baseDisplayText to primary if it exists
|
||||||
|
if (baseDisplayText) {
|
||||||
|
displayText.primary.push(...baseDisplayText.primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add finding sites to primary
|
||||||
|
if (findingSites) {
|
||||||
|
findingSites.forEach(site => {
|
||||||
|
if (site?.text && site.text !== label) {
|
||||||
|
displayText.primary.push(site.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add finding to primary if it's different from the label
|
||||||
|
if (finding && finding.text && finding.text !== label) {
|
||||||
|
displayText.primary.push(finding.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...measurement,
|
||||||
|
displayText,
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom hook that provides mapped measurements based on the given services and filters.
|
||||||
|
*
|
||||||
|
* @param {Object} servicesManager - The services manager object.
|
||||||
|
* @param {Object} options - The options for filtering and mapping measurements.
|
||||||
|
* @param {Function} options.measurementFilter - Optional function to filter measurements.
|
||||||
|
* @param {Object} options.valueTypes - The value types for mapping measurements.
|
||||||
|
* @returns {Array} An array of mapped and filtered measurements.
|
||||||
|
*/
|
||||||
|
export function useMeasurements(servicesManager, { measurementFilter }) {
|
||||||
|
const { measurementService, displaySetService } = servicesManager.services;
|
||||||
|
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateDisplayMeasurements = () => {
|
||||||
|
let measurements = measurementService.getMeasurements();
|
||||||
|
if (measurementFilter) {
|
||||||
|
measurements = measurements.filter(measurementFilter);
|
||||||
|
}
|
||||||
|
const mappedMeasurements = measurements.map(m =>
|
||||||
|
mapMeasurementToDisplay(m, displaySetService)
|
||||||
|
);
|
||||||
|
setDisplayMeasurements(prevMeasurements => {
|
||||||
|
if (JSON.stringify(prevMeasurements) !== JSON.stringify(mappedMeasurements)) {
|
||||||
|
return mappedMeasurements;
|
||||||
|
}
|
||||||
|
return prevMeasurements;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedUpdate = debounce(updateDisplayMeasurements, 100);
|
||||||
|
|
||||||
|
updateDisplayMeasurements();
|
||||||
|
|
||||||
|
const events = [
|
||||||
|
measurementService.EVENTS.MEASUREMENT_ADDED,
|
||||||
|
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
|
||||||
|
measurementService.EVENTS.MEASUREMENT_UPDATED,
|
||||||
|
measurementService.EVENTS.MEASUREMENT_REMOVED,
|
||||||
|
measurementService.EVENTS.MEASUREMENTS_CLEARED,
|
||||||
|
];
|
||||||
|
|
||||||
|
const subscriptions = events.map(
|
||||||
|
evt => measurementService.subscribe(evt, debouncedUpdate).unsubscribe
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(unsub => unsub());
|
||||||
|
debouncedUpdate.cancel();
|
||||||
|
};
|
||||||
|
}, [measurementService, measurementFilter, displaySetService]);
|
||||||
|
|
||||||
|
return displayMeasurements;
|
||||||
|
}
|
||||||
148
extensions/cornerstone/src/hooks/useSegmentations.ts
Normal file
148
extensions/cornerstone/src/hooks/useSegmentations.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
import { roundNumber } from '@ohif/core/src/utils';
|
||||||
|
import { SegmentationData } from '../services/SegmentationService/SegmentationService';
|
||||||
|
|
||||||
|
function mapSegmentationToDisplay(segmentation, customizationService) {
|
||||||
|
const { label, segments } = segmentation;
|
||||||
|
|
||||||
|
// Get the readable text mapping once
|
||||||
|
const { readableText: readableTextMap } = customizationService.getCustomization(
|
||||||
|
'PanelSegmentation.readableText',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper function to recursively map cachedStats to readable display text
|
||||||
|
function mapStatsToDisplay(stats, indent = 0) {
|
||||||
|
const primary = [];
|
||||||
|
const indentation = ' '.repeat(indent);
|
||||||
|
|
||||||
|
for (const key in stats) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(stats, key)) {
|
||||||
|
const value = stats[key];
|
||||||
|
const readableText = readableTextMap?.[key];
|
||||||
|
|
||||||
|
if (!readableText) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||||
|
// Add empty row before category (except for the first category)
|
||||||
|
if (primary.length > 0) {
|
||||||
|
primary.push('');
|
||||||
|
}
|
||||||
|
// Add category title
|
||||||
|
primary.push(`${indentation}${readableText}`);
|
||||||
|
// Recursively handle nested objects
|
||||||
|
primary.push(...mapStatsToDisplay(value, indent + 1));
|
||||||
|
} else {
|
||||||
|
// For non-nested values, don't add empty rows
|
||||||
|
primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get customization for display text mapping
|
||||||
|
const displayTextMapper = segment => {
|
||||||
|
const defaultDisplay = {
|
||||||
|
primary: [],
|
||||||
|
secondary: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the segment has cachedStats, map it to readable text
|
||||||
|
if (segment.cachedStats) {
|
||||||
|
const primary = mapStatsToDisplay(segment.cachedStats);
|
||||||
|
defaultDisplay.primary = primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultDisplay;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSegments = {};
|
||||||
|
|
||||||
|
Object.entries(segments).forEach(([segmentIndex, segment]) => {
|
||||||
|
updatedSegments[segmentIndex] = {
|
||||||
|
...segment,
|
||||||
|
displayText: displayTextMapper(segment),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map the segments and apply the display text mapper
|
||||||
|
return {
|
||||||
|
...segmentation,
|
||||||
|
label,
|
||||||
|
segments: updatedSegments,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook that provides segmentation data.
|
||||||
|
* @param options - The options object.
|
||||||
|
* @param options.servicesManager - The services manager object.
|
||||||
|
* @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
|
||||||
|
* @param options.debounceTime - Debounce time in milliseconds for updates.
|
||||||
|
* @returns An array of segmentation data.
|
||||||
|
*/
|
||||||
|
export function useSegmentations({
|
||||||
|
servicesManager,
|
||||||
|
subscribeToDataModified = false,
|
||||||
|
debounceTime = 0,
|
||||||
|
}: withAppTypes<{ debounceTime?: number }>): SegmentationData[] {
|
||||||
|
const { segmentationService, customizationService } = servicesManager.services;
|
||||||
|
|
||||||
|
const [segmentations, setSegmentations] = useState<SegmentationData[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const update = () => {
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
|
||||||
|
if (!segmentations?.length) {
|
||||||
|
setSegmentations([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedSegmentations = segmentations.map(segmentation =>
|
||||||
|
mapSegmentationToDisplay(segmentation, customizationService)
|
||||||
|
);
|
||||||
|
|
||||||
|
setSegmentations(mappedSegmentations);
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedUpdate =
|
||||||
|
debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
const subscriptions = [
|
||||||
|
segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||||
|
debouncedUpdate
|
||||||
|
),
|
||||||
|
segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||||
|
debouncedUpdate
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (subscribeToDataModified) {
|
||||||
|
subscriptions.push(
|
||||||
|
segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
|
||||||
|
debouncedUpdate
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
if (debounceTime > 0) {
|
||||||
|
debouncedUpdate.cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [segmentationService, customizationService, debounceTime, subscribeToDataModified]);
|
||||||
|
|
||||||
|
return segmentations;
|
||||||
|
}
|
||||||
@ -1,5 +1,25 @@
|
|||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
|
const VOI_SYNC_GROUP = {
|
||||||
|
type: 'voi',
|
||||||
|
id: 'mpr',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const HYDRATE_SEG_SYNC_GROUP = {
|
||||||
|
type: 'hydrateseg',
|
||||||
|
id: 'sameFORId',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
options: {
|
||||||
|
matchingRules: ['sameFOR'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const mpr: Types.HangingProtocol.Protocol = {
|
export const mpr: Types.HangingProtocol.Protocol = {
|
||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
name: 'MPR',
|
name: 'MPR',
|
||||||
@ -10,7 +30,6 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
|||||||
modifiedDate: '2023-08-15',
|
modifiedDate: '2023-08-15',
|
||||||
availableTo: {},
|
availableTo: {},
|
||||||
editableBy: {},
|
editableBy: {},
|
||||||
// Unknown number of priors referenced - so just match any study
|
|
||||||
numberOfPriorsReferenced: 0,
|
numberOfPriorsReferenced: 0,
|
||||||
protocolMatchingRules: [],
|
protocolMatchingRules: [],
|
||||||
imageLoadStrategy: 'nth',
|
imageLoadStrategy: 'nth',
|
||||||
@ -71,17 +90,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
|||||||
initialImageOptions: {
|
initialImageOptions: {
|
||||||
preset: 'middle',
|
preset: 'middle',
|
||||||
},
|
},
|
||||||
syncGroups: [
|
syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'mpr',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
options: {
|
|
||||||
syncColormap: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
@ -98,17 +107,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
|||||||
initialImageOptions: {
|
initialImageOptions: {
|
||||||
preset: 'middle',
|
preset: 'middle',
|
||||||
},
|
},
|
||||||
syncGroups: [
|
syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'mpr',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
options: {
|
|
||||||
syncColormap: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
@ -125,17 +124,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
|||||||
initialImageOptions: {
|
initialImageOptions: {
|
||||||
preset: 'middle',
|
preset: 'middle',
|
||||||
},
|
},
|
||||||
syncGroups: [
|
syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'mpr',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
options: {
|
|
||||||
syncColormap: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
imageLoadPoolManager,
|
imageLoadPoolManager,
|
||||||
imageRetrievalPoolManager,
|
imageRetrievalPoolManager,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import * as csStreamingImageVolumeLoader from '@cornerstonejs/streaming-image-volume-loader';
|
|
||||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
import Enums from './enums';
|
import Enums from './enums';
|
||||||
@ -33,17 +32,28 @@ import { id } from './id';
|
|||||||
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||||
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||||
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||||
import { showLabelAnnotationPopup } from './utils/callInputDialog';
|
|
||||||
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
||||||
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
|
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
|
||||||
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
|
||||||
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||||
import { findNearbyToolData } from './utils/findNearbyToolData';
|
import { findNearbyToolData } from './utils/findNearbyToolData';
|
||||||
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
|
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
|
||||||
import { getSopClassHandlerModule } from './getSopClassHandlerModule';
|
import { getSopClassHandlerModule } from './getSopClassHandlerModule';
|
||||||
|
import { getDynamicVolumeInfo } from '@cornerstonejs/core/utilities';
|
||||||
|
import {
|
||||||
|
useLutPresentationStore,
|
||||||
|
usePositionPresentationStore,
|
||||||
|
useSegmentationPresentationStore,
|
||||||
|
useSynchronizersStore,
|
||||||
|
} from './stores';
|
||||||
|
import { useToggleOneUpViewportGridStore } from '../../default/src/stores/useToggleOneUpViewportGridStore';
|
||||||
|
import { useActiveViewportSegmentationRepresentations } from './hooks/useActiveViewportSegmentationRepresentations';
|
||||||
|
import { useMeasurements } from './hooks/useMeasurements';
|
||||||
|
import getPanelModule from './getPanelModule';
|
||||||
|
import PanelSegmentation from './panels/PanelSegmentation';
|
||||||
|
import PanelMeasurement from './panels/PanelMeasurement';
|
||||||
|
import DicomUpload from './components/DicomUpload/DicomUpload';
|
||||||
|
import { useSegmentations } from './hooks/useSegmentations';
|
||||||
|
|
||||||
const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader;
|
|
||||||
const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {};
|
|
||||||
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
|
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
@ -84,9 +94,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
toolbarService.registerEventForToolbarUpdate(segmentationService, [
|
toolbarService.registerEventForToolbarUpdate(segmentationService, [
|
||||||
segmentationService.EVENTS.SEGMENTATION_ADDED,
|
|
||||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||||
segmentationService.EVENTS.SEGMENTATION_UPDATED,
|
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
|
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
|
||||||
@ -108,9 +117,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
// how to define them.
|
// how to define them.
|
||||||
imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions);
|
imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions);
|
||||||
},
|
},
|
||||||
|
getPanelModule,
|
||||||
onModeExit: ({ servicesManager }: withAppTypes): void => {
|
onModeExit: ({ servicesManager }: withAppTypes): void => {
|
||||||
const { cineService } = servicesManager.services;
|
const { cineService, segmentationService } = servicesManager.services;
|
||||||
// Empty out the image load and retrieval pools to prevent memory leaks
|
// Empty out the image load and retrieval pools to prevent memory leaks
|
||||||
// on the mode exits
|
// on the mode exits
|
||||||
Object.values(cs3DEnums.RequestType).forEach(type => {
|
Object.values(cs3DEnums.RequestType).forEach(type => {
|
||||||
@ -121,6 +130,13 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
cineService.setIsCineEnabled(false);
|
cineService.setIsCineEnabled(false);
|
||||||
|
|
||||||
enabledElementReset();
|
enabledElementReset();
|
||||||
|
|
||||||
|
useLutPresentationStore.getState().clearLutPresentationStore();
|
||||||
|
usePositionPresentationStore.getState().clearPositionPresentationStore();
|
||||||
|
useSynchronizersStore.getState().clearSynchronizersStore();
|
||||||
|
useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore();
|
||||||
|
useSegmentationPresentationStore.getState().clearSegmentationPresentationStore();
|
||||||
|
segmentationService.removeAllSegmentations();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -145,20 +161,12 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
|
|
||||||
const { syncGroupService } = servicesManager.services;
|
const { syncGroupService } = servicesManager.services;
|
||||||
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
|
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
|
||||||
|
servicesManager.services.customizationService.setGlobalCustomization('dicomUploadComponent', {
|
||||||
|
component: props => <DicomUpload {...props} />,
|
||||||
|
});
|
||||||
return init.call(this, props);
|
return init.call(this, props);
|
||||||
},
|
},
|
||||||
|
|
||||||
getToolbarModule,
|
getToolbarModule,
|
||||||
getPanelModule({ servicesManager }) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'activeViewportWindowLevel',
|
|
||||||
component: () => {
|
|
||||||
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
getHangingProtocolModule,
|
getHangingProtocolModule,
|
||||||
getViewportModule({ servicesManager, commandsManager }) {
|
getViewportModule({ servicesManager, commandsManager }) {
|
||||||
const ExtendedOHIFCornerstoneViewport = props => {
|
const ExtendedOHIFCornerstoneViewport = props => {
|
||||||
@ -196,7 +204,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
},
|
},
|
||||||
getEnabledElement,
|
getEnabledElement,
|
||||||
dicomLoaderService,
|
dicomLoaderService,
|
||||||
showLabelAnnotationPopup,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -235,6 +242,17 @@ export {
|
|||||||
ImageOverlayViewerTool,
|
ImageOverlayViewerTool,
|
||||||
getSOPInstanceAttributes,
|
getSOPInstanceAttributes,
|
||||||
dicomLoaderService,
|
dicomLoaderService,
|
||||||
|
// Export all stores
|
||||||
|
useLutPresentationStore,
|
||||||
|
usePositionPresentationStore,
|
||||||
|
useSegmentationPresentationStore,
|
||||||
|
useSynchronizersStore,
|
||||||
Enums,
|
Enums,
|
||||||
|
useMeasurements,
|
||||||
|
useActiveViewportSegmentationRepresentations,
|
||||||
|
useSegmentations,
|
||||||
|
PanelSegmentation,
|
||||||
|
PanelMeasurement,
|
||||||
|
DicomUpload,
|
||||||
};
|
};
|
||||||
export default cornerstoneExtension;
|
export default cornerstoneExtension;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import OHIF, { Types, errorHandler } from '@ohif/core';
|
import OHIF, { errorHandler } from '@ohif/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import * as cornerstone from '@cornerstonejs/core';
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
@ -13,12 +13,13 @@ import {
|
|||||||
getEnabledElement,
|
getEnabledElement,
|
||||||
Settings,
|
Settings,
|
||||||
utilities as csUtilities,
|
utilities as csUtilities,
|
||||||
Enums as csEnums,
|
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import {
|
import {
|
||||||
cornerstoneStreamingImageVolumeLoader,
|
cornerstoneStreamingImageVolumeLoader,
|
||||||
cornerstoneStreamingDynamicImageVolumeLoader,
|
cornerstoneStreamingDynamicImageVolumeLoader,
|
||||||
} from '@cornerstonejs/streaming-image-volume-loader';
|
} from '@cornerstonejs/core/loaders';
|
||||||
|
|
||||||
|
import RequestTypes from '@cornerstonejs/core/enums/RequestType';
|
||||||
|
|
||||||
import initWADOImageLoader from './initWADOImageLoader';
|
import initWADOImageLoader from './initWADOImageLoader';
|
||||||
import initCornerstoneTools from './initCornerstoneTools';
|
import initCornerstoneTools from './initCornerstoneTools';
|
||||||
@ -33,6 +34,10 @@ import initContextMenu from './initContextMenu';
|
|||||||
import initDoubleClick from './initDoubleClick';
|
import initDoubleClick from './initDoubleClick';
|
||||||
import initViewTiming from './utils/initViewTiming';
|
import initViewTiming from './utils/initViewTiming';
|
||||||
import { colormaps } from './utils/colormaps';
|
import { colormaps } from './utils/colormaps';
|
||||||
|
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||||
|
import { useLutPresentationStore } from './stores/useLutPresentationStore';
|
||||||
|
import { usePositionPresentationStore } from './stores/usePositionPresentationStore';
|
||||||
|
import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore';
|
||||||
|
|
||||||
const { registerColormap } = csUtilities.colormap;
|
const { registerColormap } = csUtilities.colormap;
|
||||||
|
|
||||||
@ -46,27 +51,12 @@ export default async function init({
|
|||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
appConfig
|
appConfig,
|
||||||
}: Types.Extensions.ExtensionParams): Promise<void> {
|
}: withAppTypes): Promise<void> {
|
||||||
// Note: this should run first before initializing the cornerstone
|
// Note: this should run first before initializing the cornerstone
|
||||||
// DO NOT CHANGE THE ORDER
|
// DO NOT CHANGE THE ORDER
|
||||||
const value = appConfig.useSharedArrayBuffer;
|
|
||||||
let sharedArrayBufferDisabled = false;
|
|
||||||
|
|
||||||
if (value === 'AUTO') {
|
|
||||||
cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.AUTO);
|
|
||||||
} else if (value === 'FALSE' || value === false) {
|
|
||||||
cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.FALSE);
|
|
||||||
sharedArrayBufferDisabled = true;
|
|
||||||
} else {
|
|
||||||
cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.TRUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
await cs3DInit({
|
await cs3DInit({
|
||||||
rendering: {
|
|
||||||
preferSizeOverAccuracy: Boolean(appConfig.preferSizeOverAccuracy),
|
|
||||||
useNorm16Texture: Boolean(appConfig.useNorm16Texture),
|
|
||||||
},
|
|
||||||
peerImport: appConfig.peerImport,
|
peerImport: appConfig.peerImport,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -99,63 +89,39 @@ export default async function init({
|
|||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
hangingProtocolService,
|
hangingProtocolService,
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
stateSyncService,
|
|
||||||
studyPrefetcherService,
|
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
window.services = servicesManager.services;
|
window.services = servicesManager.services;
|
||||||
window.extensionManager = extensionManager;
|
window.extensionManager = extensionManager;
|
||||||
window.commandsManager = commandsManager;
|
window.commandsManager = commandsManager;
|
||||||
|
|
||||||
if (
|
|
||||||
appConfig.showWarningMessageForCrossOrigin &&
|
|
||||||
!window.crossOriginIsolated &&
|
|
||||||
!sharedArrayBufferDisabled
|
|
||||||
) {
|
|
||||||
uiNotificationService.show({
|
|
||||||
title: 'Cross Origin Isolation',
|
|
||||||
message:
|
|
||||||
'Cross Origin Isolation is not enabled, read more about it here: https://docs.ohif.org/faq/',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appConfig.showCPUFallbackMessage && cornerstone.getShouldUseCPURendering()) {
|
if (appConfig.showCPUFallbackMessage && cornerstone.getShouldUseCPURendering()) {
|
||||||
_showCPURenderingModal(uiModalService, hangingProtocolService);
|
_showCPURenderingModal(uiModalService, hangingProtocolService);
|
||||||
}
|
}
|
||||||
|
const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState();
|
||||||
|
|
||||||
// Stores a map from `lutPresentationId` to a Presentation object so that
|
const { getPresentationId: getSegmentationPresentationId } =
|
||||||
// an OHIFCornerstoneViewport can be redisplayed with the same LUT
|
useSegmentationPresentationStore.getState();
|
||||||
stateSyncService.register('lutPresentationStore', { clearOnModeExit: true });
|
|
||||||
|
|
||||||
// Stores synchronizers state to be restored
|
const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState();
|
||||||
stateSyncService.register('synchronizersStore', { clearOnModeExit: true });
|
|
||||||
|
|
||||||
// Stores a map from `positionPresentationId` to a Presentation object so that
|
// register presentation id providers
|
||||||
// an OHIFCornerstoneViewport can be redisplayed with the same position
|
viewportGridService.addPresentationIdProvider(
|
||||||
stateSyncService.register('positionPresentationStore', {
|
'positionPresentationId',
|
||||||
clearOnModeExit: true,
|
getPositionPresentationId
|
||||||
});
|
);
|
||||||
|
viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId);
|
||||||
|
viewportGridService.addPresentationIdProvider(
|
||||||
|
'segmentationPresentationId',
|
||||||
|
getSegmentationPresentationId
|
||||||
|
);
|
||||||
|
|
||||||
// Stores the entire ViewportGridService getState when toggling to one up
|
cornerstoneTools.segmentation.config.style.setStyle(
|
||||||
// (e.g. via a double click) so that it can be restored when toggling back.
|
{ type: SegmentationRepresentations.Contour },
|
||||||
stateSyncService.register('toggleOneUpViewportGridStore', {
|
{
|
||||||
clearOnModeExit: true,
|
renderFill: false,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
const labelmapRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
|
|
||||||
const contourRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Contour;
|
|
||||||
|
|
||||||
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(labelmapRepresentation, {
|
|
||||||
fillAlpha: 0.5,
|
|
||||||
fillAlphaInactive: 0.2,
|
|
||||||
outlineOpacity: 1,
|
|
||||||
outlineOpacityInactive: 0.65,
|
|
||||||
});
|
|
||||||
|
|
||||||
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(contourRepresentation, {
|
|
||||||
renderFill: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||||
|
|
||||||
@ -184,9 +150,10 @@ export default async function init({
|
|||||||
// These are set reasonably low to allow for interleaved retrieves and slower
|
// These are set reasonably low to allow for interleaved retrieves and slower
|
||||||
// connections.
|
// connections.
|
||||||
imageLoadPoolManager.maxNumRequests = {
|
imageLoadPoolManager.maxNumRequests = {
|
||||||
interaction: appConfig?.maxNumRequests?.interaction || 10,
|
[RequestTypes.Interaction]: appConfig?.maxNumRequests?.interaction || 10,
|
||||||
thumbnail: appConfig?.maxNumRequests?.thumbnail || 5,
|
[RequestTypes.Thumbnail]: appConfig?.maxNumRequests?.thumbnail || 5,
|
||||||
prefetch: appConfig?.maxNumRequests?.prefetch || 5,
|
[RequestTypes.Prefetch]: appConfig?.maxNumRequests?.prefetch || 5,
|
||||||
|
[RequestTypes.Compute]: appConfig?.maxNumRequests?.compute || 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
|
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
|
||||||
@ -201,17 +168,23 @@ export default async function init({
|
|||||||
hangingProtocolService.subscribe(
|
hangingProtocolService.subscribe(
|
||||||
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
|
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
|
||||||
volumeInputArrayMap => {
|
volumeInputArrayMap => {
|
||||||
|
const { lutPresentationStore } = useLutPresentationStore.getState();
|
||||||
|
const { segmentationPresentationStore } = useSegmentationPresentationStore.getState();
|
||||||
|
const { positionPresentationStore } = usePositionPresentationStore.getState();
|
||||||
|
|
||||||
for (const entry of volumeInputArrayMap.entries()) {
|
for (const entry of volumeInputArrayMap.entries()) {
|
||||||
const [viewportId, volumeInputArray] = entry;
|
const [viewportId, volumeInputArray] = entry;
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
|
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||||
|
|
||||||
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
|
|
||||||
const { presentationIds } = ohifViewport.getViewportOptions();
|
const { presentationIds } = ohifViewport.getViewportOptions();
|
||||||
|
|
||||||
const presentations = {
|
const presentations = {
|
||||||
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
||||||
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
|
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
|
||||||
|
segmentationPresentation:
|
||||||
|
segmentationPresentationStore[presentationIds?.segmentationPresentationId],
|
||||||
};
|
};
|
||||||
|
|
||||||
cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations);
|
cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations);
|
||||||
@ -249,10 +222,6 @@ export default async function init({
|
|||||||
handler(detail.error);
|
handler(detail.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
|
|
||||||
const { element } = evt.detail;
|
|
||||||
cornerstoneTools.utilities.stackContextPrefetch.enable(element);
|
|
||||||
});
|
|
||||||
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler);
|
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler);
|
||||||
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler);
|
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler);
|
||||||
|
|
||||||
@ -269,26 +238,11 @@ export default async function init({
|
|||||||
commandsManager.runCommand('resetCrosshairs', { viewportId });
|
commandsManager.runCommand('resetCrosshairs', { viewportId });
|
||||||
});
|
});
|
||||||
|
|
||||||
// eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
|
|
||||||
|
|
||||||
initViewTiming({ element });
|
initViewTiming({ element });
|
||||||
}
|
}
|
||||||
|
|
||||||
function elementDisabledHandler(evt) {
|
|
||||||
const { element } = evt.detail;
|
|
||||||
|
|
||||||
// element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
|
||||||
|
|
||||||
// TODO - consider removing the callback when all elements are gone
|
|
||||||
// eventTarget.removeEventListener(
|
|
||||||
// EVENTS.STACK_VIEWPORT_NEW_STACK,
|
|
||||||
// newStackCallback
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
|
|
||||||
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
|
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
|
||||||
|
|
||||||
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
|
|
||||||
colormaps.forEach(registerColormap);
|
colormaps.forEach(registerColormap);
|
||||||
|
|
||||||
// Event listener
|
// Event listener
|
||||||
@ -301,8 +255,45 @@ export default async function init({
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
1000
|
100
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Call this function when initializing
|
||||||
|
initializeWebWorkerProgressHandler(servicesManager.services.uiNotificationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeWebWorkerProgressHandler(uiNotificationService) {
|
||||||
|
const activeToasts = new Map();
|
||||||
|
|
||||||
|
eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => {
|
||||||
|
const { progress, type, id } = detail;
|
||||||
|
|
||||||
|
const cacheKey = `${type}-${id}`;
|
||||||
|
if (progress === 0 && !activeToasts.has(cacheKey)) {
|
||||||
|
const progressPromise = new Promise((resolve, reject) => {
|
||||||
|
activeToasts.set(cacheKey, { resolve, reject });
|
||||||
|
});
|
||||||
|
|
||||||
|
uiNotificationService.show({
|
||||||
|
id: cacheKey,
|
||||||
|
title: `${type}`,
|
||||||
|
message: `${type}: ${progress}%`,
|
||||||
|
autoClose: false,
|
||||||
|
promise: progressPromise,
|
||||||
|
promiseMessages: {
|
||||||
|
loading: `Computing...`,
|
||||||
|
success: `Completed successfully`,
|
||||||
|
error: 'Web Worker failed',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (progress === 100) {
|
||||||
|
const { resolve } = activeToasts.get(cacheKey);
|
||||||
|
resolve({ progress, type });
|
||||||
|
activeToasts.delete(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function CPUModal() {
|
function CPUModal() {
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
import { cache } from '@cornerstonejs/core';
|
import { cache, Types } from '@cornerstonejs/core';
|
||||||
import { utilities } from '@cornerstonejs/tools';
|
import { utilities } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
function _getVolumesFromViewport(viewport) {
|
function _getVolumeFromViewport(viewport: Types.IBaseVolumeViewport) {
|
||||||
return viewport ? viewport.getActors().map(actor => cache.getVolume(actor.uid)) : [];
|
const volumeIds = viewport.getAllVolumeIds();
|
||||||
}
|
const volumes = volumeIds.map(id => cache.getVolume(id));
|
||||||
|
|
||||||
function _getVolumeFromViewport(viewport) {
|
|
||||||
const volumes = _getVolumesFromViewport(viewport).filter(volume => volume);
|
|
||||||
const dynamicVolume = volumes.find(volume => volume.isDynamicVolume());
|
const dynamicVolume = volumes.find(volume => volume.isDynamicVolume());
|
||||||
|
|
||||||
return dynamicVolume ?? volumes[0];
|
return dynamicVolume ?? volumes[0];
|
||||||
|
|||||||
@ -2,9 +2,8 @@ import {
|
|||||||
PanTool,
|
PanTool,
|
||||||
WindowLevelTool,
|
WindowLevelTool,
|
||||||
StackScrollTool,
|
StackScrollTool,
|
||||||
StackScrollMouseWheelTool,
|
VolumeRotateTool,
|
||||||
ZoomTool,
|
ZoomTool,
|
||||||
VolumeRotateMouseWheelTool,
|
|
||||||
MIPJumpToClickTool,
|
MIPJumpToClickTool,
|
||||||
LengthTool,
|
LengthTool,
|
||||||
RectangleROITool,
|
RectangleROITool,
|
||||||
@ -19,7 +18,6 @@ import {
|
|||||||
CobbAngleTool,
|
CobbAngleTool,
|
||||||
MagnifyTool,
|
MagnifyTool,
|
||||||
CrosshairsTool,
|
CrosshairsTool,
|
||||||
SegmentationDisplayTool,
|
|
||||||
RectangleScissorsTool,
|
RectangleScissorsTool,
|
||||||
SphereScissorsTool,
|
SphereScissorsTool,
|
||||||
CircleScissorsTool,
|
CircleScissorsTool,
|
||||||
@ -47,15 +45,15 @@ export default function initCornerstoneTools(configuration = {}) {
|
|||||||
CrosshairsTool.isAnnotation = false;
|
CrosshairsTool.isAnnotation = false;
|
||||||
ReferenceLinesTool.isAnnotation = false;
|
ReferenceLinesTool.isAnnotation = false;
|
||||||
AdvancedMagnifyTool.isAnnotation = false;
|
AdvancedMagnifyTool.isAnnotation = false;
|
||||||
|
PlanarFreehandContourSegmentationTool.isAnnotation = false;
|
||||||
|
|
||||||
init(configuration);
|
init(configuration);
|
||||||
addTool(PanTool);
|
addTool(PanTool);
|
||||||
addTool(WindowLevelTool);
|
addTool(WindowLevelTool);
|
||||||
addTool(StackScrollMouseWheelTool);
|
|
||||||
addTool(StackScrollTool);
|
addTool(StackScrollTool);
|
||||||
|
addTool(VolumeRotateTool);
|
||||||
addTool(ZoomTool);
|
addTool(ZoomTool);
|
||||||
addTool(ProbeTool);
|
addTool(ProbeTool);
|
||||||
addTool(VolumeRotateMouseWheelTool);
|
|
||||||
addTool(MIPJumpToClickTool);
|
addTool(MIPJumpToClickTool);
|
||||||
addTool(LengthTool);
|
addTool(LengthTool);
|
||||||
addTool(RectangleROITool);
|
addTool(RectangleROITool);
|
||||||
@ -69,7 +67,6 @@ export default function initCornerstoneTools(configuration = {}) {
|
|||||||
addTool(CobbAngleTool);
|
addTool(CobbAngleTool);
|
||||||
addTool(MagnifyTool);
|
addTool(MagnifyTool);
|
||||||
addTool(CrosshairsTool);
|
addTool(CrosshairsTool);
|
||||||
addTool(SegmentationDisplayTool);
|
|
||||||
addTool(RectangleScissorsTool);
|
addTool(RectangleScissorsTool);
|
||||||
addTool(SphereScissorsTool);
|
addTool(SphereScissorsTool);
|
||||||
addTool(CircleScissorsTool);
|
addTool(CircleScissorsTool);
|
||||||
@ -108,9 +105,8 @@ const toolNames = {
|
|||||||
ArrowAnnotate: ArrowAnnotateTool.toolName,
|
ArrowAnnotate: ArrowAnnotateTool.toolName,
|
||||||
WindowLevel: WindowLevelTool.toolName,
|
WindowLevel: WindowLevelTool.toolName,
|
||||||
StackScroll: StackScrollTool.toolName,
|
StackScroll: StackScrollTool.toolName,
|
||||||
StackScrollMouseWheel: StackScrollMouseWheelTool.toolName,
|
|
||||||
Zoom: ZoomTool.toolName,
|
Zoom: ZoomTool.toolName,
|
||||||
VolumeRotateMouseWheel: VolumeRotateMouseWheelTool.toolName,
|
VolumeRotate: VolumeRotateTool.toolName,
|
||||||
MipJumpToClick: MIPJumpToClickTool.toolName,
|
MipJumpToClick: MIPJumpToClickTool.toolName,
|
||||||
Length: LengthTool.toolName,
|
Length: LengthTool.toolName,
|
||||||
DragProbe: DragProbeTool.toolName,
|
DragProbe: DragProbeTool.toolName,
|
||||||
@ -124,7 +120,6 @@ const toolNames = {
|
|||||||
CobbAngle: CobbAngleTool.toolName,
|
CobbAngle: CobbAngleTool.toolName,
|
||||||
Magnify: MagnifyTool.toolName,
|
Magnify: MagnifyTool.toolName,
|
||||||
Crosshairs: CrosshairsTool.toolName,
|
Crosshairs: CrosshairsTool.toolName,
|
||||||
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
|
||||||
Brush: BrushTool.toolName,
|
Brush: BrushTool.toolName,
|
||||||
PaintFill: PaintFillTool.toolName,
|
PaintFill: PaintFillTool.toolName,
|
||||||
ReferenceLines: ReferenceLinesTool.toolName,
|
ReferenceLines: ReferenceLinesTool.toolName,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { eventTarget } from '@cornerstonejs/core';
|
import { eventTarget, Types } from '@cornerstonejs/core';
|
||||||
import { Enums, annotation } from '@cornerstonejs/tools';
|
import { Enums, annotation } from '@cornerstonejs/tools';
|
||||||
import { DicomMetadataStore } from '@ohif/core';
|
import { DicomMetadataStore } from '@ohif/core';
|
||||||
|
|
||||||
@ -7,6 +7,7 @@ import { toolNames } from './initCornerstoneTools';
|
|||||||
import { onCompletedCalibrationLine } from './tools/CalibrationLineTool';
|
import { onCompletedCalibrationLine } from './tools/CalibrationLineTool';
|
||||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||||
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||||
|
import { triggerAnnotationRenderForViewportIds } from '@cornerstonejs/tools/utilities';
|
||||||
|
|
||||||
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
|
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
|
||||||
const { removeAnnotation } = annotation.state;
|
const { removeAnnotation } = annotation.state;
|
||||||
@ -344,8 +345,7 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { uid, label } = measurement;
|
const { uid, label, isLocked, isVisible } = measurement;
|
||||||
|
|
||||||
const sourceAnnotation = annotation.state.getAnnotation(uid);
|
const sourceAnnotation = annotation.state.getAnnotation(uid);
|
||||||
const { data, metadata } = sourceAnnotation;
|
const { data, metadata } = sourceAnnotation;
|
||||||
|
|
||||||
@ -361,7 +361,23 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
|
|||||||
data.text = label;
|
data.text = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: trigger render for annotation
|
// update the isLocked state
|
||||||
|
annotation.locking.setAnnotationLocked(uid, isLocked);
|
||||||
|
|
||||||
|
// update the isVisible state
|
||||||
|
annotation.visibility.setAnnotationVisibility(uid, isVisible);
|
||||||
|
|
||||||
|
// annotation.config.style.setAnnotationStyles(uid, {
|
||||||
|
// color: `rgb(${color[0]}, ${color[1]}, ${color[2]})`,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// I don't like this but will fix later
|
||||||
|
const renderingEngine =
|
||||||
|
cornerstoneViewportService.getRenderingEngine() as Types.IRenderingEngine;
|
||||||
|
// Note: We could do a better job by triggering the render on the
|
||||||
|
// viewport itself, but the removeAnnotation does not include that info...
|
||||||
|
const viewportIds = renderingEngine.getViewports().map(viewport => viewport.id);
|
||||||
|
triggerAnnotationRenderForViewportIds(viewportIds);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,47 +1,18 @@
|
|||||||
import * as cornerstone from '@cornerstonejs/core';
|
|
||||||
import { volumeLoader } from '@cornerstonejs/core';
|
import { volumeLoader } from '@cornerstonejs/core';
|
||||||
import {
|
import {
|
||||||
cornerstoneStreamingImageVolumeLoader,
|
cornerstoneStreamingImageVolumeLoader,
|
||||||
cornerstoneStreamingDynamicImageVolumeLoader,
|
cornerstoneStreamingDynamicImageVolumeLoader,
|
||||||
} from '@cornerstonejs/streaming-image-volume-loader';
|
} from '@cornerstonejs/core/loaders';
|
||||||
import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader';
|
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
|
||||||
import dicomParser from 'dicom-parser';
|
|
||||||
import { errorHandler, utils } from '@ohif/core';
|
import { errorHandler, utils } from '@ohif/core';
|
||||||
|
|
||||||
const { registerVolumeLoader } = volumeLoader;
|
const { registerVolumeLoader } = volumeLoader;
|
||||||
|
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
function initWebWorkers(appConfig) {
|
|
||||||
const config = {
|
|
||||||
maxWebWorkers: Math.min(
|
|
||||||
Math.max(navigator.hardwareConcurrency - 1, 1),
|
|
||||||
appConfig.maxNumberOfWebWorkers
|
|
||||||
),
|
|
||||||
startWebWorkersOnDemand: true,
|
|
||||||
taskConfiguration: {
|
|
||||||
decodeTask: {
|
|
||||||
initializeCodecsOnStartup: false,
|
|
||||||
usePDFJS: false,
|
|
||||||
strict: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!initialized) {
|
|
||||||
dicomImageLoader.webWorkerManager.initialize(config);
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function initWADOImageLoader(
|
export default function initWADOImageLoader(
|
||||||
userAuthenticationService,
|
userAuthenticationService,
|
||||||
appConfig,
|
appConfig,
|
||||||
extensionManager
|
extensionManager
|
||||||
) {
|
) {
|
||||||
dicomImageLoader.external.cornerstone = cornerstone;
|
|
||||||
dicomImageLoader.external.dicomParser = dicomParser;
|
|
||||||
|
|
||||||
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
|
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
|
||||||
|
|
||||||
registerVolumeLoader(
|
registerVolumeLoader(
|
||||||
@ -49,17 +20,11 @@ export default function initWADOImageLoader(
|
|||||||
cornerstoneStreamingDynamicImageVolumeLoader
|
cornerstoneStreamingDynamicImageVolumeLoader
|
||||||
);
|
);
|
||||||
|
|
||||||
dicomImageLoader.configure({
|
dicomImageLoader.init({
|
||||||
decodeConfig: {
|
maxWebWorkers: Math.min(
|
||||||
// !! IMPORTANT !!
|
Math.max(navigator.hardwareConcurrency - 1, 1),
|
||||||
// We should set this flag to false, since, by default @cornerstonejs/dicom-image-loader
|
appConfig.maxNumberOfWebWorkers
|
||||||
// will convert everything to integers (to be able to work with cornerstone-2d).
|
),
|
||||||
// Until the default is set to true (which is the case for cornerstone3D),
|
|
||||||
// we should set this flag to false.
|
|
||||||
convertFloatPixelDataToInt: false,
|
|
||||||
use16BitDataType:
|
|
||||||
Boolean(appConfig.useNorm16Texture) || Boolean(appConfig.preferSizeOverAccuracy),
|
|
||||||
},
|
|
||||||
beforeSend: function (xhr) {
|
beforeSend: function (xhr) {
|
||||||
//TODO should be removed in the future and request emitted by DicomWebDataSource
|
//TODO should be removed in the future and request emitted by DicomWebDataSource
|
||||||
const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {};
|
const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {};
|
||||||
@ -84,16 +49,8 @@ export default function initWADOImageLoader(
|
|||||||
errorHandler.getHTTPErrorHandler(error);
|
errorHandler.getHTTPErrorHandler(error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
initWebWorkers(appConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function destroy() {
|
export function destroy() {
|
||||||
// Note: we don't want to call .terminate on the webWorkerManager since
|
console.debug('Destroying WADO Image Loader');
|
||||||
// that resets the config
|
|
||||||
const webWorkers = webWorkerManager.webWorkers;
|
|
||||||
for (let i = 0; i < webWorkers.length; i++) {
|
|
||||||
webWorkers[i].worker.terminate();
|
|
||||||
}
|
|
||||||
webWorkers.length = 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
147
extensions/cornerstone/src/panels/PanelMeasurement.tsx
Normal file
147
extensions/cornerstone/src/panels/PanelMeasurement.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { useViewportGrid } from '@ohif/ui';
|
||||||
|
import { MeasurementTable } from '@ohif/ui-next';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
import { useMeasurements } from '../hooks/useMeasurements';
|
||||||
|
import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-default';
|
||||||
|
|
||||||
|
export default function PanelMeasurementTable({
|
||||||
|
servicesManager,
|
||||||
|
customHeader,
|
||||||
|
measurementFilter,
|
||||||
|
}: withAppTypes): React.ReactNode {
|
||||||
|
const measurementsPanelRef = useRef(null);
|
||||||
|
|
||||||
|
const [viewportGrid] = useViewportGrid();
|
||||||
|
const { measurementService, customizationService, uiDialogService } = servicesManager.services;
|
||||||
|
|
||||||
|
const displayMeasurements = useMeasurements(servicesManager, {
|
||||||
|
measurementFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (displayMeasurements.length > 0) {
|
||||||
|
debounce(() => {
|
||||||
|
measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight;
|
||||||
|
}, 300)();
|
||||||
|
}
|
||||||
|
}, [displayMeasurements.length]);
|
||||||
|
|
||||||
|
const onMeasurementItemClickHandler = (uid: string, isActive: boolean) => {
|
||||||
|
if (isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurements = [...displayMeasurements];
|
||||||
|
const measurement = measurements.find(m => m.uid === uid);
|
||||||
|
|
||||||
|
measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true));
|
||||||
|
measurement.isActive = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const jumpToImage = (uid: string) => {
|
||||||
|
measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid);
|
||||||
|
onMeasurementItemClickHandler(uid, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMeasurement = (uid: string) => {
|
||||||
|
measurementService.remove(uid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renameMeasurement = (uid: string) => {
|
||||||
|
jumpToImage(uid);
|
||||||
|
const labelConfig = customizationService.get('measurementLabels');
|
||||||
|
const measurement = measurementService.getMeasurement(uid);
|
||||||
|
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => {
|
||||||
|
measurementService.update(
|
||||||
|
uid,
|
||||||
|
{
|
||||||
|
...val,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeColorMeasurement = (uid: string) => {
|
||||||
|
const { color } = measurementService.getMeasurement(uid);
|
||||||
|
const rgbaColor = {
|
||||||
|
r: color[0],
|
||||||
|
g: color[1],
|
||||||
|
b: color[2],
|
||||||
|
a: color[3] / 255.0,
|
||||||
|
};
|
||||||
|
colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
||||||
|
if (actionId === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
|
||||||
|
// segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleLockMeasurement = (uid: string) => {
|
||||||
|
measurementService.toggleLockMeasurement(uid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleVisibilityMeasurement = (uid: string) => {
|
||||||
|
measurementService.toggleVisibilityMeasurement(uid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const measurements = displayMeasurements.filter(
|
||||||
|
dm => dm.measurementType !== measurementService.VALUE_TYPES.POINT && dm.referencedImageId
|
||||||
|
);
|
||||||
|
const additionalFindings = displayMeasurements.filter(
|
||||||
|
dm => dm.measurementType === measurementService.VALUE_TYPES.POINT && dm.referencedImageId
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="invisible-scrollbar overflow-y-auto overflow-x-hidden"
|
||||||
|
ref={measurementsPanelRef}
|
||||||
|
data-cy={'trackedMeasurements-panel'}
|
||||||
|
>
|
||||||
|
<MeasurementTable
|
||||||
|
title="Measurements"
|
||||||
|
data={measurements}
|
||||||
|
onClick={jumpToImage}
|
||||||
|
onDelete={removeMeasurement}
|
||||||
|
onToggleVisibility={toggleVisibilityMeasurement}
|
||||||
|
onToggleLocked={toggleLockMeasurement}
|
||||||
|
onRename={renameMeasurement}
|
||||||
|
// onColor={changeColorMeasurement}
|
||||||
|
>
|
||||||
|
<MeasurementTable.Header>
|
||||||
|
{customHeader && (
|
||||||
|
<>
|
||||||
|
{typeof customHeader === 'function'
|
||||||
|
? customHeader({
|
||||||
|
additionalFindings,
|
||||||
|
measurements,
|
||||||
|
})
|
||||||
|
: customHeader}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MeasurementTable.Header>
|
||||||
|
<MeasurementTable.Body />
|
||||||
|
</MeasurementTable>
|
||||||
|
{additionalFindings.length > 0 && (
|
||||||
|
<MeasurementTable
|
||||||
|
data={additionalFindings}
|
||||||
|
title="Additional Findings"
|
||||||
|
onClick={jumpToImage}
|
||||||
|
onDelete={removeMeasurement}
|
||||||
|
onToggleVisibility={toggleVisibilityMeasurement}
|
||||||
|
onToggleLocked={toggleLockMeasurement}
|
||||||
|
onRename={renameMeasurement}
|
||||||
|
// onColor={changeColorMeasurement}
|
||||||
|
>
|
||||||
|
<MeasurementTable.Body />
|
||||||
|
</MeasurementTable>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
230
extensions/cornerstone/src/panels/PanelSegmentation.tsx
Normal file
230
extensions/cornerstone/src/panels/PanelSegmentation.tsx
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SegmentationTable } from '@ohif/ui-next';
|
||||||
|
import { useActiveViewportSegmentationRepresentations } from '../hooks/useActiveViewportSegmentationRepresentations';
|
||||||
|
import { metaData } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
export default function PanelSegmentation({
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
extensionManager,
|
||||||
|
configuration,
|
||||||
|
children,
|
||||||
|
}: withAppTypes) {
|
||||||
|
const { customizationService, viewportGridService, displaySetService } = servicesManager.services;
|
||||||
|
|
||||||
|
const { segmentationsWithRepresentations, disabled } =
|
||||||
|
useActiveViewportSegmentationRepresentations({
|
||||||
|
servicesManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
onSegmentationAdd: async () => {
|
||||||
|
const viewportId = viewportGridService.getState().activeViewportId;
|
||||||
|
commandsManager.run('createLabelmapForViewport', { viewportId });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentationClick: (segmentationId: string) => {
|
||||||
|
commandsManager.run('setActiveSegmentation', { segmentationId });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentAdd: segmentationId => {
|
||||||
|
commandsManager.run('addSegment', { segmentationId });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentClick: (segmentationId, segmentIndex) => {
|
||||||
|
commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentEdit: (segmentationId, segmentIndex) => {
|
||||||
|
commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentationEdit: segmentationId => {
|
||||||
|
commandsManager.run('editSegmentationLabel', { segmentationId });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentColorClick: (segmentationId, segmentIndex) => {
|
||||||
|
commandsManager.run('editSegmentColor', { segmentationId, segmentIndex });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentDelete: (segmentationId, segmentIndex) => {
|
||||||
|
commandsManager.run('deleteSegment', { segmentationId, segmentIndex });
|
||||||
|
},
|
||||||
|
|
||||||
|
onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => {
|
||||||
|
commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type });
|
||||||
|
},
|
||||||
|
|
||||||
|
onToggleSegmentLock: (segmentationId, segmentIndex) => {
|
||||||
|
commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex });
|
||||||
|
},
|
||||||
|
|
||||||
|
onToggleSegmentationRepresentationVisibility: (segmentationId, type) => {
|
||||||
|
commandsManager.run('toggleSegmentationVisibility', { segmentationId, type });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentationDownload: segmentationId => {
|
||||||
|
commandsManager.run('downloadSegmentation', { segmentationId });
|
||||||
|
},
|
||||||
|
|
||||||
|
storeSegmentation: async segmentationId => {
|
||||||
|
commandsManager.run('storeSegmentation', { segmentationId });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentationDownloadRTSS: segmentationId => {
|
||||||
|
commandsManager.run('downloadRTSS', { segmentationId });
|
||||||
|
},
|
||||||
|
|
||||||
|
setStyle: (segmentationId, type, key, value) => {
|
||||||
|
commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value });
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleRenderInactiveSegmentations: () => {
|
||||||
|
commandsManager.run('toggleRenderInactiveSegmentations');
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentationRemoveFromViewport: segmentationId => {
|
||||||
|
commandsManager.run('removeSegmentationFromViewport', { segmentationId });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSegmentationDelete: segmentationId => {
|
||||||
|
commandsManager.run('deleteSegmentation', { segmentationId });
|
||||||
|
},
|
||||||
|
|
||||||
|
setFillAlpha: (type, value) => {
|
||||||
|
commandsManager.run('setFillAlpha', { type, value });
|
||||||
|
},
|
||||||
|
|
||||||
|
setOutlineWidth: (type, value) => {
|
||||||
|
commandsManager.run('setOutlineWidth', { type, value });
|
||||||
|
},
|
||||||
|
|
||||||
|
setRenderFill: (type, value) => {
|
||||||
|
commandsManager.run('setRenderFill', { type, value });
|
||||||
|
},
|
||||||
|
|
||||||
|
setRenderOutline: (type, value) => {
|
||||||
|
commandsManager.run('setRenderOutline', { type, value });
|
||||||
|
},
|
||||||
|
|
||||||
|
setFillAlphaInactive: (type, value) => {
|
||||||
|
commandsManager.run('setFillAlphaInactive', { type, value });
|
||||||
|
},
|
||||||
|
|
||||||
|
getRenderInactiveSegmentations: () => {
|
||||||
|
return commandsManager.run('getRenderInactiveSegmentations');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mode: SegmentationTableMode } = customizationService.getCustomization(
|
||||||
|
'PanelSegmentation.tableMode',
|
||||||
|
{
|
||||||
|
id: 'default.segmentationTable.mode',
|
||||||
|
mode: 'collapsed',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// custom onSegmentationAdd if provided
|
||||||
|
const { onSegmentationAdd } = customizationService.getCustomization(
|
||||||
|
'PanelSegmentation.onSegmentationAdd',
|
||||||
|
{
|
||||||
|
id: 'segmentation.onSegmentationAdd',
|
||||||
|
onSegmentationAdd: handlers.onSegmentationAdd,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { disableEditing } = customizationService.getCustomization(
|
||||||
|
'PanelSegmentation.disableEditing',
|
||||||
|
{
|
||||||
|
id: 'default.disableEditing',
|
||||||
|
disableEditing: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { showAddSegment } = customizationService.getCustomization(
|
||||||
|
'PanelSegmentation.showAddSegment',
|
||||||
|
{
|
||||||
|
id: 'default.showAddSegment',
|
||||||
|
showAddSegment: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
|
||||||
|
const { representationData, segmentationId } = segmentation;
|
||||||
|
const { Labelmap } = representationData;
|
||||||
|
const referencedImageIds = Labelmap.referencedImageIds;
|
||||||
|
const firstImageId = referencedImageIds[0];
|
||||||
|
|
||||||
|
const instance = metaData.get('instance', firstImageId);
|
||||||
|
const { SOPInstanceUID, SeriesInstanceUID } = instance;
|
||||||
|
|
||||||
|
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
const isExportable = displaySet.isReconstructable;
|
||||||
|
|
||||||
|
return {
|
||||||
|
segmentationId,
|
||||||
|
isExportable,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SegmentationTable
|
||||||
|
disabled={disabled}
|
||||||
|
data={segmentationsWithRepresentations}
|
||||||
|
mode={SegmentationTableMode}
|
||||||
|
title="Segmentations"
|
||||||
|
exportOptions={exportOptions}
|
||||||
|
disableEditing={disableEditing}
|
||||||
|
onSegmentationAdd={onSegmentationAdd}
|
||||||
|
onSegmentationClick={handlers.onSegmentationClick}
|
||||||
|
onSegmentationDelete={handlers.onSegmentationDelete}
|
||||||
|
showAddSegment={showAddSegment}
|
||||||
|
onSegmentAdd={handlers.onSegmentAdd}
|
||||||
|
onSegmentClick={handlers.onSegmentClick}
|
||||||
|
onSegmentEdit={handlers.onSegmentEdit}
|
||||||
|
onSegmentationEdit={handlers.onSegmentationEdit}
|
||||||
|
onSegmentColorClick={handlers.onSegmentColorClick}
|
||||||
|
onSegmentDelete={handlers.onSegmentDelete}
|
||||||
|
onToggleSegmentVisibility={handlers.onToggleSegmentVisibility}
|
||||||
|
onToggleSegmentLock={handlers.onToggleSegmentLock}
|
||||||
|
onToggleSegmentationRepresentationVisibility={
|
||||||
|
handlers.onToggleSegmentationRepresentationVisibility
|
||||||
|
}
|
||||||
|
onSegmentationDownload={handlers.onSegmentationDownload}
|
||||||
|
storeSegmentation={handlers.storeSegmentation}
|
||||||
|
onSegmentationDownloadRTSS={handlers.onSegmentationDownloadRTSS}
|
||||||
|
setStyle={handlers.setStyle}
|
||||||
|
toggleRenderInactiveSegmentations={handlers.toggleRenderInactiveSegmentations}
|
||||||
|
onSegmentationRemoveFromViewport={handlers.onSegmentationRemoveFromViewport}
|
||||||
|
setFillAlpha={handlers.setFillAlpha}
|
||||||
|
setOutlineWidth={handlers.setOutlineWidth}
|
||||||
|
setRenderFill={handlers.setRenderFill}
|
||||||
|
setRenderOutline={handlers.setRenderOutline}
|
||||||
|
setFillAlphaInactive={handlers.setFillAlphaInactive}
|
||||||
|
renderInactiveSegmentations={handlers.getRenderInactiveSegmentations()}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SegmentationTable.Config />
|
||||||
|
<SegmentationTable.AddSegmentationRow />
|
||||||
|
|
||||||
|
{SegmentationTableMode === 'collapsed' ? (
|
||||||
|
<SegmentationTable.Collapsed>
|
||||||
|
<SegmentationTable.SelectorHeader />
|
||||||
|
<SegmentationTable.AddSegmentRow />
|
||||||
|
<SegmentationTable.Segments />
|
||||||
|
</SegmentationTable.Collapsed>
|
||||||
|
) : (
|
||||||
|
<SegmentationTable.Expanded>
|
||||||
|
<SegmentationTable.Header />
|
||||||
|
{/* <SegmentationTable.AddSegmentRow /> */}
|
||||||
|
<SegmentationTable.Segments />
|
||||||
|
</SegmentationTable.Expanded>
|
||||||
|
)}
|
||||||
|
</SegmentationTable>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -48,6 +48,17 @@ export default class ColorbarService extends PubSubService {
|
|||||||
super(ColorbarService.EVENTS);
|
super(ColorbarService.EVENTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the volume ID for a given identifier by searching through the viewport's volume IDs.
|
||||||
|
* @param viewport - The viewport instance to search volumes in
|
||||||
|
* @param searchId - The identifier to search for within volume IDs
|
||||||
|
* @returns The matching volume ID if found, null otherwise
|
||||||
|
*/
|
||||||
|
private getVolumeIdForIdentifier(viewport, searchId: string): string | null {
|
||||||
|
const volumeIds = viewport.getAllVolumeIds?.() || [];
|
||||||
|
return volumeIds.length > 0 ? volumeIds.find(id => id.includes(searchId)) || null : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`.
|
* Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`.
|
||||||
* This method sets up the colorbar, associates it with the viewport, and applies initial configurations based on the provided options.
|
* This method sets up the colorbar, associates it with the viewport, and applies initial configurations based on the provided options.
|
||||||
@ -59,8 +70,18 @@ export default class ColorbarService extends PubSubService {
|
|||||||
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
|
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
|
||||||
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
|
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
|
|
||||||
|
if (!viewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { element } = viewport;
|
const { element } = viewport;
|
||||||
const actorEntries = viewport.getActors();
|
const actorEntries = viewport.getActors();
|
||||||
|
|
||||||
|
if (!actorEntries || actorEntries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { position, width: thickness, activeColormapName, colormaps } = options;
|
const { position, width: thickness, activeColormapName, colormaps } = options;
|
||||||
|
|
||||||
const numContainers = displaySetInstanceUIDs.length;
|
const numContainers = displaySetInstanceUIDs.length;
|
||||||
@ -74,11 +95,9 @@ export default class ColorbarService extends PubSubService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
|
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
|
||||||
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
|
const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID);
|
||||||
const volumeId = actorEntry?.uid;
|
|
||||||
const properties = viewport?.getProperties(volumeId);
|
const properties = viewport?.getProperties(volumeId);
|
||||||
const colormap = properties?.colormap;
|
const colormap = properties?.colormap;
|
||||||
// if there's an initial colormap set, and no colormap on the viewport, set it
|
|
||||||
if (activeColormapName && !colormap) {
|
if (activeColormapName && !colormap) {
|
||||||
this.setViewportColormap(
|
this.setViewportColormap(
|
||||||
viewportId,
|
viewportId,
|
||||||
@ -196,9 +215,8 @@ export default class ColorbarService extends PubSubService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const setViewportProperties = (viewport, uid) => {
|
const setViewportProperties = (viewport, uid) => {
|
||||||
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
|
const volumeId = this.getVolumeIdForIdentifier(viewport, uid);
|
||||||
const { actor: volumeActor, uid: volumeId } = actorEntry;
|
viewport.setProperties({ colormap }, volumeId);
|
||||||
viewport.setProperties({ colormap, volumeActor }, volumeId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (viewport instanceof StackViewport) {
|
if (viewport instanceof StackViewport) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
import { cache as cs3DCache, Enums, volumeLoader, utilities as utils } from '@cornerstonejs/core';
|
import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
|
||||||
|
|
||||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||||
@ -33,27 +33,11 @@ class CornerstoneCacheService {
|
|||||||
|
|
||||||
public async createViewportData(
|
public async createViewportData(
|
||||||
displaySets: Types.DisplaySet[],
|
displaySets: Types.DisplaySet[],
|
||||||
viewportOptions: Record<string, unknown>,
|
viewportOptions: AppTypes.ViewportGrid.GridViewportOptions,
|
||||||
dataSource: unknown,
|
dataSource: unknown,
|
||||||
initialImageIndex?: number
|
initialImageIndex?: number
|
||||||
): Promise<StackViewportData | VolumeViewportData> {
|
): Promise<StackViewportData | VolumeViewportData> {
|
||||||
let viewportType = viewportOptions.viewportType as string;
|
const viewportType = viewportOptions.viewportType as string;
|
||||||
|
|
||||||
// Todo: Since Cornerstone 3D currently doesn't support segmentation
|
|
||||||
// on stack viewport, we should check if whether the the displaySets
|
|
||||||
// that are about to be displayed are referenced in a segmentation
|
|
||||||
// as a reference volume, if so, we should hang a volume viewport
|
|
||||||
// instead of a stack viewport
|
|
||||||
if (this._shouldRenderSegmentation(displaySets)) {
|
|
||||||
// if the viewport type is volume 3D, we should let it be as it is
|
|
||||||
// Todo: in future here we should kick start the conversion of the
|
|
||||||
// segmentation to closed surface
|
|
||||||
viewportType =
|
|
||||||
viewportType === Enums.ViewportType.VOLUME_3D ? Enums.ViewportType.VOLUME_3D : 'volume';
|
|
||||||
|
|
||||||
// update viewportOptions to reflect the new viewport type
|
|
||||||
viewportOptions.viewportType = viewportType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets);
|
const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets);
|
||||||
let viewportData: StackViewportData | VolumeViewportData;
|
let viewportData: StackViewportData | VolumeViewportData;
|
||||||
@ -86,7 +70,7 @@ class CornerstoneCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async invalidateViewportData(
|
public async invalidateViewportData(
|
||||||
viewportData: VolumeViewportData,
|
viewportData: VolumeViewportData | StackViewportData,
|
||||||
invalidatedDisplaySetInstanceUID: string,
|
invalidatedDisplaySetInstanceUID: string,
|
||||||
dataSource,
|
dataSource,
|
||||||
displaySetService
|
displaySetService
|
||||||
@ -176,9 +160,6 @@ class CornerstoneCacheService {
|
|||||||
): Promise<StackViewportData> {
|
): Promise<StackViewportData> {
|
||||||
const { uiNotificationService } = this.servicesManager.services;
|
const { uiNotificationService } = this.servicesManager.services;
|
||||||
const overlayDisplaySets = displaySets.filter(ds => ds.isOverlayDisplaySet);
|
const overlayDisplaySets = displaySets.filter(ds => ds.isOverlayDisplaySet);
|
||||||
const nonOverlayDisplaySets = displaySets.filter(ds => !ds.isOverlayDisplaySet);
|
|
||||||
|
|
||||||
// load overlays if they are not loaded
|
|
||||||
for (const overlayDisplaySet of overlayDisplaySets) {
|
for (const overlayDisplaySet of overlayDisplaySets) {
|
||||||
if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) {
|
if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) {
|
||||||
const { userAuthenticationService } = this.servicesManager.services;
|
const { userAuthenticationService } = this.servicesManager.services;
|
||||||
@ -187,7 +168,7 @@ class CornerstoneCacheService {
|
|||||||
await overlayDisplaySet.load({ headers });
|
await overlayDisplaySet.load({ headers });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uiNotificationService.show({
|
uiNotificationService.show({
|
||||||
title: 'Error loading overlayDisplaySet',
|
title: 'Error loading displaySet',
|
||||||
message: e.message,
|
message: e.message,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
@ -196,43 +177,43 @@ class CornerstoneCacheService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const displaySet = nonOverlayDisplaySets[0];
|
|
||||||
|
|
||||||
if (displaySet.load && displaySet.load instanceof Function) {
|
|
||||||
const { userAuthenticationService } = this.servicesManager.services;
|
|
||||||
const headers = userAuthenticationService.getAuthorizationHeader();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await displaySet.load({ headers });
|
|
||||||
} catch (e) {
|
|
||||||
uiNotificationService.show({
|
|
||||||
title: 'Error loading displaySet',
|
|
||||||
message: e.message,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
|
|
||||||
|
|
||||||
if (!stackImageIds) {
|
|
||||||
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
|
||||||
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensuring the first non-overlay `displaySet` is always the primary one
|
// Ensuring the first non-overlay `displaySet` is always the primary one
|
||||||
const StackViewportData = [displaySet, ...overlayDisplaySets].map(ds => {
|
const StackViewportData = [];
|
||||||
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = ds;
|
for (const displaySet of displaySets) {
|
||||||
|
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = displaySet;
|
||||||
|
|
||||||
return {
|
if (displaySet.load && displaySet.load instanceof Function) {
|
||||||
|
const { userAuthenticationService } = this.servicesManager.services;
|
||||||
|
const headers = userAuthenticationService.getAuthorizationHeader();
|
||||||
|
try {
|
||||||
|
await displaySet.load({ headers });
|
||||||
|
} catch (e) {
|
||||||
|
uiNotificationService.show({
|
||||||
|
title: 'Error loading displaySet',
|
||||||
|
message: e.message,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
|
||||||
|
|
||||||
|
if (!stackImageIds) {
|
||||||
|
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
||||||
|
// assign imageIds to the displaySet
|
||||||
|
displaySet.imageIds = stackImageIds;
|
||||||
|
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
StackViewportData.push({
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
isCompositeStack,
|
isCompositeStack,
|
||||||
imageIds: stackImageIds,
|
imageIds: stackImageIds,
|
||||||
initialImageIndex,
|
initialImageIndex,
|
||||||
};
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
viewportType,
|
viewportType,
|
||||||
@ -253,6 +234,7 @@ class CornerstoneCacheService {
|
|||||||
for (const displaySet of displaySets) {
|
for (const displaySet of displaySets) {
|
||||||
const { Modality } = displaySet;
|
const { Modality } = displaySet;
|
||||||
const isParametricMap = Modality === 'PMAP';
|
const isParametricMap = Modality === 'PMAP';
|
||||||
|
const isSeg = Modality === 'SEG';
|
||||||
|
|
||||||
// Don't create volumes for the displaySets that have custom load
|
// Don't create volumes for the displaySets that have custom load
|
||||||
// function (e.g., SEG, RT, since they rely on the reference volumes
|
// function (e.g., SEG, RT, since they rely on the reference volumes
|
||||||
@ -295,7 +277,7 @@ class CornerstoneCacheService {
|
|||||||
|
|
||||||
// Parametric maps do not have image ids but they already have volume data
|
// Parametric maps do not have image ids but they already have volume data
|
||||||
// therefore a new volume should not be created.
|
// therefore a new volume should not be created.
|
||||||
if (!isParametricMap && (!volumeImageIds || !volume)) {
|
if (!isParametricMap && !isSeg && (!volumeImageIds || !volume)) {
|
||||||
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
|
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
|
||||||
|
|
||||||
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
||||||
@ -303,6 +285,9 @@ class CornerstoneCacheService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds);
|
this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds);
|
||||||
|
|
||||||
|
// Add imageIds to the displaySet for volumes
|
||||||
|
displaySet.imageIds = volumeImageIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
volumeData.push({
|
volumeData.push({
|
||||||
@ -321,39 +306,15 @@ class CornerstoneCacheService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _shouldRenderSegmentation(displaySets) {
|
|
||||||
const { segmentationService, displaySetService } = this.servicesManager.services;
|
|
||||||
|
|
||||||
const viewportDisplaySetInstanceUIDs = displaySets.map(
|
|
||||||
({ displaySetInstanceUID }) => displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
// check inside segmentations if any of them are referencing the displaySets
|
|
||||||
// that are about to be displayed
|
|
||||||
const segmentations = segmentationService.getSegmentations();
|
|
||||||
|
|
||||||
for (const segmentation of segmentations) {
|
|
||||||
const segDisplaySetInstanceUID = segmentation.displaySetInstanceUID;
|
|
||||||
const segDisplaySet = displaySetService.getDisplaySetByUID(segDisplaySetInstanceUID);
|
|
||||||
|
|
||||||
const instance = segDisplaySet.instances?.[0] || segDisplaySet.instance;
|
|
||||||
|
|
||||||
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
|
||||||
viewportDisplaySetInstanceUIDs,
|
|
||||||
instance?.FrameOfReferenceUID || segDisplaySet.FrameOfReferenceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldDisplaySeg) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
|
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
|
||||||
return dataSource.getImageIdsForDisplaySet(displaySet);
|
return dataSource.getImageIdsForDisplaySet(displaySet);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
|
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
|
||||||
|
if (displaySet.imageIds) {
|
||||||
|
return displaySet.imageIds;
|
||||||
|
}
|
||||||
|
|
||||||
const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
||||||
|
|
||||||
return stackImageIds;
|
return stackImageIds;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,76 +0,0 @@
|
|||||||
import { Enums as csToolsEnums, Types as cstTypes } from '@cornerstonejs/tools';
|
|
||||||
import { Types } from '@cornerstonejs/core';
|
|
||||||
|
|
||||||
type SegmentationConfig = cstTypes.LabelmapTypes.LabelmapConfig & {
|
|
||||||
renderInactiveSegmentations: boolean;
|
|
||||||
brushSize: number;
|
|
||||||
brushThresholdGate: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Segment = {
|
|
||||||
// the label for the segment
|
|
||||||
label: string;
|
|
||||||
// the index of the segment in the segmentation
|
|
||||||
segmentIndex: number;
|
|
||||||
// the color of the segment
|
|
||||||
color: Types.Point3;
|
|
||||||
// the opacity of the segment
|
|
||||||
opacity: number;
|
|
||||||
// whether the segment is visible
|
|
||||||
isVisible: boolean;
|
|
||||||
// whether the segment is locked
|
|
||||||
isLocked: boolean;
|
|
||||||
// display texts
|
|
||||||
displayText?: string[];
|
|
||||||
// The name of algorithm used to generate the segment. (0062,0009)
|
|
||||||
algorithmName?: string;
|
|
||||||
// Type of algorithm used to generate the segment. (0062,0008)
|
|
||||||
algorithmType?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Segmentation = {
|
|
||||||
// active segment index is the index of the segment that is currently being edited.
|
|
||||||
activeSegmentIndex: number;
|
|
||||||
// colorLUTIndex is the index of the color LUT that is currently being used.
|
|
||||||
colorLUTIndex: number;
|
|
||||||
// if segmentation contains any data (often calculated from labelmap)
|
|
||||||
cachedStats: Record<string, any>;
|
|
||||||
// displaySetInstanceUID
|
|
||||||
displaySetInstanceUID: string;
|
|
||||||
// displayText is the text that is displayed on the segmentation panel (often derived from the data)
|
|
||||||
displayText?: string[];
|
|
||||||
// the id of the segmentation
|
|
||||||
id: string;
|
|
||||||
// if the segmentation is the active segmentation being used in the viewer
|
|
||||||
isActive: boolean;
|
|
||||||
// if the segmentation is visible in the viewer
|
|
||||||
isVisible: boolean;
|
|
||||||
// the frame of reference UID of the segmentation
|
|
||||||
FrameOfReferenceUID: string;
|
|
||||||
// the label of the segmentation
|
|
||||||
label: string;
|
|
||||||
// the number of segments in the segmentation
|
|
||||||
segmentCount: number;
|
|
||||||
// the array of segments with their details, [null, segment1, segment2, ...]
|
|
||||||
segments: Array<Segment>;
|
|
||||||
// the set of segments that are locked
|
|
||||||
segmentsLocked: Array<number>;
|
|
||||||
// whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
|
|
||||||
// but hydrated SEG -> segmentation that is persisted in the store)
|
|
||||||
hydrated: boolean;
|
|
||||||
// the type of the segmentation (e.g., Labelmap etc.)
|
|
||||||
type: csToolsEnums.SegmentationRepresentations;
|
|
||||||
// the segmentation representation data
|
|
||||||
representationData: SegmentationRepresentationData;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LabelmapSegmentationData = {
|
|
||||||
volumeId: string;
|
|
||||||
referencedVolumeId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SegmentationRepresentationData = {
|
|
||||||
LABELMAP?: LabelmapSegmentationData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type { SegmentationConfig, Segment, Segmentation };
|
|
||||||
@ -2,6 +2,7 @@ import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs
|
|||||||
import { getRenderingEngines, utilities } from '@cornerstonejs/core';
|
import { getRenderingEngines, utilities } from '@cornerstonejs/core';
|
||||||
|
|
||||||
import { pubSubServiceInterface, Types } from '@ohif/core';
|
import { pubSubServiceInterface, Types } from '@ohif/core';
|
||||||
|
import createHydrateSegmentationSynchronizer from './createHydrateSegmentationSynchronizer';
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
||||||
@ -27,6 +28,7 @@ const VOI = 'voi';
|
|||||||
const ZOOMPAN = 'zoompan';
|
const ZOOMPAN = 'zoompan';
|
||||||
const STACKIMAGE = 'stackimage';
|
const STACKIMAGE = 'stackimage';
|
||||||
const IMAGE_SLICE = 'imageslice';
|
const IMAGE_SLICE = 'imageslice';
|
||||||
|
const HYDRATE_SEG = 'hydrateseg';
|
||||||
|
|
||||||
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
|
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
|
||||||
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
|
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
|
||||||
@ -51,6 +53,7 @@ export default class SyncGroupService {
|
|||||||
// handles both stack and volume viewports
|
// handles both stack and volume viewports
|
||||||
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
|
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
|
||||||
[IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer,
|
[IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer,
|
||||||
|
[HYDRATE_SEG]: createHydrateSegmentationSynchronizer,
|
||||||
};
|
};
|
||||||
|
|
||||||
synchronizersByType: { [key: string]: Synchronizer[] } = {};
|
synchronizersByType: { [key: string]: Synchronizer[] } = {};
|
||||||
@ -66,18 +69,18 @@ export default class SyncGroupService {
|
|||||||
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
|
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
|
||||||
// Initialize if not already done
|
// Initialize if not already done
|
||||||
this.synchronizersByType[type] = this.synchronizersByType[type] || [];
|
this.synchronizersByType[type] = this.synchronizersByType[type] || [];
|
||||||
|
|
||||||
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
|
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
|
||||||
|
|
||||||
if (syncCreator) {
|
if (syncCreator) {
|
||||||
const synchronizer = syncCreator(id, options);
|
// Pass the servicesManager along with other parameters
|
||||||
|
const synchronizer = syncCreator(id, { ...options, servicesManager: this.servicesManager });
|
||||||
|
|
||||||
if (synchronizer) {
|
if (synchronizer) {
|
||||||
this.synchronizersByType[type].push(synchronizer);
|
this.synchronizersByType[type].push(synchronizer);
|
||||||
return synchronizer;
|
return synchronizer;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Unknown synchronizer type: ${type}, id: ${id}`);
|
console.debug(`Unknown synchronizer type: ${type}, id: ${id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
import { Types, getEnabledElementByViewportId } from '@cornerstonejs/core';
|
||||||
|
import {
|
||||||
|
SynchronizerManager,
|
||||||
|
Synchronizer,
|
||||||
|
Enums,
|
||||||
|
Types as ToolsTypes,
|
||||||
|
} from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
const { createSynchronizer } = SynchronizerManager;
|
||||||
|
const { SEGMENTATION_REPRESENTATION_ADDED } = Enums.Events;
|
||||||
|
|
||||||
|
export default function createHydrateSegmentationSynchronizer(
|
||||||
|
synchronizerName: string,
|
||||||
|
{ servicesManager, ...options }: { servicesManager: AppTypes.ServicesManager; options }
|
||||||
|
): Synchronizer {
|
||||||
|
const stackImageSynchronizer = createSynchronizer(
|
||||||
|
synchronizerName,
|
||||||
|
SEGMENTATION_REPRESENTATION_ADDED,
|
||||||
|
(synchronizerInstance, sourceViewport, targetViewport, sourceEvent) =>
|
||||||
|
segmentationRepresentationModifiedCallback(
|
||||||
|
synchronizerInstance,
|
||||||
|
sourceViewport,
|
||||||
|
targetViewport,
|
||||||
|
sourceEvent,
|
||||||
|
{ servicesManager, options }
|
||||||
|
),
|
||||||
|
{
|
||||||
|
eventSource: 'eventTarget',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return stackImageSynchronizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentationRepresentationModifiedCallback = async (
|
||||||
|
synchronizerInstance: Synchronizer,
|
||||||
|
sourceViewport: Types.IViewportId,
|
||||||
|
targetViewport: Types.IViewportId,
|
||||||
|
sourceEvent: Event,
|
||||||
|
{ servicesManager, options }: { servicesManager: AppTypes.ServicesManager; options: unknown }
|
||||||
|
) => {
|
||||||
|
const event = sourceEvent as ToolsTypes.EventTypes.SegmentationRepresentationModifiedEventType;
|
||||||
|
|
||||||
|
const { segmentationId, viewportId } = event.detail;
|
||||||
|
const { segmentationService, hangingProtocolService } = servicesManager.services;
|
||||||
|
|
||||||
|
const targetViewportId = targetViewport.viewportId;
|
||||||
|
|
||||||
|
const { viewport } = getEnabledElementByViewportId(targetViewportId);
|
||||||
|
|
||||||
|
const targetFrameOfReferenceUID = viewport.getFrameOfReferenceUID();
|
||||||
|
|
||||||
|
if (!targetFrameOfReferenceUID) {
|
||||||
|
console.debug('No frame of reference UID found for the target viewport');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetViewportRepresentation = segmentationService.getSegmentationRepresentations(
|
||||||
|
targetViewportId,
|
||||||
|
{ segmentationId }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetViewportRepresentation.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// whatever type the source viewport has, we need to add that to the target viewport
|
||||||
|
const sourceViewportRepresentation = segmentationService.getSegmentationRepresentations(
|
||||||
|
sourceViewport.viewportId,
|
||||||
|
{ segmentationId }
|
||||||
|
);
|
||||||
|
|
||||||
|
const type = sourceViewportRepresentation[0].type;
|
||||||
|
|
||||||
|
await segmentationService.addSegmentationRepresentation(targetViewportId, {
|
||||||
|
segmentationId,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -7,7 +7,7 @@ export type ActionComponentInfo = {
|
|||||||
id: string;
|
id: string;
|
||||||
component: ReactNode;
|
component: ReactNode;
|
||||||
location: ViewportActionCornersLocations;
|
location: ViewportActionCornersLocations;
|
||||||
indexPriority: number;
|
indexPriority?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ViewportActionCornersService extends PubSubService {
|
class ViewportActionCornersService extends PubSubService {
|
||||||
@ -33,21 +33,21 @@ class ViewportActionCornersService extends PubSubService {
|
|||||||
|
|
||||||
public setServiceImplementation({
|
public setServiceImplementation({
|
||||||
getState: getStateImplementation,
|
getState: getStateImplementation,
|
||||||
setComponent: setComponentImplementation,
|
addComponent: addComponentImplementation,
|
||||||
setComponents: setComponentsImplementation,
|
addComponents: addComponentsImplementation,
|
||||||
clear: clearImplementation,
|
clear: clearComponentsImplementation,
|
||||||
}): void {
|
}): void {
|
||||||
if (getStateImplementation) {
|
if (getStateImplementation) {
|
||||||
this.serviceImplementation._getState = getStateImplementation;
|
this.serviceImplementation._getState = getStateImplementation;
|
||||||
}
|
}
|
||||||
if (setComponentImplementation) {
|
if (addComponentImplementation) {
|
||||||
this.serviceImplementation._setComponent = setComponentImplementation;
|
this.serviceImplementation._addComponent = addComponentImplementation;
|
||||||
}
|
}
|
||||||
if (setComponentsImplementation) {
|
if (addComponentsImplementation) {
|
||||||
this.serviceImplementation._setComponents = setComponentsImplementation;
|
this.serviceImplementation._addComponents = addComponentsImplementation;
|
||||||
}
|
}
|
||||||
if (clearImplementation) {
|
if (clearComponentsImplementation) {
|
||||||
this.serviceImplementation._clear = clearImplementation;
|
this.serviceImplementation._clear = clearComponentsImplementation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,12 +55,12 @@ class ViewportActionCornersService extends PubSubService {
|
|||||||
return this.serviceImplementation._getState();
|
return this.serviceImplementation._getState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setComponent(component: ActionComponentInfo) {
|
public addComponent(component: ActionComponentInfo) {
|
||||||
this.serviceImplementation._setComponent(component);
|
this.serviceImplementation._addComponent(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setComponents(components: Array<ActionComponentInfo>) {
|
public addComponents(components: Array<ActionComponentInfo>) {
|
||||||
this.serviceImplementation._setComponents(components);
|
this.serviceImplementation._addComponents(components);
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear(viewportId: string) {
|
public clear(viewportId: string) {
|
||||||
|
|||||||
@ -18,9 +18,20 @@ import { IViewportService } from './IViewportService';
|
|||||||
import { RENDERING_ENGINE_ID } from './constants';
|
import { RENDERING_ENGINE_ID } from './constants';
|
||||||
import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport';
|
import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport';
|
||||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||||
import { LutPresentation, PositionPresentation, Presentations } from '../../types/Presentation';
|
import {
|
||||||
|
LutPresentation,
|
||||||
|
PositionPresentation,
|
||||||
|
Presentations,
|
||||||
|
SegmentationPresentation,
|
||||||
|
SegmentationPresentationItem,
|
||||||
|
} from '../../types/Presentation';
|
||||||
|
|
||||||
import JumpPresets from '../../utils/JumpPresets';
|
import JumpPresets from '../../utils/JumpPresets';
|
||||||
|
import { ViewportProperties } from '@cornerstonejs/core/types';
|
||||||
|
import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
|
||||||
|
import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
|
||||||
|
import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
|
||||||
|
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
|
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
|
||||||
@ -176,11 +187,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
* @param presentations - The presentations to apply to the viewport.
|
* @param presentations - The presentations to apply to the viewport.
|
||||||
* @param viewportInfo - Contains a view reference for immediate application
|
* @param viewportInfo - Contains a view reference for immediate application
|
||||||
*/
|
*/
|
||||||
public setPresentations(
|
public setPresentations(viewportId: string, presentations: Presentations): void {
|
||||||
viewportId: string,
|
|
||||||
presentations?: Presentations,
|
|
||||||
viewportInfo?: ViewportInfo
|
|
||||||
): void {
|
|
||||||
const viewport = this.getCornerstoneViewport(viewportId) as
|
const viewport = this.getCornerstoneViewport(viewportId) as
|
||||||
| Types.IStackViewport
|
| Types.IStackViewport
|
||||||
| Types.IVolumeViewport;
|
| Types.IVolumeViewport;
|
||||||
@ -189,83 +196,113 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { lutPresentation, positionPresentation } = presentations;
|
const { lutPresentation, positionPresentation, segmentationPresentation } = presentations;
|
||||||
if (lutPresentation) {
|
|
||||||
const { presentation } = lutPresentation;
|
// Always set the segmentation presentation first, since there might be some
|
||||||
if (viewport instanceof BaseVolumeViewport) {
|
// lutpresentation states that need to be set on the segmentation
|
||||||
if (presentation instanceof Map) {
|
// Todo: i think we should even await this
|
||||||
presentation.forEach((properties, volumeId) => {
|
this._setSegmentationPresentation(viewport, segmentationPresentation);
|
||||||
viewport.setProperties(properties, volumeId);
|
|
||||||
});
|
this._setLutPresentation(viewport, lutPresentation);
|
||||||
} else {
|
this._setPositionPresentation(viewport, { ...positionPresentation, viewportId });
|
||||||
viewport.setProperties(presentation);
|
}
|
||||||
}
|
|
||||||
} else {
|
/**
|
||||||
viewport.setProperties(presentation);
|
* Stores the presentation state for a given viewport inside the
|
||||||
}
|
* each store. This is used to persist the presentation state
|
||||||
|
* across different scenarios e.g., when the viewport is changing the
|
||||||
|
* display set, or when the viewport is moving to a different layout.
|
||||||
|
*
|
||||||
|
* @param viewportId The ID of the viewport.
|
||||||
|
*/
|
||||||
|
public storePresentation({ viewportId }) {
|
||||||
|
const presentationIds = this.getPresentationIds(viewportId);
|
||||||
|
const { syncGroupService } = this.servicesManager.services;
|
||||||
|
const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
|
||||||
|
|
||||||
|
if (!presentationIds || Object.keys(presentationIds).length === 0) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewRef = viewportInfo?.getViewReference() || positionPresentation?.viewReference;
|
const { lutPresentationId, positionPresentationId, segmentationPresentationId } =
|
||||||
if (viewRef) {
|
presentationIds;
|
||||||
viewport.setViewReference(viewRef);
|
|
||||||
|
const positionPresentation = this._getPositionPresentation(viewportId);
|
||||||
|
const lutPresentation = this._getLutPresentation(viewportId);
|
||||||
|
const segmentationPresentation = this._getSegmentationPresentation(viewportId);
|
||||||
|
|
||||||
|
const { setLutPresentation } = useLutPresentationStore.getState();
|
||||||
|
const { setPositionPresentation } = usePositionPresentationStore.getState();
|
||||||
|
const { setSynchronizers } = useSynchronizersStore.getState();
|
||||||
|
const { setSegmentationPresentation } = useSegmentationPresentationStore.getState();
|
||||||
|
|
||||||
|
if (lutPresentationId) {
|
||||||
|
setLutPresentation(lutPresentationId, lutPresentation);
|
||||||
}
|
}
|
||||||
if (positionPresentation?.position) {
|
|
||||||
viewport.setViewPresentation(positionPresentation.position);
|
if (positionPresentationId) {
|
||||||
|
setPositionPresentation(positionPresentationId, positionPresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segmentationPresentationId) {
|
||||||
|
setSegmentationPresentation(segmentationPresentationId, segmentationPresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (synchronizers?.length) {
|
||||||
|
setSynchronizers(
|
||||||
|
viewportId,
|
||||||
|
synchronizers.map(synchronizer => ({
|
||||||
|
id: synchronizer.id,
|
||||||
|
sourceViewports: [...synchronizer.getSourceViewports()],
|
||||||
|
targetViewports: [...synchronizer.getTargetViewports()],
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the position presentation information for a given viewport.
|
* Retrieves the presentations for a given viewport.
|
||||||
* @param viewportId The ID of the viewport.
|
* @param viewportId - The ID of the viewport.
|
||||||
* @returns The position presentation object containing various properties
|
* @returns The presentations for the viewport.
|
||||||
* such as ID, viewport type, initial image index, view plane normal, view up, zoom, and pan.
|
|
||||||
*/
|
*/
|
||||||
public getPositionPresentation(viewportId: string): PositionPresentation {
|
public getPresentations(viewportId: string): Presentations {
|
||||||
|
const positionPresentation = this._getPositionPresentation(viewportId);
|
||||||
|
const lutPresentation = this._getLutPresentation(viewportId);
|
||||||
|
const segmentationPresentation = this._getSegmentationPresentation(viewportId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
positionPresentation,
|
||||||
|
lutPresentation,
|
||||||
|
segmentationPresentation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPresentationIds(viewportId: string): AppTypes.PresentationIds | null {
|
||||||
const viewportInfo = this.viewportsById.get(viewportId);
|
const viewportInfo = this.viewportsById.get(viewportId);
|
||||||
if (!viewportInfo) {
|
if (!viewportInfo) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const presentationIds = viewportInfo.getPresentationIds();
|
return viewportInfo.getPresentationIds();
|
||||||
|
}
|
||||||
if (!presentationIds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { positionPresentationId } = presentationIds;
|
|
||||||
|
|
||||||
|
private _getPositionPresentation(viewportId: string): PositionPresentation {
|
||||||
const csViewport = this.getCornerstoneViewport(viewportId);
|
const csViewport = this.getCornerstoneViewport(viewportId);
|
||||||
if (!csViewport) {
|
if (!csViewport) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewportInfo = this.viewportsById.get(viewportId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: positionPresentationId,
|
|
||||||
viewportType: viewportInfo.getViewportType(),
|
viewportType: viewportInfo.getViewportType(),
|
||||||
viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(),
|
viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(),
|
||||||
position: csViewport.getViewPresentation({ pan: true, zoom: true }),
|
viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }),
|
||||||
|
viewportId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private _getLutPresentation(viewportId: string): LutPresentation {
|
||||||
* Retrieves the LUT (Lookup Table) presentation for a given viewport.
|
|
||||||
* @param viewportId The ID of the viewport.
|
|
||||||
* @returns The LUT presentation object, or undefined if the viewport does not exist.
|
|
||||||
*/
|
|
||||||
public getLutPresentation(viewportId: string): LutPresentation {
|
|
||||||
const viewportInfo = this.viewportsById.get(viewportId);
|
|
||||||
if (!viewportInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const presentationIds = viewportInfo.getPresentationIds();
|
|
||||||
|
|
||||||
if (!presentationIds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { lutPresentationId } = presentationIds;
|
|
||||||
|
|
||||||
const csViewport = this.getCornerstoneViewport(viewportId) as
|
const csViewport = this.getCornerstoneViewport(viewportId) as
|
||||||
| Types.IStackViewport
|
| Types.IStackViewport
|
||||||
| Types.IVolumeViewport;
|
| Types.IVolumeViewport;
|
||||||
@ -275,116 +312,39 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleanProperties = properties => {
|
const cleanProperties = properties => {
|
||||||
if (properties.isComputedVOI) {
|
if (properties?.isComputedVOI) {
|
||||||
delete properties.voiRange;
|
delete properties?.voiRange;
|
||||||
delete properties.VOILUTFunction;
|
delete properties?.VOILUTFunction;
|
||||||
}
|
}
|
||||||
return properties;
|
return properties;
|
||||||
};
|
};
|
||||||
|
|
||||||
const presentation =
|
const properties =
|
||||||
csViewport instanceof BaseVolumeViewport
|
csViewport instanceof BaseVolumeViewport
|
||||||
? new Map()
|
? new Map()
|
||||||
: cleanProperties(csViewport.getProperties());
|
: cleanProperties(csViewport.getProperties());
|
||||||
|
|
||||||
if (presentation instanceof Map) {
|
if (properties instanceof Map) {
|
||||||
csViewport.getActors().forEach(({ uid: volumeId }) => {
|
const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds();
|
||||||
const properties = cleanProperties(csViewport.getProperties(volumeId));
|
volumeIds?.forEach(volumeId => {
|
||||||
presentation.set(volumeId, properties);
|
const csProps = cleanProperties(csViewport.getProperties(volumeId));
|
||||||
|
properties.set(volumeId, csProps);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
id: lutPresentationId,
|
|
||||||
viewportType: viewportInfo.getViewportType(),
|
|
||||||
presentation,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the presentations for a given viewport.
|
|
||||||
* @param viewportId - The ID of the viewport.
|
|
||||||
* @returns The presentations for the viewport.
|
|
||||||
*/
|
|
||||||
public getPresentations(viewportId: string): Presentations {
|
|
||||||
const viewportInfo = this.viewportsById.get(viewportId);
|
const viewportInfo = this.viewportsById.get(viewportId);
|
||||||
if (!viewportInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const positionPresentation = this.getPositionPresentation(viewportId);
|
|
||||||
const lutPresentation = this.getLutPresentation(viewportId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
positionPresentation,
|
viewportType: viewportInfo.getViewportType(),
|
||||||
lutPresentation,
|
properties,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private _getSegmentationPresentation(viewportId: string): SegmentationPresentation {
|
||||||
* Stores the presentation state for a given viewport inside the
|
const { segmentationService } = this.servicesManager.services;
|
||||||
* stateSyncService. This is used to persist the presentation state
|
|
||||||
* across different scenarios e.g., when the viewport is changing the
|
|
||||||
* display set, or when the viewport is moving to a different layout.
|
|
||||||
*
|
|
||||||
* @param viewportId The ID of the viewport.
|
|
||||||
*/
|
|
||||||
public storePresentation({ viewportId }) {
|
|
||||||
let presentations = null as Presentations;
|
|
||||||
try {
|
|
||||||
presentations = this.getPresentations(viewportId);
|
|
||||||
if (!presentations?.positionPresentation && !presentations?.lutPresentation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { stateSyncService, syncGroupService } = this.servicesManager.services;
|
const presentation = segmentationService.getPresentation(viewportId);
|
||||||
|
return presentation;
|
||||||
const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
|
|
||||||
|
|
||||||
const { positionPresentationStore, synchronizersStore, lutPresentationStore } =
|
|
||||||
stateSyncService.getState();
|
|
||||||
|
|
||||||
const { lutPresentation, positionPresentation } = presentations;
|
|
||||||
const { id: positionPresentationId } = positionPresentation;
|
|
||||||
const { id: lutPresentationId } = lutPresentation;
|
|
||||||
|
|
||||||
const updateStore = (store, id, value) => ({ ...store, [id]: value });
|
|
||||||
|
|
||||||
const newState = {} as { [key: string]: any };
|
|
||||||
|
|
||||||
if (lutPresentationId) {
|
|
||||||
newState.lutPresentationStore = updateStore(
|
|
||||||
lutPresentationStore,
|
|
||||||
lutPresentationId,
|
|
||||||
lutPresentation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (positionPresentationId) {
|
|
||||||
newState.positionPresentationStore = updateStore(
|
|
||||||
positionPresentationStore,
|
|
||||||
positionPresentationId,
|
|
||||||
positionPresentation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (synchronizers?.length) {
|
|
||||||
newState.synchronizersStore = updateStore(
|
|
||||||
synchronizersStore,
|
|
||||||
viewportId,
|
|
||||||
synchronizers.map(synchronizer => ({
|
|
||||||
id: synchronizer.id,
|
|
||||||
sourceViewports: [...synchronizer.getSourceViewports()],
|
|
||||||
targetViewports: [...synchronizer.getTargetViewports()],
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
stateSyncService.store(newState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -423,6 +383,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
// and we would lose the presentation.
|
// and we would lose the presentation.
|
||||||
this.storePresentation({ viewportId: viewportInfo.getViewportId() });
|
this.storePresentation({ viewportId: viewportInfo.getViewportId() });
|
||||||
|
|
||||||
|
// Todo: i don't like this here, move it
|
||||||
|
this.servicesManager.services.segmentationService.clearSegmentationRepresentations(
|
||||||
|
viewportInfo.getViewportId()
|
||||||
|
);
|
||||||
|
|
||||||
if (!viewportInfo) {
|
if (!viewportInfo) {
|
||||||
throw new Error('element is not enabled for the given viewportId');
|
throw new Error('element is not enabled for the given viewportId');
|
||||||
}
|
}
|
||||||
@ -431,8 +396,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
// since those are the newly set ones, we set them here so that it handles defaults
|
// since those are the newly set ones, we set them here so that it handles defaults
|
||||||
const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
|
const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
|
||||||
// Specify an over-ride for the viewport type, even though it is in the public
|
// Specify an over-ride for the viewport type, even though it is in the public
|
||||||
// viewport options, because the one in the public viewport options is a suggestion
|
// viewport options, because the one in the viewportData is a requirement based on the
|
||||||
// for initial view, whereas the one in viewportData is a requirement based on the
|
|
||||||
// type of data being displayed.
|
// type of data being displayed.
|
||||||
const viewportOptions = viewportInfo.setPublicViewportOptions(
|
const viewportOptions = viewportInfo.setPublicViewportOptions(
|
||||||
publicViewportOptions,
|
publicViewportOptions,
|
||||||
@ -647,13 +611,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
properties.colormap = colormap ?? properties.colormap;
|
properties.colormap = colormap ?? properties.colormap;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._handleOverlays(viewport);
|
viewport.element.addEventListener(csEnums.Events.VIEWPORT_NEW_IMAGE_SET, evt => {
|
||||||
|
const { element } = evt.detail;
|
||||||
|
|
||||||
if (!imageIds?.length) {
|
if (element !== viewport.element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
|
csToolsUtils.stackContextPrefetch.enable(element);
|
||||||
|
});
|
||||||
|
|
||||||
|
let imageIdsToSet = imageIds;
|
||||||
|
const res = this._processExtraDisplaySetsForViewport(viewport);
|
||||||
|
imageIdsToSet = res?.imageIds ?? imageIdsToSet;
|
||||||
|
|
||||||
|
return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => {
|
||||||
viewport.setProperties({ ...properties });
|
viewport.setProperties({ ...properties });
|
||||||
this.setPresentations(viewport.id, presentations, viewportInfo);
|
this.setPresentations(viewport.id, presentations, viewportInfo);
|
||||||
if (displayArea) {
|
if (displayArea) {
|
||||||
@ -702,7 +674,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
const lastSliceIndex = numberOfSlices - 1;
|
const lastSliceIndex = numberOfSlices - 1;
|
||||||
|
|
||||||
if (imageIndex !== undefined) {
|
if (imageIndex !== undefined) {
|
||||||
return csToolsUtils.clip(imageIndex, 0, lastSliceIndex);
|
return csUtils.clip(imageIndex, 0, lastSliceIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preset === JumpPresets.First) {
|
if (preset === JumpPresets.First) {
|
||||||
@ -764,7 +736,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
|
|
||||||
const displaySetOptions = displaySetOptionsArray[index];
|
const displaySetOptions = displaySetOptionsArray[index];
|
||||||
const { volumeId } = volume;
|
const { volumeId } = volume;
|
||||||
|
|
||||||
volumeInputArray.push({
|
volumeInputArray.push({
|
||||||
imageIds,
|
imageIds,
|
||||||
volumeId,
|
volumeId,
|
||||||
@ -775,8 +746,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
|
|
||||||
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
|
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
|
||||||
|
|
||||||
const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus.loaded);
|
const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus?.loaded);
|
||||||
|
|
||||||
if (volumesNotLoaded.length) {
|
if (volumesNotLoaded.length) {
|
||||||
if (hangingProtocolService.getShouldPerformCustomImageLoad()) {
|
if (hangingProtocolService.getShouldPerformCustomImageLoad()) {
|
||||||
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
|
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
|
||||||
@ -787,7 +757,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
volumesNotLoaded.forEach(volume => {
|
volumesNotLoaded.forEach(volume => {
|
||||||
if (!volume.loadStatus.loading) {
|
if (!volume.loadStatus?.loading && volume.load instanceof Function) {
|
||||||
volume.load();
|
volume.load();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -798,8 +768,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async setVolumesForViewport(viewport, volumeInputArray, presentations) {
|
public async setVolumesForViewport(viewport, volumeInputArray, presentations) {
|
||||||
const { displaySetService, toolGroupService, viewportGridService } =
|
const { displaySetService, viewportGridService } = this.servicesManager.services;
|
||||||
this.servicesManager.services;
|
|
||||||
|
|
||||||
const viewportInfo = this.getViewportInfo(viewport.id);
|
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||||
@ -811,7 +780,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
const { volumeId } = volumeInput;
|
const { volumeId } = volumeInput;
|
||||||
const displaySetOption = displaySetOptions[index];
|
const displaySetOption = displaySetOptions[index];
|
||||||
const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
|
const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
|
||||||
const properties = {};
|
const properties = {} as ViewportProperties;
|
||||||
|
|
||||||
if (voi && (voi.windowWidth || voi.windowCenter)) {
|
if (voi && (voi.windowWidth || voi.windowCenter)) {
|
||||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||||
@ -836,22 +805,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
return { properties, volumeId };
|
return { properties, volumeId };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// For SEG and RT viewports
|
||||||
|
this._processExtraDisplaySetsForViewport(viewport);
|
||||||
|
|
||||||
await viewport.setVolumes(volumeInputArray);
|
await viewport.setVolumes(volumeInputArray);
|
||||||
|
|
||||||
volumesProperties.forEach(({ properties, volumeId }) => {
|
volumesProperties.forEach(({ properties, volumeId }) => {
|
||||||
viewport.setProperties(properties, volumeId);
|
viewport.setProperties(properties, volumeId);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setPresentations(viewport.id, presentations, viewportInfo);
|
this.setPresentations(viewport.id, presentations, viewportInfo);
|
||||||
|
|
||||||
this._handleOverlays(viewport);
|
|
||||||
|
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
|
||||||
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
|
||||||
|
|
||||||
const imageIndex = this._getInitialImageIndexForViewport(viewportInfo);
|
const imageIndex = this._getInitialImageIndexForViewport(viewportInfo);
|
||||||
|
|
||||||
if (imageIndex !== undefined) {
|
if (imageIndex !== undefined) {
|
||||||
csToolsUtils.jumpToSlice(viewport.element, {
|
csUtils.jumpToSlice(viewport.element, {
|
||||||
imageIndex,
|
imageIndex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -863,7 +831,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleOverlays(viewport: Types.IStackViewport | Types.IVolumeViewport) {
|
private _processExtraDisplaySetsForViewport(
|
||||||
|
viewport: Types.IStackViewport | Types.IVolumeViewport
|
||||||
|
) {
|
||||||
const { displaySetService } = this.servicesManager.services;
|
const { displaySetService } = this.servicesManager.services;
|
||||||
|
|
||||||
// load any secondary displaySets
|
// load any secondary displaySets
|
||||||
@ -877,104 +847,41 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality)
|
displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (segOrRTSOverlayDisplaySet) {
|
// if it is only the overlay displaySet, then we need to get the reference
|
||||||
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
|
// displaySet imageIds and set them as the imageIds for the viewport,
|
||||||
} else {
|
// here we can do some logic if the reference is missing
|
||||||
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
|
// then find the most similar match of displaySet instead
|
||||||
// and we can look into hydrated segmentations to check if any of them are
|
if (!segOrRTSOverlayDisplaySet) {
|
||||||
// associated with the primary displaySet
|
return;
|
||||||
|
|
||||||
// get segmentations only returns the hydrated segmentations
|
|
||||||
this._addSegmentationRepresentationToToolGroupIfNecessary(displaySetInstanceUIDs, viewport);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const referenceDisplaySet = displaySetService.getDisplaySetByUID(
|
||||||
|
segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID
|
||||||
|
);
|
||||||
|
const imageIds = referenceDisplaySet.images.map(image => image.imageId);
|
||||||
|
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
|
||||||
|
return { imageIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addSegmentationRepresentationToToolGroupIfNecessary(
|
private addOverlayRepresentationForDisplaySet(
|
||||||
displaySetInstanceUIDs: string[],
|
displaySet: OhifTypes.DisplaySet,
|
||||||
viewport: any
|
viewport: Types.IViewport
|
||||||
) {
|
) {
|
||||||
const { segmentationService, toolGroupService } = this.servicesManager.services;
|
const { segmentationService } = this.servicesManager.services;
|
||||||
|
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
|
||||||
|
|
||||||
// this only returns hydrated segmentations
|
|
||||||
const segmentations = segmentationService.getSegmentations();
|
|
||||||
|
|
||||||
for (const segmentation of segmentations) {
|
|
||||||
const toolGroupSegmentationRepresentations =
|
|
||||||
segmentationService.getSegmentationRepresentationsForToolGroup(toolGroup.id) || [];
|
|
||||||
|
|
||||||
// if there is already a segmentation representation for this segmentation
|
|
||||||
// for this toolGroup, don't bother at all
|
|
||||||
const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find(
|
|
||||||
representation => representation.segmentationId === segmentation.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isSegmentationInToolGroup) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, check if the hydrated segmentations are in the same FrameOfReferenceUID
|
|
||||||
// as the primary displaySet, if so add the representation (since it was not there)
|
|
||||||
const { id: segDisplaySetInstanceUID } = segmentation;
|
|
||||||
let segFrameOfReferenceUID = this._getFrameOfReferenceUID(segDisplaySetInstanceUID);
|
|
||||||
|
|
||||||
if (!segFrameOfReferenceUID) {
|
|
||||||
// if the segmentation displaySet does not have a FrameOfReferenceUID, we might check the
|
|
||||||
// segmentation itself maybe it has a FrameOfReferenceUID
|
|
||||||
const { FrameOfReferenceUID } = segmentation;
|
|
||||||
if (FrameOfReferenceUID) {
|
|
||||||
segFrameOfReferenceUID = FrameOfReferenceUID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!segFrameOfReferenceUID) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let shouldDisplaySeg = false;
|
|
||||||
|
|
||||||
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
|
|
||||||
const primaryFrameOfReferenceUID = this._getFrameOfReferenceUID(displaySetInstanceUID);
|
|
||||||
|
|
||||||
if (segFrameOfReferenceUID === primaryFrameOfReferenceUID) {
|
|
||||||
shouldDisplaySeg = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldDisplaySeg) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
segmentationService.addSegmentationRepresentationToToolGroup(
|
|
||||||
toolGroup.id,
|
|
||||||
segmentation.id,
|
|
||||||
false, // already hydrated,
|
|
||||||
segmentation.type
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private addOverlayRepresentationForDisplaySet(displaySet: any, viewport: any) {
|
|
||||||
const { segmentationService, toolGroupService } = this.servicesManager.services;
|
|
||||||
|
|
||||||
const { referencedVolumeId } = displaySet;
|
|
||||||
const segmentationId = displaySet.displaySetInstanceUID;
|
const segmentationId = displaySet.displaySetInstanceUID;
|
||||||
|
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
|
||||||
|
|
||||||
const representationType =
|
const representationType =
|
||||||
referencedVolumeId && cache.getVolume(referencedVolumeId) !== undefined
|
displaySet.Modality === 'SEG'
|
||||||
? csToolsEnums.SegmentationRepresentations.Labelmap
|
? csToolsEnums.SegmentationRepresentations.Labelmap
|
||||||
: csToolsEnums.SegmentationRepresentations.Contour;
|
: csToolsEnums.SegmentationRepresentations.Contour;
|
||||||
|
|
||||||
segmentationService.addSegmentationRepresentationToToolGroup(
|
segmentationService.addSegmentationRepresentation(viewport.id, {
|
||||||
toolGroup.id,
|
|
||||||
segmentationId,
|
segmentationId,
|
||||||
false,
|
type: representationType,
|
||||||
representationType
|
});
|
||||||
);
|
|
||||||
|
// store the segmentation presentation id in the viewport info
|
||||||
|
this.storePresentation({ viewportId: viewport.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: keepCamera is an interim solution until we have a better solution for
|
// Todo: keepCamera is an interim solution until we have a better solution for
|
||||||
@ -1032,7 +939,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this._setOtherViewport(
|
return this._setOtherViewport(
|
||||||
viewport as Types.IViewport,
|
viewport,
|
||||||
viewportData as StackViewportData,
|
viewportData as StackViewportData,
|
||||||
viewportInfo,
|
viewportInfo,
|
||||||
presentations
|
presentations
|
||||||
@ -1063,11 +970,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
// calculate the slab thickness based on the volume dimensions
|
// calculate the slab thickness based on the volume dimensions
|
||||||
const imageVolume = cache.getVolume(volumeId);
|
const imageVolume = cache.getVolume(volumeId);
|
||||||
|
|
||||||
const { dimensions } = imageVolume;
|
const { dimensions, spacing } = imageVolume;
|
||||||
const slabThickness = Math.sqrt(
|
const slabThickness = Math.sqrt(
|
||||||
dimensions[0] * dimensions[0] +
|
Math.pow(dimensions[0] * spacing[0], 2) +
|
||||||
dimensions[1] * dimensions[1] +
|
Math.pow(dimensions[1] * spacing[1], 2) +
|
||||||
dimensions[2] * dimensions[2]
|
Math.pow(dimensions[2] * spacing[2], 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
return slabThickness;
|
return slabThickness;
|
||||||
@ -1128,9 +1035,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
const viewports = this.getRenderingEngine().getViewports();
|
const viewports = this.getRenderingEngine().getViewports();
|
||||||
|
|
||||||
// Store the current position presentations for each viewport.
|
// Store the current position presentations for each viewport.
|
||||||
viewports.forEach(({ id }) => {
|
viewports.forEach(({ id: viewportId }) => {
|
||||||
const presentation = this.getPositionPresentation(id);
|
const presentation = this._getPositionPresentation(viewportId);
|
||||||
this.beforeResizePositionPresentations.set(id, presentation);
|
this.beforeResizePositionPresentations.set(viewportId, presentation);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resize the rendering engine and render.
|
// Resize the rendering engine and render.
|
||||||
@ -1159,6 +1066,65 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
this.gridResizeTimeOut = null;
|
this.gridResizeTimeOut = null;
|
||||||
}, this.gridResizeDelay);
|
}, this.gridResizeDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _setLutPresentation(
|
||||||
|
viewport: Types.IStackViewport | Types.IVolumeViewport,
|
||||||
|
lutPresentation: LutPresentation
|
||||||
|
): void {
|
||||||
|
if (!lutPresentation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { properties } = lutPresentation;
|
||||||
|
if (viewport instanceof BaseVolumeViewport) {
|
||||||
|
if (properties instanceof Map) {
|
||||||
|
properties.forEach((propertiesEntry, volumeId) => {
|
||||||
|
viewport.setProperties(propertiesEntry, volumeId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
viewport.setProperties(properties);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewport.setProperties(properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setPositionPresentation(
|
||||||
|
viewport: Types.IStackViewport | Types.IVolumeViewport,
|
||||||
|
positionPresentation: PositionPresentation
|
||||||
|
): void {
|
||||||
|
const viewRef = positionPresentation?.viewReference;
|
||||||
|
if (viewRef) {
|
||||||
|
viewport.setViewReference(viewRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewPresentation = positionPresentation?.viewPresentation;
|
||||||
|
if (viewPresentation) {
|
||||||
|
viewport.setViewPresentation(viewPresentation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setSegmentationPresentation(
|
||||||
|
viewport: Types.IStackViewport | Types.IVolumeViewport,
|
||||||
|
segmentationPresentation: SegmentationPresentation
|
||||||
|
): void {
|
||||||
|
if (!segmentationPresentation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { segmentationService } = this.servicesManager.services;
|
||||||
|
|
||||||
|
segmentationPresentation.forEach((presentation: SegmentationPresentationItem) => {
|
||||||
|
const { segmentationId, type, hydrated } = presentation;
|
||||||
|
|
||||||
|
if (hydrated) {
|
||||||
|
segmentationService.addSegmentationRepresentation(viewport.id, {
|
||||||
|
segmentationId,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CornerstoneViewportService;
|
export default CornerstoneViewportService;
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import {
|
|||||||
VolumeViewport,
|
VolumeViewport,
|
||||||
utilities,
|
utilities,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import { Types as CoreTypes } from '@ohif/core';
|
|
||||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||||
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
|
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
|
||||||
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
||||||
@ -25,7 +24,7 @@ export type ViewportOptions = {
|
|||||||
toolGroupId: string;
|
toolGroupId: string;
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
// Presentation ID to store/load presentation state from
|
// Presentation ID to store/load presentation state from
|
||||||
presentationIds?: CoreTypes.PresentationIds;
|
presentationIds?: AppTypes.PresentationIds;
|
||||||
orientation?: Enums.OrientationAxis;
|
orientation?: Enums.OrientationAxis;
|
||||||
background?: Types.Point3;
|
background?: Types.Point3;
|
||||||
displayArea?: Types.DisplayArea;
|
displayArea?: Types.DisplayArea;
|
||||||
@ -46,7 +45,7 @@ export type PublicViewportOptions = {
|
|||||||
id?: string;
|
id?: string;
|
||||||
viewportType?: string;
|
viewportType?: string;
|
||||||
toolGroupId?: string;
|
toolGroupId?: string;
|
||||||
presentationIds?: CoreTypes.PresentationIds;
|
presentationIds?: string[];
|
||||||
viewportId?: string;
|
viewportId?: string;
|
||||||
orientation?: Enums.OrientationAxis;
|
orientation?: Enums.OrientationAxis;
|
||||||
background?: Types.Point3;
|
background?: Types.Point3;
|
||||||
@ -283,7 +282,7 @@ class ViewportInfo {
|
|||||||
return this.viewportOptions;
|
return this.viewportOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPresentationIds(): CoreTypes.PresentationIds {
|
public getPresentationIds(): AppTypes.PresentationIds | null {
|
||||||
const { presentationIds } = this.viewportOptions;
|
const { presentationIds } = this.viewportOptions;
|
||||||
return presentationIds;
|
return presentationIds;
|
||||||
}
|
}
|
||||||
|
|||||||
4
extensions/cornerstone/src/stores/index.ts
Normal file
4
extensions/cornerstone/src/stores/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { useLutPresentationStore } from './useLutPresentationStore';
|
||||||
|
export { usePositionPresentationStore } from './usePositionPresentationStore';
|
||||||
|
export { useSegmentationPresentationStore } from './useSegmentationPresentationStore';
|
||||||
|
export { useSynchronizersStore } from './useSynchronizersStore';
|
||||||
42
extensions/cornerstone/src/stores/presentationUtils.ts
Normal file
42
extensions/cornerstone/src/stores/presentationUtils.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const JOIN_STR = '&';
|
||||||
|
|
||||||
|
// The default lut presentation id if none defined
|
||||||
|
const DEFAULT_STR = 'default';
|
||||||
|
|
||||||
|
// This code finds the first unique index to add to the presentation id so that
|
||||||
|
// two viewports containing the same display set in the same type of viewport
|
||||||
|
// can have different presentation information. This allows comparison of
|
||||||
|
// a single display set in two or more viewports, when the user has simply
|
||||||
|
// dragged and dropped the view in twice. For example, it allows displaying
|
||||||
|
// bone, brain and soft tissue views of a single display set, and to still
|
||||||
|
// remember the specific changes to each viewport.
|
||||||
|
const addUniqueIndex = (
|
||||||
|
arr,
|
||||||
|
key,
|
||||||
|
viewports: AppTypes.ViewportGrid.Viewports,
|
||||||
|
isUpdatingSameViewport
|
||||||
|
) => {
|
||||||
|
arr.push(0);
|
||||||
|
|
||||||
|
// If we are updating the viewport, we should not increment the index
|
||||||
|
if (isUpdatingSameViewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 128 is just a value that is larger than how many viewports we
|
||||||
|
// display at once, used as an upper bound on how many unique presentation
|
||||||
|
// ID's might exist for a single display set at once.
|
||||||
|
for (let displayInstance = 0; displayInstance < 128; displayInstance++) {
|
||||||
|
arr[arr.length - 1] = displayInstance;
|
||||||
|
const testId = arr.join(JOIN_STR);
|
||||||
|
if (
|
||||||
|
!Array.from(viewports.values()).find(
|
||||||
|
viewport => viewport.viewportOptions?.presentationIds?.[key] === testId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { addUniqueIndex, DEFAULT_STR, JOIN_STR };
|
||||||
166
extensions/cornerstone/src/stores/useLutPresentationStore.ts
Normal file
166
extensions/cornerstone/src/stores/useLutPresentationStore.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
import { LutPresentation } from '../types/Presentation';
|
||||||
|
import { addUniqueIndex, DEFAULT_STR, JOIN_STR } from './presentationUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifier for the LUT Presentation store type.
|
||||||
|
*/
|
||||||
|
const PRESENTATION_TYPE_ID = 'lutPresentationId';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to enable or disable debug mode for the store.
|
||||||
|
* Set to `true` to enable zustand devtools.
|
||||||
|
*/
|
||||||
|
const DEBUG_STORE = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state and actions for managing LUT presentations.
|
||||||
|
*/
|
||||||
|
type LutPresentationState = {
|
||||||
|
/**
|
||||||
|
* Type identifier for the store.
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores LUT presentations indexed by their presentation ID.
|
||||||
|
*/
|
||||||
|
lutPresentationStore: Record<string, LutPresentation>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the LUT presentation for a given key.
|
||||||
|
*
|
||||||
|
* @param key - The key identifying the LUT presentation.
|
||||||
|
* @param value - The `LutPresentation` to associate with the key.
|
||||||
|
*/
|
||||||
|
setLutPresentation: (key: string, value: LutPresentation) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all LUT presentations from the store.
|
||||||
|
*/
|
||||||
|
clearLutPresentationStore: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the presentation ID based on the provided parameters.
|
||||||
|
*
|
||||||
|
* @param id - The presentation ID to check.
|
||||||
|
* @param options - Configuration options.
|
||||||
|
* @param options.viewport - The current viewport in grid
|
||||||
|
* @param options.viewports - All available viewports in grid
|
||||||
|
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
|
||||||
|
* @returns The presentation ID or undefined.
|
||||||
|
*/
|
||||||
|
getPresentationId: (
|
||||||
|
id: string,
|
||||||
|
options: {
|
||||||
|
viewport: AppTypes.ViewportGrid.Viewport;
|
||||||
|
viewports: AppTypes.ViewportGrid.Viewports;
|
||||||
|
isUpdatingSameViewport: boolean;
|
||||||
|
}
|
||||||
|
) => string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a presentation ID for LUT based on the viewport configuration.
|
||||||
|
*
|
||||||
|
* @param id - The ID to check.
|
||||||
|
* @param options - Configuration options.
|
||||||
|
* @param options.viewport - The current viewport.
|
||||||
|
* @param options.viewports - All available viewports.
|
||||||
|
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
|
||||||
|
* @returns The LUT presentation ID or undefined.
|
||||||
|
*/
|
||||||
|
const getLutPresentationId = (
|
||||||
|
id: string,
|
||||||
|
{
|
||||||
|
viewport,
|
||||||
|
viewports,
|
||||||
|
isUpdatingSameViewport,
|
||||||
|
}: {
|
||||||
|
viewport: AppTypes.ViewportGrid.Viewport;
|
||||||
|
viewports: AppTypes.ViewportGrid.Viewports;
|
||||||
|
isUpdatingSameViewport: boolean;
|
||||||
|
}
|
||||||
|
): string | undefined => {
|
||||||
|
if (id !== PRESENTATION_TYPE_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLutId = (ds): string => {
|
||||||
|
if (!ds || !ds.options) {
|
||||||
|
return DEFAULT_STR;
|
||||||
|
}
|
||||||
|
if (ds.options.id) {
|
||||||
|
return ds.options.id;
|
||||||
|
}
|
||||||
|
const arr = Object.entries(ds.options).map(([key, val]) => `${key}=${val}`);
|
||||||
|
if (!arr.length) {
|
||||||
|
return DEFAULT_STR;
|
||||||
|
}
|
||||||
|
return arr.join(JOIN_STR);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!viewport || !viewport.viewportOptions || !viewport.displaySetInstanceUIDs?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { displaySetOptions, displaySetInstanceUIDs } = viewport;
|
||||||
|
const lutId = getLutId(displaySetOptions[0]);
|
||||||
|
const lutPresentationArr = [lutId];
|
||||||
|
|
||||||
|
for (const uid of displaySetInstanceUIDs) {
|
||||||
|
lutPresentationArr.push(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
addUniqueIndex(lutPresentationArr, PRESENTATION_TYPE_ID, viewports, isUpdatingSameViewport);
|
||||||
|
|
||||||
|
return lutPresentationArr.join(JOIN_STR);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the LUT Presentation store.
|
||||||
|
*
|
||||||
|
* @param set - The zustand set function.
|
||||||
|
* @returns The LUT Presentation store state and actions.
|
||||||
|
*/
|
||||||
|
const createLutPresentationStore = (set): LutPresentationState => ({
|
||||||
|
type: PRESENTATION_TYPE_ID,
|
||||||
|
lutPresentationStore: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the LUT presentation for a given key.
|
||||||
|
*/
|
||||||
|
setLutPresentation: (key, value) =>
|
||||||
|
set(
|
||||||
|
state => ({
|
||||||
|
lutPresentationStore: {
|
||||||
|
...state.lutPresentationStore,
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'setLutPresentation'
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all LUT presentations from the store.
|
||||||
|
*/
|
||||||
|
clearLutPresentationStore: () =>
|
||||||
|
set({ lutPresentationStore: {} }, false, 'clearLutPresentationStore'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the presentation ID based on the provided parameters.
|
||||||
|
*/
|
||||||
|
getPresentationId: getLutPresentationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zustand store for managing LUT presentations.
|
||||||
|
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||||
|
*/
|
||||||
|
export const useLutPresentationStore = create<LutPresentationState>()(
|
||||||
|
DEBUG_STORE
|
||||||
|
? devtools(createLutPresentationStore, { name: 'LutPresentationStore' })
|
||||||
|
: createLutPresentationStore
|
||||||
|
);
|
||||||
@ -0,0 +1,172 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
import { PositionPresentation } from '../types/Presentation';
|
||||||
|
import { addUniqueIndex, JOIN_STR } from './presentationUtils';
|
||||||
|
|
||||||
|
const PRESENTATION_TYPE_ID = 'positionPresentationId';
|
||||||
|
const DEBUG_STORE = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state and actions for managing position presentations.
|
||||||
|
*/
|
||||||
|
type PositionPresentationState = {
|
||||||
|
/**
|
||||||
|
* Type identifier for the store.
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores position presentations indexed by their presentation ID.
|
||||||
|
*/
|
||||||
|
positionPresentationStore: Record<string, PositionPresentation>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the position presentation for a given key.
|
||||||
|
*
|
||||||
|
* @param key - The key identifying the position presentation.
|
||||||
|
* @param value - The `PositionPresentation` to associate with the key.
|
||||||
|
*/
|
||||||
|
setPositionPresentation: (key: string, value: PositionPresentation) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all position presentations from the store.
|
||||||
|
*/
|
||||||
|
clearPositionPresentationStore: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the presentation ID based on the provided parameters.
|
||||||
|
*
|
||||||
|
* @param id - The ID to check.
|
||||||
|
* @param options - Configuration options.
|
||||||
|
* @param options.viewport - The current viewport.
|
||||||
|
* @param options.viewports - All available viewports.
|
||||||
|
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
|
||||||
|
* @returns The position presentation ID or undefined.
|
||||||
|
*/
|
||||||
|
getPresentationId: (
|
||||||
|
id: string,
|
||||||
|
options: {
|
||||||
|
viewport: any;
|
||||||
|
viewports: any;
|
||||||
|
isUpdatingSameViewport: boolean;
|
||||||
|
}
|
||||||
|
) => string | undefined;
|
||||||
|
|
||||||
|
getPositionPresentationId: (
|
||||||
|
viewport: any,
|
||||||
|
viewports?: any,
|
||||||
|
isUpdatingSameViewport?: boolean
|
||||||
|
) => string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a position presentation ID based on the viewport configuration.
|
||||||
|
*
|
||||||
|
* @param id - The ID to check.
|
||||||
|
* @param options - Configuration options.
|
||||||
|
* @param options.viewport - The current viewport.
|
||||||
|
* @param options.viewports - All available viewports.
|
||||||
|
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
|
||||||
|
* @returns The position presentation ID or undefined.
|
||||||
|
*/
|
||||||
|
const getPresentationId = (
|
||||||
|
id: string,
|
||||||
|
{
|
||||||
|
viewport,
|
||||||
|
viewports,
|
||||||
|
isUpdatingSameViewport,
|
||||||
|
}: {
|
||||||
|
viewport: any;
|
||||||
|
viewports: any;
|
||||||
|
isUpdatingSameViewport: boolean;
|
||||||
|
}
|
||||||
|
): string | undefined => {
|
||||||
|
if (id !== PRESENTATION_TYPE_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!viewport?.viewportOptions || !viewport.displaySetInstanceUIDs?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPositionPresentationId(viewport, viewports, isUpdatingSameViewport);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPositionPresentationId(viewport, viewports, isUpdatingSameViewport) {
|
||||||
|
const { viewportOptions = {}, displaySetInstanceUIDs = [], displaySetOptions = [] } = viewport;
|
||||||
|
const { id: viewportOptionId, orientation } = viewportOptions;
|
||||||
|
|
||||||
|
const positionPresentationArr = [orientation || 'acquisition'];
|
||||||
|
if (viewportOptionId) {
|
||||||
|
positionPresentationArr.push(viewportOptionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displaySetOptions?.some(ds => ds.options?.blendMode || ds.options?.displayPreset)) {
|
||||||
|
positionPresentationArr.push(`custom`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const uid of displaySetInstanceUIDs) {
|
||||||
|
positionPresentationArr.push(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewports && viewports.length && isUpdatingSameViewport !== undefined) {
|
||||||
|
addUniqueIndex(
|
||||||
|
positionPresentationArr,
|
||||||
|
PRESENTATION_TYPE_ID,
|
||||||
|
viewports,
|
||||||
|
isUpdatingSameViewport
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
positionPresentationArr.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return positionPresentationArr.join(JOIN_STR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Position Presentation store.
|
||||||
|
*
|
||||||
|
* @param set - The zustand set function.
|
||||||
|
* @returns The Position Presentation store state and actions.
|
||||||
|
*/
|
||||||
|
const createPositionPresentationStore = set => ({
|
||||||
|
type: PRESENTATION_TYPE_ID,
|
||||||
|
positionPresentationStore: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the position presentation for a given key.
|
||||||
|
*/
|
||||||
|
setPositionPresentation: (key, value) =>
|
||||||
|
set(
|
||||||
|
state => ({
|
||||||
|
positionPresentationStore: {
|
||||||
|
...state.positionPresentationStore,
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'setPositionPresentation'
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all position presentations from the store.
|
||||||
|
*/
|
||||||
|
clearPositionPresentationStore: () =>
|
||||||
|
set({ positionPresentationStore: {} }, false, 'clearPositionPresentationStore'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the presentation ID based on the provided parameters.
|
||||||
|
*/
|
||||||
|
getPresentationId,
|
||||||
|
getPositionPresentationId: getPositionPresentationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zustand store for managing position presentations.
|
||||||
|
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||||
|
*/
|
||||||
|
export const usePositionPresentationStore = create<PositionPresentationState>()(
|
||||||
|
DEBUG_STORE
|
||||||
|
? devtools(createPositionPresentationStore, { name: 'PositionPresentationStore' })
|
||||||
|
: createPositionPresentationStore
|
||||||
|
);
|
||||||
@ -0,0 +1,238 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
import { SegmentationPresentation } from '../types/Presentation';
|
||||||
|
import { JOIN_STR } from './presentationUtils';
|
||||||
|
import { getViewportOrientationFromImageOrientationPatient } from '../utils/getViewportOrientationFromImageOrientationPatient';
|
||||||
|
|
||||||
|
const PRESENTATION_TYPE_ID = 'segmentationPresentationId';
|
||||||
|
const DEBUG_STORE = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The keys are the presentationId.
|
||||||
|
*/
|
||||||
|
type SegmentationPresentationStore = {
|
||||||
|
/**
|
||||||
|
* Type identifier for the store.
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores segmentation presentations indexed by their presentation ID.
|
||||||
|
*/
|
||||||
|
segmentationPresentationStore: Record<string, SegmentationPresentation>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the segmentation presentation for a given segmentation ID.
|
||||||
|
*
|
||||||
|
* @param presentationId - The presentation ID.
|
||||||
|
* @param value - The `SegmentationPresentation` to associate with the ID.
|
||||||
|
*/
|
||||||
|
setSegmentationPresentation: (presentationId: string, value: SegmentationPresentation) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all segmentation presentations from the store.
|
||||||
|
*/
|
||||||
|
clearSegmentationPresentationStore: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the presentation ID based on the provided parameters.
|
||||||
|
*
|
||||||
|
* @param id - The ID to check.
|
||||||
|
* @param options - Configuration options.
|
||||||
|
* @param options.viewport - The current viewport.
|
||||||
|
* @param options.viewports - All available viewports.
|
||||||
|
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
|
||||||
|
* @param options.servicesManager - The services manager instance.
|
||||||
|
* @returns The segmentation presentation ID or undefined.
|
||||||
|
*/
|
||||||
|
getPresentationId: (
|
||||||
|
id: string,
|
||||||
|
options: {
|
||||||
|
viewport: AppTypes.ViewportGrid.Viewport;
|
||||||
|
viewports: AppTypes.ViewportGrid.Viewports;
|
||||||
|
isUpdatingSameViewport: boolean;
|
||||||
|
servicesManager: AppTypes.ServicesManager;
|
||||||
|
}
|
||||||
|
) => string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new segmentation presentation state.
|
||||||
|
*
|
||||||
|
* @param presentationId - The presentation ID.
|
||||||
|
* @param segmentationPresentation - The `SegmentationPresentation` to add.
|
||||||
|
* @param servicesManager - The services manager instance.
|
||||||
|
*/
|
||||||
|
addSegmentationPresentation: (
|
||||||
|
presentationId: string,
|
||||||
|
segmentationPresentation: SegmentationPresentation,
|
||||||
|
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current segmentation presentation ID.
|
||||||
|
*
|
||||||
|
* @param params - Parameters for retrieving the segmentation presentation ID.
|
||||||
|
* @param params.viewport - The current viewport.
|
||||||
|
* @param params.servicesManager - The services manager instance.
|
||||||
|
* @returns The current segmentation presentation ID.
|
||||||
|
*/
|
||||||
|
getSegmentationPresentationId: ({
|
||||||
|
viewport,
|
||||||
|
servicesManager,
|
||||||
|
}: {
|
||||||
|
viewport: AppTypes.ViewportGrid.Viewport;
|
||||||
|
servicesManager: AppTypes.ServicesManager;
|
||||||
|
}) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a segmentation presentation ID based on the viewport configuration.
|
||||||
|
*
|
||||||
|
* @param id - The ID to check.
|
||||||
|
* @param options - Configuration options.
|
||||||
|
* @param options.viewport - The current viewport.
|
||||||
|
* @param options.viewports - All available viewports.
|
||||||
|
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
|
||||||
|
* @param options.servicesManager - The services manager instance.
|
||||||
|
* @returns The segmentation presentation ID or undefined.
|
||||||
|
*/
|
||||||
|
const getPresentationId = (
|
||||||
|
id: string,
|
||||||
|
{
|
||||||
|
viewport,
|
||||||
|
viewports,
|
||||||
|
isUpdatingSameViewport,
|
||||||
|
servicesManager,
|
||||||
|
}: {
|
||||||
|
viewport: AppTypes.ViewportGrid.Viewport;
|
||||||
|
viewports: AppTypes.ViewportGrid.Viewports;
|
||||||
|
isUpdatingSameViewport: boolean;
|
||||||
|
servicesManager: AppTypes.ServicesManager;
|
||||||
|
}
|
||||||
|
): string | undefined => {
|
||||||
|
if (id !== PRESENTATION_TYPE_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _getSegmentationPresentationId({ viewport, servicesManager });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to generate the segmentation presentation ID.
|
||||||
|
*
|
||||||
|
* @param params - Parameters for generating the segmentation presentation ID.
|
||||||
|
* @param params.viewport - The current viewport.
|
||||||
|
* @param params.servicesManager - The services manager instance.
|
||||||
|
* @returns The segmentation presentation ID or undefined.
|
||||||
|
*/
|
||||||
|
const _getSegmentationPresentationId = ({
|
||||||
|
viewport,
|
||||||
|
servicesManager,
|
||||||
|
}: {
|
||||||
|
viewport: AppTypes.ViewportGrid.Viewport;
|
||||||
|
servicesManager: AppTypes.ServicesManager;
|
||||||
|
}) => {
|
||||||
|
if (!viewport?.viewportOptions || !viewport.displaySetInstanceUIDs?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { displaySetInstanceUIDs, viewportOptions } = viewport;
|
||||||
|
|
||||||
|
let orientation = viewportOptions.orientation;
|
||||||
|
|
||||||
|
if (!orientation) {
|
||||||
|
// Calculate orientation from the viewport sample image
|
||||||
|
const displaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
|
||||||
|
displaySetInstanceUIDs[0]
|
||||||
|
);
|
||||||
|
const sampleImage = displaySet.images?.[0];
|
||||||
|
const imageOrientationPatient = sampleImage?.ImageOrientationPatient;
|
||||||
|
|
||||||
|
orientation = getViewportOrientationFromImageOrientationPatient(imageOrientationPatient);
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentationPresentationArr = [];
|
||||||
|
|
||||||
|
segmentationPresentationArr.push(...displaySetInstanceUIDs);
|
||||||
|
|
||||||
|
// Uncomment if unique indexing is needed
|
||||||
|
// addUniqueIndex(
|
||||||
|
// segmentationPresentationArr,
|
||||||
|
// 'segmentationPresentationId',
|
||||||
|
// viewports,
|
||||||
|
// isUpdatingSameViewport
|
||||||
|
// );
|
||||||
|
|
||||||
|
return segmentationPresentationArr.join(JOIN_STR);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Segmentation Presentation store.
|
||||||
|
*
|
||||||
|
* @param set - The zustand set function.
|
||||||
|
* @returns The Segmentation Presentation store state and actions.
|
||||||
|
*/
|
||||||
|
const createSegmentationPresentationStore = set => ({
|
||||||
|
type: PRESENTATION_TYPE_ID,
|
||||||
|
segmentationPresentationStore: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all segmentation presentations from the store.
|
||||||
|
*/
|
||||||
|
clearSegmentationPresentationStore: () =>
|
||||||
|
set({ segmentationPresentationStore: {} }, false, 'clearSegmentationPresentationStore'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new segmentation presentation to the store.
|
||||||
|
*/
|
||||||
|
addSegmentationPresentation: (
|
||||||
|
presentationId: string,
|
||||||
|
segmentationPresentation: SegmentationPresentation,
|
||||||
|
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
|
||||||
|
) =>
|
||||||
|
set(
|
||||||
|
state => ({
|
||||||
|
segmentationPresentationStore: {
|
||||||
|
...state.segmentationPresentationStore,
|
||||||
|
[presentationId]: segmentationPresentation,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'addSegmentationPresentation'
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the segmentation presentation for a given presentation ID.
|
||||||
|
*/
|
||||||
|
setSegmentationPresentation: (presentationId: string, value: SegmentationPresentation) =>
|
||||||
|
set(
|
||||||
|
state => ({
|
||||||
|
segmentationPresentationStore: {
|
||||||
|
...state.segmentationPresentationStore,
|
||||||
|
[presentationId]: value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'setSegmentationPresentation'
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the presentation ID based on the provided parameters.
|
||||||
|
*/
|
||||||
|
getPresentationId,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current segmentation presentation ID.
|
||||||
|
*/
|
||||||
|
getSegmentationPresentationId: _getSegmentationPresentationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zustand store for managing segmentation presentations.
|
||||||
|
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||||
|
*/
|
||||||
|
export const useSegmentationPresentationStore = create<SegmentationPresentationStore>()(
|
||||||
|
DEBUG_STORE
|
||||||
|
? devtools(createSegmentationPresentationStore, { name: 'Segmentation Presentation Store' })
|
||||||
|
: createSegmentationPresentationStore
|
||||||
|
);
|
||||||
84
extensions/cornerstone/src/stores/useSynchronizersStore.ts
Normal file
84
extensions/cornerstone/src/stores/useSynchronizersStore.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifier for the synchronizers store type.
|
||||||
|
*/
|
||||||
|
const PRESENTATION_TYPE_ID = 'synchronizersStoreId';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to enable or disable debug mode for the store.
|
||||||
|
* Set to `true` to enable zustand devtools.
|
||||||
|
*/
|
||||||
|
const DEBUG_STORE = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a single synchronizer.
|
||||||
|
*/
|
||||||
|
type SynchronizerInfo = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
sourceViewports: Array<{ viewportId: string; renderingEngineId: string }>;
|
||||||
|
targetViewports: Array<{ viewportId: string; renderingEngineId: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State shape for the Synchronizers store.
|
||||||
|
*/
|
||||||
|
type SynchronizersState = {
|
||||||
|
/**
|
||||||
|
* Stores synchronizer information indexed by a unique key.
|
||||||
|
*/
|
||||||
|
synchronizersStore: Record<string, SynchronizerInfo[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the synchronizers for a specific viewport.
|
||||||
|
*
|
||||||
|
* @param viewportId - The ID of the viewport.
|
||||||
|
* @param synchronizers - An array of SynchronizerInfo.
|
||||||
|
*/
|
||||||
|
setSynchronizers: (viewportId: string, synchronizers: SynchronizerInfo[]) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the entire synchronizers store.
|
||||||
|
*/
|
||||||
|
clearSynchronizersStore: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Synchronizers store.
|
||||||
|
*
|
||||||
|
* @param set - The zustand set function.
|
||||||
|
* @returns The synchronizers store state and actions.
|
||||||
|
*/
|
||||||
|
const createSynchronizersStore = (set): SynchronizersState => ({
|
||||||
|
synchronizersStore: {},
|
||||||
|
type: PRESENTATION_TYPE_ID,
|
||||||
|
|
||||||
|
setSynchronizers: (viewportId: string, synchronizers: SynchronizerInfo[]) => {
|
||||||
|
set(
|
||||||
|
state => ({
|
||||||
|
synchronizersStore: {
|
||||||
|
...state.synchronizersStore,
|
||||||
|
[viewportId]: synchronizers,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'setSynchronizers'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSynchronizersStore: () => {
|
||||||
|
set({ synchronizersStore: {} }, false, 'clearSynchronizersStore');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zustand store for managing synchronizers.
|
||||||
|
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||||
|
*/
|
||||||
|
export const useSynchronizersStore = create<SynchronizersState>()(
|
||||||
|
DEBUG_STORE
|
||||||
|
? devtools(createSynchronizersStore, { name: 'SynchronizersStore' })
|
||||||
|
: createSynchronizersStore
|
||||||
|
);
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { SynchronizerManager, Synchronizer, utilities } from '@cornerstonejs/tools';
|
import { SynchronizerManager, Synchronizer } from '@cornerstonejs/tools';
|
||||||
import { EVENTS, getRenderingEngine, Types } from '@cornerstonejs/core';
|
import { EVENTS, getRenderingEngine, type Types, utilities } from '@cornerstonejs/core';
|
||||||
|
|
||||||
const frameViewSyncCallback = (
|
const frameViewSyncCallback = (
|
||||||
synchronizerInstance: Synchronizer,
|
synchronizerInstance: Synchronizer,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { LengthTool, utilities } from '@cornerstonejs/tools';
|
import { LengthTool, utilities } from '@cornerstonejs/tools';
|
||||||
import callInputDialog from '../utils/callInputDialog';
|
import { callInputDialog } from '@ohif/extension-default';
|
||||||
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
|
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
|
||||||
|
|
||||||
const { calibrateImageSpacing } = utilities;
|
const { calibrateImageSpacing } = utilities;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core';
|
import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core';
|
||||||
import { IStackViewport, IVolumeViewport, Point3 } from '@cornerstonejs/core/dist/esm/types';
|
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/types';
|
||||||
import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools';
|
import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools';
|
||||||
import { guid, b64toBlob } from '@ohif/core/src/utils';
|
import { guid, b64toBlob } from '@ohif/core/src/utils';
|
||||||
import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider';
|
import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider';
|
||||||
|
|||||||
@ -9,6 +9,13 @@ import ColorbarServiceType from '../services/ColorbarService';
|
|||||||
import * as cornerstone from '@cornerstonejs/core';
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
SegmentRepresentation as SegmentRep,
|
||||||
|
SegmentationData as SegData,
|
||||||
|
SegmentationRepresentation as SegRep,
|
||||||
|
SegmentationInfo as SegInfo,
|
||||||
|
} from '../services/SegmentationService/SegmentationService';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace AppTypes {
|
namespace AppTypes {
|
||||||
export type CornerstoneCacheService = CornerstoneCacheServiceType;
|
export type CornerstoneCacheService = CornerstoneCacheServiceType;
|
||||||
@ -18,6 +25,7 @@ declare global {
|
|||||||
export type ToolGroupService = ToolGroupServiceType;
|
export type ToolGroupService = ToolGroupServiceType;
|
||||||
export type ViewportActionCornersService = ViewportActionCornersServiceType;
|
export type ViewportActionCornersService = ViewportActionCornersServiceType;
|
||||||
export type ColorbarService = ColorbarServiceType;
|
export type ColorbarService = ColorbarServiceType;
|
||||||
|
|
||||||
export interface Services {
|
export interface Services {
|
||||||
cornerstoneViewportService?: CornerstoneViewportServiceType;
|
cornerstoneViewportService?: CornerstoneViewportServiceType;
|
||||||
toolGroupService?: ToolGroupServiceType;
|
toolGroupService?: ToolGroupServiceType;
|
||||||
@ -28,6 +36,19 @@ declare global {
|
|||||||
colorbarService?: ColorbarServiceType;
|
colorbarService?: ColorbarServiceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace Segmentation {
|
||||||
|
export type SegmentRepresentation = SegmentRep;
|
||||||
|
export type SegmentationData = SegData;
|
||||||
|
export type SegmentationRepresentation = SegRep;
|
||||||
|
export type SegmentationInfo = SegInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresentationIds {
|
||||||
|
lutPresentationId: string;
|
||||||
|
positionPresentationId: string;
|
||||||
|
segmentationPresentationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Test {
|
export interface Test {
|
||||||
services?: Services;
|
services?: Services;
|
||||||
cornerstone?: typeof cornerstone;
|
cornerstone?: typeof cornerstone;
|
||||||
|
|||||||
@ -1,16 +1,24 @@
|
|||||||
import type { Types } from '@cornerstonejs/core';
|
import type { Types } from '@cornerstonejs/core';
|
||||||
|
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a position presentation in a viewport. This is basically
|
* Represents a position presentation in a viewport. This is basically
|
||||||
* viewport specific camera position and zoom, and not the display set
|
* viewport specific camera position and zoom, and not the display set
|
||||||
*/
|
*/
|
||||||
export type PositionPresentation = {
|
export type PositionPresentation = {
|
||||||
id: string;
|
|
||||||
viewportType: string;
|
viewportType: string;
|
||||||
// The view reference has the basic information as to what image orientation/slice is shown
|
// The view reference has the basic information as to what image orientation/slice is shown
|
||||||
viewReference: Types.ViewReference;
|
viewReference: Types.ViewReference;
|
||||||
// The position information has the zoom/pan and possibly other related information, but not LUT
|
// The position information has the zoom/pan and possibly other related information, but not LUT
|
||||||
position: Types.ViewPresentation;
|
viewPresentation: Types.ViewPresentation;
|
||||||
|
/**
|
||||||
|
* Optionals
|
||||||
|
*/
|
||||||
|
initialImageIndex?: number;
|
||||||
|
// viewportId helps when hydrating SR or SEG - we can use it to filter
|
||||||
|
// presentations to get the one prior to the current viewport and reuse it
|
||||||
|
// for viewReference and viewPresentation
|
||||||
|
viewportId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,15 +28,40 @@ export type PositionPresentation = {
|
|||||||
* itself
|
* itself
|
||||||
*/
|
*/
|
||||||
export interface LutPresentation {
|
export interface LutPresentation {
|
||||||
id: string;
|
|
||||||
viewportType: string;
|
viewportType: string;
|
||||||
presentation: Record<string, Types.ViewportProperties> | Types.ViewportProperties;
|
// either a single object with the properties itself or a map of properties with volumeId keys
|
||||||
|
properties: Record<string, Types.ViewportProperties> | Types.ViewportProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a LUT presentation in a viewport, and is really related
|
||||||
|
* to displaySets and not the viewport itself. So that is why it can
|
||||||
|
* be an object with volumeId keys, or a single object with the properties
|
||||||
|
* itself
|
||||||
|
*
|
||||||
|
* each presentation has a segmentationId and a type and a value for
|
||||||
|
* hydrated and config.
|
||||||
|
*
|
||||||
|
* The hydrated property can be a boolean or null. It's null if the segmentation
|
||||||
|
* representation hasn't been created yet. It's true if the representation is
|
||||||
|
* currently in the viewport. It's false if the representation was in the viewport
|
||||||
|
* but has been removed.
|
||||||
|
*
|
||||||
|
* Config is the segmentation config, Todo: add stuff here
|
||||||
|
*/
|
||||||
|
export type SegmentationPresentationItem = {
|
||||||
|
segmentationId: string;
|
||||||
|
type: SegmentationRepresentations;
|
||||||
|
hydrated: boolean | null;
|
||||||
|
config?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SegmentationPresentation = SegmentationPresentationItem[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presentation can be a PositionPresentation or a LutPresentation.
|
* Presentation can be a PositionPresentation or a LutPresentation.
|
||||||
*/
|
*/
|
||||||
type Presentation = PositionPresentation | LutPresentation;
|
type Presentation = PositionPresentation | LutPresentation | SegmentationPresentation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Viewport presentations object that can contain a positionPresentation
|
* Viewport presentations object that can contain a positionPresentation
|
||||||
@ -37,6 +70,7 @@ type Presentation = PositionPresentation | LutPresentation;
|
|||||||
export type Presentations = {
|
export type Presentations = {
|
||||||
positionPresentation?: PositionPresentation;
|
positionPresentation?: PositionPresentation;
|
||||||
lutPresentation?: LutPresentation;
|
lutPresentation?: LutPresentation;
|
||||||
|
segmentationPresentation?: SegmentationPresentation;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Presentation;
|
export default Presentation;
|
||||||
|
|||||||
78
extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx
Normal file
78
extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useEffect, useState, memo } from 'react';
|
||||||
|
|
||||||
|
const MODALITIES_REQUIRING_CINE_AUTO_MOUNT = ['OT', 'US'];
|
||||||
|
|
||||||
|
const ActiveViewportBehavior = memo(
|
||||||
|
({ servicesManager, viewportId }: withAppTypes<{ viewportId: string }>) => {
|
||||||
|
const { displaySetService, cineService, viewportGridService, customizationService } =
|
||||||
|
servicesManager.services;
|
||||||
|
|
||||||
|
const [activeViewportId, setActiveViewportId] = useState(viewportId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = viewportGridService.subscribe(
|
||||||
|
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||||
|
({ viewportId }) => setActiveViewportId(viewportId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [viewportId, viewportGridService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cineService.isViewportCineClosed(activeViewportId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displaySetInstanceUIDs =
|
||||||
|
viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
||||||
|
|
||||||
|
if (!displaySetInstanceUIDs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displaySets = displaySetInstanceUIDs.map(uid =>
|
||||||
|
displaySetService.getDisplaySetByUID(uid)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!displaySets.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalities = displaySets.map(displaySet => displaySet?.Modality);
|
||||||
|
|
||||||
|
const { modalities: sourceModalities } = customizationService.getModeCustomization(
|
||||||
|
'autoCineModalities',
|
||||||
|
{
|
||||||
|
id: 'autoCineModalities',
|
||||||
|
modalities: MODALITIES_REQUIRING_CINE_AUTO_MOUNT,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const requiresCine = modalities.some(modality => sourceModalities.includes(modality));
|
||||||
|
|
||||||
|
if (requiresCine && !cineService.getState().isCineEnabled) {
|
||||||
|
cineService.setIsCineEnabled(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
activeViewportId,
|
||||||
|
cineService,
|
||||||
|
viewportGridService,
|
||||||
|
displaySetService,
|
||||||
|
customizationService,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
arePropsEqual
|
||||||
|
);
|
||||||
|
|
||||||
|
ActiveViewportBehavior.displayName = 'ActiveViewportBehavior';
|
||||||
|
|
||||||
|
function arePropsEqual(prevProps, nextProps) {
|
||||||
|
return (
|
||||||
|
prevProps.viewportId === nextProps.viewportId &&
|
||||||
|
prevProps.servicesManager === nextProps.servicesManager
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActiveViewportBehavior;
|
||||||
@ -8,7 +8,6 @@ import {
|
|||||||
BaseVolumeViewport,
|
BaseVolumeViewport,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import { ToolGroupManager } from '@cornerstonejs/tools';
|
import { ToolGroupManager } from '@cornerstonejs/tools';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { ViewportDownloadForm } from '@ohif/ui';
|
import { ViewportDownloadForm } from '@ohif/ui';
|
||||||
|
|
||||||
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
|
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
|
||||||
@ -22,7 +21,7 @@ const CornerstoneViewportDownloadForm = ({
|
|||||||
onClose,
|
onClose,
|
||||||
activeViewportId: activeViewportIdProp,
|
activeViewportId: activeViewportIdProp,
|
||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
}) => {
|
}: withAppTypes) => {
|
||||||
const enabledElement = OHIFgetEnabledElement(activeViewportIdProp);
|
const enabledElement = OHIFgetEnabledElement(activeViewportIdProp);
|
||||||
const activeViewportElement = enabledElement?.element;
|
const activeViewportElement = enabledElement?.element;
|
||||||
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
||||||
@ -246,9 +245,4 @@ const CornerstoneViewportDownloadForm = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CornerstoneViewportDownloadForm.propTypes = {
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
activeViewportId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CornerstoneViewportDownloadForm;
|
export default CornerstoneViewportDownloadForm;
|
||||||
|
|||||||
@ -161,7 +161,7 @@ class DicomLoaderService {
|
|||||||
getDicomDataMethod = fetchIt.bind(this, imageId);
|
getDicomDataMethod = fetchIt.bind(this, imageId);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported image type: ${loaderType} for imageId: ${imageId}`);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getDicomDataMethod();
|
return getDicomDataMethod();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { getEnabledElement } from '@cornerstonejs/core';
|
import { getEnabledElement } from '@cornerstonejs/core';
|
||||||
import { IEnabledElement } from '@cornerstonejs/core/dist/esm/types';
|
import { IEnabledElement } from '@cornerstonejs/core/types';
|
||||||
|
|
||||||
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
|
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export default function getCornerstoneViewportType(
|
|||||||
return Enums.ViewportType.VIDEO;
|
return Enums.ViewportType.VIDEO;
|
||||||
}
|
}
|
||||||
if (lowerViewportType === WHOLESLIDE) {
|
if (lowerViewportType === WHOLESLIDE) {
|
||||||
return Enums.ViewportType.WholeSlide;
|
return Enums.ViewportType.WHOLE_SLIDE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) {
|
if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) {
|
||||||
|
|||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { CONSTANTS, utilities } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
const { MPR_CAMERA_VALUES } = CONSTANTS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the viewport orientation (axial, sagittal, or coronal) based on the image orientation patient values.
|
||||||
|
* This is done by comparing the view vectors with predefined MPR camera values.
|
||||||
|
*
|
||||||
|
* @param imageOrientationPatient - Array of 6 numbers representing the image orientation patient values.
|
||||||
|
* The first 3 numbers represent the direction cosines of the first row and the second 3 numbers
|
||||||
|
* represent the direction cosines of the first column.
|
||||||
|
*
|
||||||
|
* @returns The viewport orientation as a string ('axial', 'sagittal', 'coronal') or undefined if
|
||||||
|
* the orientation cannot be determined or if the input is invalid.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const orientation = getViewportOrientationFromImageOrientationPatient([1,0,0,0,1,0]);
|
||||||
|
* console.debug(orientation); // 'axial'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const getViewportOrientationFromImageOrientationPatient = (
|
||||||
|
imageOrientationPatient: number[]
|
||||||
|
): string | undefined => {
|
||||||
|
if (!imageOrientationPatient || imageOrientationPatient.length !== 6) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewRight = imageOrientationPatient.slice(0, 3);
|
||||||
|
const viewDown = imageOrientationPatient.slice(3, 6);
|
||||||
|
const viewUp = [-viewDown[0], -viewDown[1], -viewDown[2]];
|
||||||
|
|
||||||
|
// Compare vectors with MPR camera values using utilities.isEqual
|
||||||
|
if (
|
||||||
|
utilities.isEqual(viewRight, MPR_CAMERA_VALUES.axial.viewRight) &&
|
||||||
|
utilities.isEqual(viewUp, MPR_CAMERA_VALUES.axial.viewUp)
|
||||||
|
) {
|
||||||
|
return 'axial';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
utilities.isEqual(viewRight, MPR_CAMERA_VALUES.sagittal.viewRight) &&
|
||||||
|
utilities.isEqual(viewUp, MPR_CAMERA_VALUES.sagittal.viewUp)
|
||||||
|
) {
|
||||||
|
return 'sagittal';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
utilities.isEqual(viewRight, MPR_CAMERA_VALUES.coronal.viewRight) &&
|
||||||
|
utilities.isEqual(viewUp, MPR_CAMERA_VALUES.coronal.viewUp)
|
||||||
|
) {
|
||||||
|
return 'coronal';
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user