OHIF with Cornerstone3D 2.0 (#4347)
This commit is contained in:
parent
8981a61b8c
commit
7ce7127d7c
@ -180,10 +180,6 @@ jobs:
|
||||
- run:
|
||||
name: Authenticate with NPM registry
|
||||
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
- run:
|
||||
name: Increase the event emitter limit
|
||||
command: |
|
||||
node ./increaseEventEmitterLimit.mjs
|
||||
- run:
|
||||
name: build half of the packages (to avoid out of memory in circleci)
|
||||
command: |
|
||||
@ -230,10 +226,6 @@ jobs:
|
||||
- run:
|
||||
name: Authenticate with NPM registry
|
||||
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
- run:
|
||||
name: Increase the event emitter limit
|
||||
command: |
|
||||
node ./increaseEventEmitterLimit.mjs
|
||||
- run:
|
||||
name: build half of the packages (to avoid out of memory in circleci)
|
||||
command: |
|
||||
|
||||
@ -144,11 +144,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
use: 'source-map-loader',
|
||||
},
|
||||
transpileJavaScriptRule(mode),
|
||||
loadWebWorkersRule,
|
||||
// loadShadersRule,
|
||||
@ -188,8 +183,8 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
|
||||
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
|
||||
'@state': path.resolve(__dirname, '../platform/app/src/state'),
|
||||
'@cornerstonejs/dicom-image-loader':
|
||||
'@cornerstonejs/dicom-image-loader/dist/dynamic-import/cornerstoneDICOMImageLoader.min.js',
|
||||
'dicom-microscopy-viewer':
|
||||
'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js',
|
||||
},
|
||||
// Which directories to search when resolving modules
|
||||
modules: [
|
||||
@ -217,7 +212,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin()]),
|
||||
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]),
|
||||
// Uncomment to generate bundle analyzer
|
||||
// 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
|
||||
|
||||
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": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"@kitware/vtk.js": "32.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"core-js": "^3.2.1",
|
||||
"moment": "^2.9.4"
|
||||
@ -77,7 +77,7 @@
|
||||
"stylus": "^0.59.0",
|
||||
"stylus-loader": "^7.1.3",
|
||||
"terser-webpack-plugin": "^5.1.4",
|
||||
"typescript": "4.6.4",
|
||||
"typescript": "5.5.4",
|
||||
"unused-webpack-plugin": "2.4.0",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
"glob-parent": "^6.0.2",
|
||||
"trim": "^1.0.0",
|
||||
"package-json": "^8.1.0",
|
||||
"typescript": "4.6.4",
|
||||
"typescript": "5.5.4",
|
||||
"sharp": "^0.32.6"
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.86.0",
|
||||
"@cornerstonejs/core": "^1.86.0",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"@cornerstonejs/adapters": "^2.1.13",
|
||||
"@cornerstonejs/core": "^2.1.13",
|
||||
"@kitware/vtk.js": "32.1.0",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ function _getDisplaySetsFromSeries(
|
||||
};
|
||||
|
||||
// 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 = () => {
|
||||
if (displaySet.referencedVolumeId) {
|
||||
return displaySet.referencedVolumeId;
|
||||
@ -119,6 +119,22 @@ function _getDisplaySetsFromSeries(
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
const getRangeFromPixelData = (pixelData: Float32Array) => {
|
||||
let lowest = pixelData[0];
|
||||
let highest = pixelData[0];
|
||||
|
||||
for (let i = 1; i < pixelData.length; i++) {
|
||||
if (pixelData[i] < lowest) {
|
||||
lowest = pixelData[i];
|
||||
}
|
||||
if (pixelData[i] > highest) {
|
||||
highest = pixelData[i];
|
||||
}
|
||||
}
|
||||
|
||||
return [lowest, highest];
|
||||
};
|
||||
|
||||
async function _load(
|
||||
displaySet,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
@ -132,11 +148,11 @@ async function _load(
|
||||
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
|
||||
// and also return the same promise to any other callers.
|
||||
// loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||
const promise = _loadParametricMap({
|
||||
extensionManager,
|
||||
displaySet,
|
||||
@ -149,18 +165,26 @@ async function _load(
|
||||
|
||||
promise
|
||||
.then(() => {
|
||||
displaySet.loadStatus.loading = false;
|
||||
displaySet.loadStatus.loaded = true;
|
||||
displaySet.loading = false;
|
||||
displaySet.isLoaded = true;
|
||||
// Broadcast that loading is complete
|
||||
servicesManager.services.segmentationService._broadcastEvent(
|
||||
servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
{
|
||||
pmapDisplaySet: displaySet,
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
displaySet.loadStatus.loading = false;
|
||||
displaySet.loading = false;
|
||||
displaySet.isLoaded = false;
|
||||
throw err;
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function _loadParametricMap({ extensionManager, displaySet, headers }: withAppTypes) {
|
||||
async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
|
||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
|
||||
const referencedVolumeId = displaySet.getReferencedVolumeId();
|
||||
const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
|
||||
@ -189,9 +213,12 @@ async function _loadParametricMap({ extensionManager, displaySet, headers }: wit
|
||||
},
|
||||
});
|
||||
|
||||
derivedVolume.getScalarData().set(pixelData);
|
||||
|
||||
const range = derivedVolume.imageData.getPointData().getScalars().getRange();
|
||||
const newPixelData = new TypedArrayConstructor(pixelData.length);
|
||||
for (let i = 0; i < pixelData.length; i++) {
|
||||
newPixelData[i] = pixelData[i] * 100;
|
||||
}
|
||||
derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData);
|
||||
const range = getRangeFromPixelData(newPixelData);
|
||||
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
|
||||
|
||||
derivedVolume.metadata.voiLut = [windowLevel];
|
||||
|
||||
@ -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 React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import createPMAPToolGroupAndAddTools from '../utils/initPMAPToolGroup';
|
||||
|
||||
const PMAP_TOOLGROUP_BASE_NAME = 'PMAPToolGroup';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
|
||||
|
||||
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
const { displaySets, viewportOptions, displaySetOptions, servicesManager, extensionManager } =
|
||||
props;
|
||||
const {
|
||||
displaySets,
|
||||
children,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
const { displaySetService, toolGroupService, customizationService } = servicesManager.services;
|
||||
const toolGroupId = `${PMAP_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
||||
const { displaySetService, segmentationService, uiNotificationService } =
|
||||
servicesManager.services;
|
||||
|
||||
// PMAP viewport will always have a single display set
|
||||
if (displaySets.length !== 1) {
|
||||
@ -32,6 +35,24 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
metadata: referencedDisplaySetMetadata,
|
||||
};
|
||||
|
||||
const [pmapIsLoading, setPmapIsLoading] = useState(!pmapDisplaySet.isLoaded);
|
||||
|
||||
// Add effect to listen for loading complete
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
if (evt.pmapDisplaySet?.displaySetInstanceUID === pmapDisplaySet.displaySetInstanceUID) {
|
||||
setPmapIsLoading(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [pmapDisplaySet]);
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
@ -46,12 +67,25 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
|
||||
Object.assign(pmapDisplaySetOptions.options, {
|
||||
colormap: {
|
||||
name: 'rainbow',
|
||||
name: 'rainbow_2',
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0.5 },
|
||||
{ value: 1, opacity: 1 },
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.25, opacity: 0.25 },
|
||||
{ value: 0.5, opacity: 0.5 },
|
||||
{ value: 0.75, opacity: 0.75 },
|
||||
{ value: 0.9, opacity: 0.99 },
|
||||
],
|
||||
},
|
||||
voi: {
|
||||
windowCenter: 50,
|
||||
windowWidth: 100,
|
||||
},
|
||||
});
|
||||
|
||||
uiNotificationService.show({
|
||||
title: 'Parametric Map',
|
||||
type: 'warning',
|
||||
message: 'The values are multiplied by 100 in the viewport for better visibility',
|
||||
});
|
||||
|
||||
return (
|
||||
@ -61,7 +95,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
displaySets={[referencedDisplaySet, pmapDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'volume',
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
@ -73,7 +106,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
displaySetOptions,
|
||||
props,
|
||||
pmapDisplaySet,
|
||||
toolGroupId,
|
||||
viewportOptions.orientation,
|
||||
viewportOptions.viewportId,
|
||||
]);
|
||||
@ -98,23 +130,33 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
};
|
||||
}, [activeViewportId, displaySetService, viewportGridService, viewports]);
|
||||
|
||||
useEffect(() => {
|
||||
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
let childrenWithProps = null;
|
||||
|
||||
if (toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This creates a custom tool group which has the lifetime of this view only
|
||||
toolGroup = createPMAPToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
|
||||
|
||||
return () => toolGroupService.destroyToolGroup(toolGroupId);
|
||||
}, [customizationService, toolGroupId, toolGroupService]);
|
||||
if (children && children.length) {
|
||||
childrenWithProps = children.map((child, index) => {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportId,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
{pmapIsLoading && (
|
||||
<LoadingIndicatorTotalPercent
|
||||
className="h-full w-full"
|
||||
totalNumbers={null}
|
||||
percentComplete={null}
|
||||
loadingText="Loading Parametric Map..."
|
||||
/>
|
||||
)}
|
||||
{getCornerstoneViewport()}
|
||||
{childrenWithProps}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
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.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
|
||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||
// Instead of throwing error, subscribe to display sets added
|
||||
const { unsubscribe } = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
({ displaySetsAdded }) => {
|
||||
const addedDisplaySet = displaySetsAdded[0];
|
||||
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
|
||||
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||
throw new Error('Referenced DisplaySet is missing for the RT');
|
||||
}
|
||||
|
||||
} else {
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
}
|
||||
|
||||
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.
|
||||
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||
if (!rtDisplaySet.structureSet) {
|
||||
const structureSet = await loadRTStruct(
|
||||
extensionManager,
|
||||
rtDisplaySet,
|
||||
rtDisplaySet.getReferenceDisplaySet(),
|
||||
headers
|
||||
);
|
||||
const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers);
|
||||
|
||||
rtDisplaySet.structureSet = structureSet;
|
||||
}
|
||||
|
||||
const suppressEvents = true;
|
||||
segmentationService
|
||||
.createSegmentationForRTDisplaySet(rtDisplaySet, null, suppressEvents)
|
||||
.createSegmentationForRTDisplaySet(rtDisplaySet)
|
||||
.then(() => {
|
||||
rtDisplaySet.loading = false;
|
||||
resolve();
|
||||
|
||||
@ -2,6 +2,7 @@ import { id } from './id';
|
||||
import React from 'react';
|
||||
import { Types } from '@ohif/core';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getCommandsModule from './getCommandsModule';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
|
||||
@ -24,6 +25,7 @@ const extension: Types.Extensions.Extension = {
|
||||
* You ID can be anything you want, but it should be unique.
|
||||
*/
|
||||
id,
|
||||
getCommandsModule,
|
||||
|
||||
/**
|
||||
* PanelModule should provide a list of panels that will be available in OHIF
|
||||
|
||||
@ -89,12 +89,7 @@ async function checkAndLoadContourData(instance, datasource) {
|
||||
});
|
||||
}
|
||||
|
||||
export default async function loadRTStruct(
|
||||
extensionManager,
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
headers
|
||||
) {
|
||||
export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
@ -102,8 +97,6 @@ export default async function loadRTStruct(
|
||||
const { bulkDataURI } = dataSource.getConfig?.() || {};
|
||||
|
||||
const { dicomLoaderService } = utilityModule.exports;
|
||||
const imageIdSopInstanceUidPairs =
|
||||
_getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
|
||||
|
||||
// Set here is loading is asynchronous.
|
||||
// If this function throws its set back to false.
|
||||
@ -149,8 +142,7 @@ export default async function loadRTStruct(
|
||||
|
||||
const contourPoints = [];
|
||||
for (let c = 0; c < ContourSequenceArray.length; c++) {
|
||||
const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } =
|
||||
ContourSequenceArray[c];
|
||||
const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c];
|
||||
|
||||
let isSupported = false;
|
||||
|
||||
@ -194,24 +186,6 @@ export default async function loadRTStruct(
|
||||
return structureSet;
|
||||
}
|
||||
|
||||
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
|
||||
imageIdSopInstanceUidPairsEntry =>
|
||||
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||
);
|
||||
|
||||
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
|
||||
};
|
||||
|
||||
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
|
||||
return referencedDisplaySet.images.map(image => {
|
||||
return {
|
||||
imageId: image.imageId,
|
||||
sopInstanceUID: image.SOPInstanceUID,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function _setROIContourMetadata(
|
||||
structureSet,
|
||||
StructureSetROISequence,
|
||||
|
||||
@ -10,7 +10,6 @@ function promptHydrateRT({
|
||||
servicesManager,
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
toolGroupId = 'default',
|
||||
preHydrateCallbacks,
|
||||
hydrateRTDisplaySet,
|
||||
}: withAppTypes) {
|
||||
@ -27,16 +26,13 @@ function promptHydrateRT({
|
||||
callback();
|
||||
});
|
||||
|
||||
window.setTimeout(async () => {
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
toolGroupId,
|
||||
servicesManager,
|
||||
});
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
resolve(isHydrated);
|
||||
}, 0);
|
||||
resolve(isHydrated);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -4,7 +4,9 @@ import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } f
|
||||
|
||||
import promptHydrateRT from '../utils/promptHydrateRT';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||
|
||||
@ -62,7 +64,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
|
||||
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
referencedDisplaySetInstanceUID
|
||||
);
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
|
||||
|
||||
referencedDisplaySetRef.current = {
|
||||
@ -91,12 +96,15 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
});
|
||||
}, [viewportGrid]);
|
||||
|
||||
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => {
|
||||
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
|
||||
displaySets: [rtDisplaySet],
|
||||
viewportId,
|
||||
});
|
||||
};
|
||||
const hydrateRTDisplaySet = useCallback(
|
||||
({ rtDisplaySet, viewportId }) => {
|
||||
commandsManager.runCommand('hydrateRTSDisplaySet', {
|
||||
displaySet: rtDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
},
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
@ -143,10 +151,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
|
||||
setSelectedSegment(newSelectedSegmentIndex);
|
||||
},
|
||||
[selectedSegment]
|
||||
[selectedSegment, segmentationService]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -243,7 +251,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
segmentationService.removeSegmentationRepresentations(viewportId);
|
||||
|
||||
toolGroupService.destroyToolGroup(toolGroupId);
|
||||
};
|
||||
@ -254,11 +262,27 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
segmentationService.removeSegmentationRepresentations(viewportId);
|
||||
referencedDisplaySetRef.current = null;
|
||||
};
|
||||
}, [rtDisplaySet]);
|
||||
|
||||
const onStatusClick = useCallback(async () => {
|
||||
// Before hydrating a RT and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the RT. This is
|
||||
// required if the user has changed the viewport (other viewport than RT viewport)
|
||||
// presentation state (w/l and invert) and then opens the RT. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
|
||||
@ -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(() => {
|
||||
viewportActionCornersService.setComponents([
|
||||
viewportActionCornersService.addComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
@ -318,7 +326,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
key="actionArrows"
|
||||
onArrowsClick={onSegmentChange}
|
||||
className={
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
|
||||
}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.86.0",
|
||||
"@cornerstonejs/core": "^1.86.0",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"@cornerstonejs/adapters": "^2.1.13",
|
||||
"@cornerstonejs/core": "^2.1.13",
|
||||
"@kitware/vtk.js": "32.1.0",
|
||||
"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 vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||
|
||||
import {
|
||||
updateViewportsForSegmentationRendering,
|
||||
getUpdatedViewportsForSegmentation,
|
||||
getTargetViewport,
|
||||
} from './utils/hydrationUtils';
|
||||
const { segmentation: segmentationUtils } = utilities;
|
||||
|
||||
const { datasetToBlob } = dcmjs.data;
|
||||
|
||||
const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||
const targetViewportId = viewportId || activeViewportId;
|
||||
|
||||
const viewport = viewports.get(targetViewportId);
|
||||
|
||||
return viewport;
|
||||
};
|
||||
|
||||
const {
|
||||
Cornerstone3D: {
|
||||
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation },
|
||||
Segmentation: { generateSegmentation },
|
||||
},
|
||||
} = adaptersSEG;
|
||||
|
||||
@ -42,87 +46,14 @@ const commandsModule = ({
|
||||
extensionManager,
|
||||
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
||||
const {
|
||||
uiNotificationService,
|
||||
segmentationService,
|
||||
uiDialogService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
cornerstoneViewportService,
|
||||
} = servicesManager.services;
|
||||
} = servicesManager.services as AppTypes.Services;
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
|
||||
* This function evaluates viewports based on their compatibility with the provided segmentation's
|
||||
* frame of reference UID and appends them to the updated list if they should render the segmentation.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - the ID of the viewport to be updated.
|
||||
* @param params.servicesManager - The services manager
|
||||
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
|
||||
*
|
||||
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
|
||||
*/
|
||||
getUpdatedViewportsForSegmentation,
|
||||
/**
|
||||
* Creates an empty segmentation for a specified viewport.
|
||||
* It first checks if the display set associated with the viewport is reconstructable.
|
||||
* If not, it raises a notification error. Otherwise, it creates a new segmentation
|
||||
* for the display set after handling the necessary steps for making the viewport
|
||||
* a volume viewport first
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - the target viewport ID.
|
||||
*
|
||||
*/
|
||||
createEmptySegmentationForViewport: async ({ viewportId }) => {
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
// Todo: add support for multiple display sets
|
||||
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
if (!displaySet.isReconstructable) {
|
||||
uiNotificationService.show({
|
||||
title: 'Segmentation',
|
||||
message: 'Segmentation is not supported for non-reconstructible displaysets yet',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
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.
|
||||
* The function prepares the viewport for rendering, then loads the segmentation details.
|
||||
@ -134,106 +65,30 @@ const commandsModule = ({
|
||||
*
|
||||
*/
|
||||
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
loadFn: async () => {
|
||||
// Todo: handle adding more than one segmentation
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
||||
|
||||
const segmentation = segmentations[0];
|
||||
const segmentationId = segmentation.id;
|
||||
const label = segmentation.label;
|
||||
const segments = segmentation.segments;
|
||||
|
||||
delete segmentation.segments;
|
||||
|
||||
await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, {
|
||||
segmentationId,
|
||||
label,
|
||||
});
|
||||
|
||||
if (segmentation.scalarData) {
|
||||
const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId);
|
||||
labelmapVolume.scalarData.set(segmentation.scalarData);
|
||||
}
|
||||
|
||||
segmentationService.addOrUpdateSegmentation(segmentation);
|
||||
|
||||
const toolGroupId = viewport.viewportOptions.toolGroupId;
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId
|
||||
);
|
||||
|
||||
segments.forEach(segment => {
|
||||
if (segment === null) {
|
||||
return;
|
||||
}
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
segmentIndex: segment.segmentIndex,
|
||||
toolGroupId,
|
||||
properties: {
|
||||
color: segment.color,
|
||||
label: segment.label,
|
||||
opacity: segment.opacity,
|
||||
isLocked: segment.isLocked,
|
||||
visibility: segment.isVisible,
|
||||
active: segmentation.activeSegmentIndex === segment.segmentIndex,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (segmentation.centroidsIJK) {
|
||||
segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK);
|
||||
}
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Loads segmentation display sets for a specified viewport.
|
||||
* Depending on the modality of the display set (SEG or RTSTRUCT),
|
||||
* it chooses the appropriate service function to create
|
||||
* the segmentation for the display set.
|
||||
* The function then prepares the viewport for rendering segmentation.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded.
|
||||
* @param params.displaySets - Array of display sets to be loaded for segmentation.
|
||||
*
|
||||
*/
|
||||
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
|
||||
// Todo: handle adding more than one segmentation
|
||||
const displaySet = displaySets[0];
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const initialSliceIndex = viewport.getSliceIndex();
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
||||
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
displaySet,
|
||||
loadFn: async () => {
|
||||
const segDisplaySet = displaySet;
|
||||
const suppressEvents = false;
|
||||
const serviceFunction =
|
||||
segDisplaySet.Modality === 'SEG'
|
||||
? 'createSegmentationForSEGDisplaySet'
|
||||
: 'createSegmentationForRTDisplaySet';
|
||||
const segmentation = segmentations[0];
|
||||
const segmentationId = segmentation.segmentationId;
|
||||
const label = segmentation.config.label;
|
||||
const segments = segmentation.config.segments;
|
||||
|
||||
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
|
||||
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
|
||||
return segmentationId;
|
||||
},
|
||||
initialSliceIndex,
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
await segmentationService.createLabelmapForDisplaySet(displaySet, {
|
||||
segmentationId,
|
||||
segments,
|
||||
label,
|
||||
});
|
||||
|
||||
segmentationService.addOrUpdateSegmentation(segmentation);
|
||||
|
||||
await segmentationService.addSegmentationRepresentation(viewport.viewportId, {
|
||||
segmentationId,
|
||||
});
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
/**
|
||||
* Generates a segmentation from a given segmentation ID.
|
||||
@ -251,25 +106,61 @@ const commandsModule = ({
|
||||
generateSegmentation: ({ segmentationId, options = {} }) => {
|
||||
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
|
||||
|
||||
const { referencedVolumeId } = segmentation.representationData.LABELMAP;
|
||||
const { imageIds } = segmentation.representationData.Labelmap;
|
||||
|
||||
const segmentationVolume = cache.getVolume(segmentationId);
|
||||
const referencedVolume = cache.getVolume(referencedVolumeId);
|
||||
const referencedImages = referencedVolume.getCornerstoneImages();
|
||||
const segImages = imageIds.map(imageId => cache.getImage(imageId));
|
||||
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
|
||||
|
||||
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume);
|
||||
const labelmaps2D = [];
|
||||
|
||||
// Generate fake metadata as an example
|
||||
labelmapObj.metadata = [];
|
||||
let z = 0;
|
||||
|
||||
for (const segImage of segImages) {
|
||||
const segmentsOnLabelmap = new Set();
|
||||
const pixelData = segImage.getPixelData();
|
||||
const { rows, columns } = segImage;
|
||||
|
||||
// Use a single pass through the pixel data
|
||||
for (let i = 0; i < pixelData.length; i++) {
|
||||
const segment = pixelData[i];
|
||||
if (segment !== 0) {
|
||||
segmentsOnLabelmap.add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
labelmaps2D[z++] = {
|
||||
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
|
||||
pixelData,
|
||||
rows,
|
||||
columns,
|
||||
};
|
||||
}
|
||||
|
||||
const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap);
|
||||
|
||||
const labelmap3D = {
|
||||
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
|
||||
metadata: [],
|
||||
labelmaps2D,
|
||||
};
|
||||
|
||||
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
|
||||
if (!segment) {
|
||||
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(
|
||||
color.slice(0, 3).map(value => value / 255)
|
||||
@ -292,12 +183,12 @@ const commandsModule = ({
|
||||
CodeMeaning: 'Tissue',
|
||||
},
|
||||
};
|
||||
labelmapObj.metadata[segmentIndex] = segmentMetadata;
|
||||
labelmap3D.metadata[segmentIndex] = segmentMetadata;
|
||||
});
|
||||
|
||||
const generatedSegmentation = generateSegmentation(
|
||||
referencedImages,
|
||||
labelmapObj,
|
||||
labelmap3D,
|
||||
metaData,
|
||||
options
|
||||
);
|
||||
@ -445,18 +336,19 @@ const commandsModule = ({
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
getUpdatedViewportsForSegmentation: {
|
||||
commandFn: actions.getUpdatedViewportsForSegmentation,
|
||||
},
|
||||
/**
|
||||
* Obsolete?
|
||||
*/
|
||||
loadSegmentationDisplaySetsForViewport: {
|
||||
commandFn: actions.loadSegmentationDisplaySetsForViewport,
|
||||
},
|
||||
/**
|
||||
* Obsolete?
|
||||
*/
|
||||
loadSegmentationsForViewport: {
|
||||
commandFn: actions.loadSegmentationsForViewport,
|
||||
},
|
||||
createEmptySegmentationForViewport: {
|
||||
commandFn: actions.createEmptySegmentationForViewport,
|
||||
},
|
||||
|
||||
generateSegmentation: {
|
||||
commandFn: actions.generateSegmentation,
|
||||
},
|
||||
|
||||
@ -20,6 +20,17 @@ const segProtocol: Types.HangingProtocol.Protocol = {
|
||||
viewportType: 'stack',
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
// options: {
|
||||
// matchingRules: ['sameFOR'],
|
||||
// },
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
@ -52,7 +63,20 @@ const segProtocol: Types.HangingProtocol.Protocol = {
|
||||
},
|
||||
viewports: [
|
||||
{
|
||||
viewportOptions: { allowUnmatchedView: true },
|
||||
viewportOptions: {
|
||||
allowUnmatchedView: true,
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
// options: {
|
||||
// matchingRules: ['sameFOR'],
|
||||
// },
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'segDisplaySetId',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
||||
import { CONSTANTS } from '@cornerstonejs/tools';
|
||||
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
||||
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
|
||||
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
||||
|
||||
import { SOPClassHandlerId } from './id';
|
||||
@ -70,28 +70,32 @@ function _getDisplaySetsFromSeries(
|
||||
|
||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
if (!referencedDisplaySet) {
|
||||
// subscribe to display sets added which means at some point it will be available
|
||||
const { unsubscribe } = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
({ displaySetsAdded }) => {
|
||||
// here we can also do a little bit of search, since sometimes DICOM SEG
|
||||
// does not contain the referenced display set uid , and we can just
|
||||
// see which of the display sets added is more similar and assign it
|
||||
// to the referencedDisplaySet
|
||||
const addedDisplaySet = displaySetsAdded[0];
|
||||
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
|
||||
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||
throw new Error('Referenced DisplaySet is missing for the SEG');
|
||||
}
|
||||
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
} else {
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
|
||||
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
|
||||
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
|
||||
const referencedVolumeId = `cornerstoneStreamingImageVolume:${displaySet.referencedVolumeURI}`;
|
||||
displaySet.referencedVolumeId = referencedVolumeId;
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
}
|
||||
|
||||
displaySet.load = async ({ headers }) =>
|
||||
await _load(displaySet, servicesManager, extensionManager, headers);
|
||||
@ -111,7 +115,7 @@ function _load(
|
||||
if (
|
||||
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
|
||||
loadPromises[SOPInstanceUID] &&
|
||||
_segmentationExists(segDisplaySet, segmentationService)
|
||||
_segmentationExists(segDisplaySet)
|
||||
) {
|
||||
return loadPromises[SOPInstanceUID];
|
||||
}
|
||||
@ -135,9 +139,8 @@ function _load(
|
||||
}
|
||||
}
|
||||
|
||||
const suppressEvents = true;
|
||||
segmentationService
|
||||
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents)
|
||||
.createSegmentationForSEGDisplaySet(segDisplaySet)
|
||||
.then(() => {
|
||||
segDisplaySet.loading = false;
|
||||
resolve();
|
||||
@ -166,20 +169,20 @@ async function _loadSegments({
|
||||
const { dicomLoaderService } = utilityModule.exports;
|
||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
|
||||
|
||||
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
|
||||
const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
|
||||
segDisplaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!cachedReferencedVolume) {
|
||||
throw new Error(
|
||||
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
|
||||
);
|
||||
if (!referencedDisplaySet) {
|
||||
throw new Error('referencedDisplaySet is missing for SEG');
|
||||
}
|
||||
|
||||
const { imageIds } = cachedReferencedVolume;
|
||||
const { instances: images } = referencedDisplaySet;
|
||||
const imageIds = images.map(({ imageId }) => imageId);
|
||||
|
||||
// Todo: what should be defaults here
|
||||
const tolerance = 0.001;
|
||||
const skipOverlapping = true;
|
||||
|
||||
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
|
||||
const { percentComplete } = evt.detail;
|
||||
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
|
||||
@ -231,9 +234,8 @@ async function _loadSegments({
|
||||
Object.assign(segDisplaySet, results);
|
||||
}
|
||||
|
||||
function _segmentationExists(segDisplaySet, segmentationService: AppTypes.SegmentationService) {
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
function _segmentationExists(segDisplaySet) {
|
||||
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
|
||||
@ -8,7 +8,7 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
// forcing the button to have black background since initially
|
||||
// it is designed for the toolbox not the toolbar on top
|
||||
// we should then branch the buttonSectionId to have different styles
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
|
||||
if (!segmentations?.length) {
|
||||
return {
|
||||
disabled: true,
|
||||
|
||||
@ -3,7 +3,6 @@ import React from 'react';
|
||||
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import { getToolbarModule } from './getToolbarModule';
|
||||
|
||||
@ -28,13 +27,6 @@ const extension = {
|
||||
* You ID can be anything you want, but it should be unique.
|
||||
*/
|
||||
id,
|
||||
/**
|
||||
* PanelModule should provide a list of panels that will be available in OHIF
|
||||
* for Modes to consume and render. Each panel is defined by a {name,
|
||||
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||
*/
|
||||
getPanelModule,
|
||||
getCommandsModule,
|
||||
getToolbarModule,
|
||||
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||
|
||||
@ -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,
|
||||
viewportId,
|
||||
preHydrateCallbacks,
|
||||
hydrateSEGDisplaySet,
|
||||
hydrateCallback,
|
||||
}: withAppTypes) {
|
||||
const { uiViewportDialogService } = servicesManager.services;
|
||||
const extensionManager = servicesManager._extensionManager;
|
||||
@ -28,7 +28,7 @@ function promptHydrateSEG({
|
||||
});
|
||||
|
||||
window.setTimeout(async () => {
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
const isHydrated = await hydrateCallback({
|
||||
segDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
import { useSegmentationPresentationStore } from '@ohif/extension-cornerstone';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||
|
||||
@ -37,7 +38,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
}
|
||||
|
||||
const segDisplaySet = displaySets[0];
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
|
||||
// States
|
||||
@ -61,7 +61,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
|
||||
const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
referencedDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||
referencedDisplaySet,
|
||||
segDisplaySet
|
||||
@ -98,18 +102,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||
|
||||
// Todo: jump to the center of the first segment
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
displaySets={[referencedDisplaySet, segDisplaySet]}
|
||||
displaySets={[segDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'volume',
|
||||
viewportType: viewportOptions.viewportType,
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
presentationIds: viewportOptions.presentationIds,
|
||||
}}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
@ -139,12 +142,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
|
||||
setSelectedSegment(newSelectedSegmentIndex);
|
||||
},
|
||||
[selectedSegment]
|
||||
);
|
||||
|
||||
const hydrateSEG = useCallback(() => {
|
||||
// update the previously stored segmentationPresentation with the new viewportId
|
||||
// presentation so that when we put the referencedDisplaySet back in the viewport
|
||||
// it will have the correct segmentation representation hydrated
|
||||
commandsManager.runCommand('updateStoredSegmentationPresentation', {
|
||||
displaySet: segDisplaySet,
|
||||
type: SegmentationRepresentations.Labelmap,
|
||||
});
|
||||
|
||||
// update the previously stored positionPresentation with the new viewportId
|
||||
// presentation so that when we put the referencedDisplaySet back in the viewport
|
||||
// it will be in the correct position zoom and pan
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId,
|
||||
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
|
||||
});
|
||||
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
|
||||
});
|
||||
}, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (segIsLoading) {
|
||||
return;
|
||||
@ -155,15 +181,18 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
viewportId,
|
||||
segDisplaySet,
|
||||
preHydrateCallbacks: [storePresentationState],
|
||||
hydrateSEGDisplaySet,
|
||||
hydrateCallback: hydrateSEG,
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
});
|
||||
}, [servicesManager, viewportId, segDisplaySet, segIsLoading]);
|
||||
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
|
||||
|
||||
useEffect(() => {
|
||||
// on new seg display set, remove all segmentations from all viewports
|
||||
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
@ -222,6 +251,9 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
if (toolGroup) {
|
||||
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
|
||||
// only, and does NOT interfere with currently displayed segmentations.
|
||||
@ -229,33 +261,14 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
|
||||
return () => {
|
||||
// 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
|
||||
toolGroupService.destroyToolGroup(toolGroupId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsHydrated(segDisplaySet.isHydrated);
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
referencedDisplaySetRef.current = null;
|
||||
};
|
||||
}, [segDisplaySet]);
|
||||
|
||||
const hydrateSEGDisplaySet = useCallback(
|
||||
({ segDisplaySet, viewportId }) => {
|
||||
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
|
||||
displaySets: [segDisplaySet],
|
||||
viewportId,
|
||||
});
|
||||
},
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
const onStatusClick = useCallback(async () => {
|
||||
// Before hydrating a SEG and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
@ -264,16 +277,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
// presentation state (w/l and invert) and then opens the SEG. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
|
||||
hydrateSEG();
|
||||
}, [storePresentationState, hydrateSEG]);
|
||||
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.setComponents([
|
||||
viewportActionCornersService.addComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
@ -292,7 +300,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
key="actionArrows"
|
||||
onArrowsClick={onSegmentChange}
|
||||
className={
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
|
||||
}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
@ -332,18 +340,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
PatientID,
|
||||
PatientName,
|
||||
PatientSex,
|
||||
PatientAge,
|
||||
SliceThickness,
|
||||
ManufacturerModelName,
|
||||
StudyDate,
|
||||
SeriesDescription,
|
||||
SpacingBetweenSlices,
|
||||
} = referencedDisplaySetRef.current.metadata;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
@ -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) {
|
||||
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
|
||||
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.86.0",
|
||||
"@cornerstonejs/core": "^1.86.0",
|
||||
"@cornerstonejs/tools": "^1.86.0",
|
||||
"@cornerstonejs/adapters": "^2.1.13",
|
||||
"@cornerstonejs/core": "^2.1.13",
|
||||
"@cornerstonejs/tools": "^2.1.13",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,8 +15,15 @@ const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'
|
||||
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||
|
||||
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
|
||||
props;
|
||||
const {
|
||||
commandsManager,
|
||||
children,
|
||||
dataSource,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
@ -63,6 +70,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
trackedMeasurements = tracked?.[0];
|
||||
sendTrackedMeasurementsEvent = tracked?.[1];
|
||||
}
|
||||
|
||||
if (!sendTrackedMeasurementsEvent) {
|
||||
// if no panels from measurement-tracking extension is used, this code will run
|
||||
trackedMeasurements = null;
|
||||
@ -72,6 +80,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
|
||||
if (displaySets.length) {
|
||||
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
|
||||
* show the SR measurements on the referenced image on the correct viewport,
|
||||
@ -297,17 +311,12 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
updateSR();
|
||||
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
|
||||
|
||||
/**
|
||||
* Todo: what is this, not sure what it does regarding the react aspect,
|
||||
* it is updating a local variable? which is not state.
|
||||
*/
|
||||
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
useEffect(() => {
|
||||
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
}, [trackedMeasurements]);
|
||||
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.setComponents([
|
||||
viewportActionCornersService.addComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
utilities as csToolsUtils,
|
||||
} from '@cornerstonejs/tools';
|
||||
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 DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
|
||||
@ -95,7 +95,7 @@ export default function init({
|
||||
const imageIndex = (currentViewport as StackViewport)
|
||||
.getImageIds()
|
||||
.indexOf(referencedImageId);
|
||||
csToolsUtils.jumpToSlice(currentViewport.element, { imageIndex });
|
||||
csUtils.jumpToSlice(currentViewport.element, { imageIndex });
|
||||
} catch (error) {
|
||||
console.warn('Unable to jump to image based on measurement coordinate', error);
|
||||
}
|
||||
|
||||
@ -45,11 +45,19 @@ export default function hydrateStructuredReport(
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
||||
const disableEditing = appConfig?.disableEditing;
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
const { measurementService, displaySetService, customizationService } = servicesManager.services;
|
||||
|
||||
const codingValues = customizationService.getCustomization('codingValues', {});
|
||||
|
||||
const { disableEditing } = customizationService.getCustomization(
|
||||
'PanelMeasurement.disableEditing',
|
||||
{
|
||||
id: 'default.disableEditing',
|
||||
disableEditing: false,
|
||||
}
|
||||
);
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
// TODO -> We should define a strict versioning somewhere.
|
||||
|
||||
@ -42,9 +42,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^1.86.0",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
|
||||
"@cornerstonejs/tools": "^1.86.0",
|
||||
"@cornerstonejs/core": "^2.1.13",
|
||||
"@cornerstonejs/tools": "^2.1.13",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +110,12 @@ function _getTimePointsData(volume) {
|
||||
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 dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||
@ -145,8 +150,11 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
|
||||
referencedDynamicVolume.imageIds[0]
|
||||
);
|
||||
|
||||
const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
|
||||
const maskVolumeId = segmentationVolume?.volumeId;
|
||||
|
||||
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
|
||||
maskVolumeId: segmentation.id,
|
||||
maskVolumeId,
|
||||
}) as number[][];
|
||||
|
||||
const pixelCount = timeData.length;
|
||||
@ -155,27 +163,15 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
|
||||
return [];
|
||||
}
|
||||
|
||||
// since we only use one segmentation representation per segmentationId
|
||||
// it is fine to pick the first one
|
||||
const segmentationRepresentations = csTools.segmentation.state.getSegmentationIdRepresentations(
|
||||
segmentation.id
|
||||
);
|
||||
|
||||
const segmentationRepresentationUID =
|
||||
segmentationRepresentations[0].segmentationRepresentationUID;
|
||||
|
||||
const toolGroupId = csTools.segmentation.state.getToolGroupIdFromSegmentationRepresentationUID(
|
||||
segmentationRepresentationUID
|
||||
);
|
||||
|
||||
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
|
||||
const color = csTools.segmentation.config.color.getColorForSegmentIndex(
|
||||
toolGroupId,
|
||||
segmentationRepresentationUID,
|
||||
1 // segmentIndex
|
||||
);
|
||||
// const color = csTools.segmentation.config.color.getSegmentIndexColor(
|
||||
// segmentationRepresentationUID,
|
||||
// 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);
|
||||
|
||||
if (!timePointsData) {
|
||||
@ -219,14 +215,14 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
|
||||
};
|
||||
}
|
||||
|
||||
function _getInstanceFromSegmentations(segmentations, displaySetService) {
|
||||
function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volumesTimePointsCache = new WeakMap();
|
||||
const segmentationsData = segmentations.map(segmentation =>
|
||||
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
|
||||
_getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager })
|
||||
);
|
||||
|
||||
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
|
||||
@ -266,10 +262,11 @@ function _getInstanceFromSegmentations(segmentations, displaySetService) {
|
||||
}
|
||||
|
||||
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
||||
const { segmentationService, displaySetService } = servicesManager.services;
|
||||
debugger;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const { seriesMetadata, instance } =
|
||||
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
|
||||
_getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {};
|
||||
|
||||
if (seriesMetadata && instance) {
|
||||
// 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);
|
||||
return stdDeviation;
|
||||
}
|
||||
|
||||
// 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, {
|
||||
maskVolumeId: segmentation.id,
|
||||
maskVolumeId: volume.volumeId,
|
||||
}) as number[][];
|
||||
|
||||
if (summaryStats) {
|
||||
@ -113,17 +113,33 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
|
||||
// for each voxel in the ROI, get the value at the current time point
|
||||
const voxelValues = [];
|
||||
let sum = 0;
|
||||
let minValue = Infinity;
|
||||
let maxValue = -Infinity;
|
||||
let minIndex = 0;
|
||||
let maxIndex = 0;
|
||||
|
||||
// Single pass through the data to collect all needed values
|
||||
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
|
||||
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);
|
||||
const minimum = Math.min(...voxelValues);
|
||||
min.push(minimum);
|
||||
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]);
|
||||
const maximum = Math.max(...voxelValues);
|
||||
max.push(maximum);
|
||||
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
|
||||
mean.push(sum / numVoxels);
|
||||
min.push(minValue);
|
||||
minIJK.push(ijkCoords[minIndex]);
|
||||
max.push(maxValue);
|
||||
maxIJK.push(ijkCoords[maxIndex]);
|
||||
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');
|
||||
}
|
||||
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
referenceDisplaySet.displaySetInstanceUID,
|
||||
{ label }
|
||||
const displaySet = displaySetService.getDisplaySetByUID(
|
||||
referenceDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Add Segmentation to all toolGroupIds in the viewer
|
||||
const toolGroupIds = Array.from(
|
||||
viewports.values(),
|
||||
viewport => viewport.viewportOptions.toolGroupId
|
||||
);
|
||||
const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
|
||||
label,
|
||||
});
|
||||
|
||||
const representationType = LABELMAP;
|
||||
const firstViewport = viewports.values().next().value;
|
||||
|
||||
for (const toolGroupId of toolGroupIds) {
|
||||
const hydrateSegmentation = true;
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId,
|
||||
hydrateSegmentation,
|
||||
representationType
|
||||
);
|
||||
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||
}
|
||||
await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
|
||||
segmentationId,
|
||||
});
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
|
||||
@ -147,8 +147,8 @@ function getFusionViewports() {
|
||||
name: DEFAULT_COLORMAP,
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.3 },
|
||||
{ value: 1, opacity: 0.3 },
|
||||
{ value: 0.1, opacity: 0.8 },
|
||||
{ value: 1, opacity: 0.8 },
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -191,6 +191,15 @@ function getFusionViewports() {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@ -240,6 +249,15 @@ function getFusionViewports() {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@ -289,6 +307,15 @@ function getFusionViewports() {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { DynamicDataPanel } from './panels';
|
||||
import { Toolbox } from '@ohif/ui-next';
|
||||
import { PanelSegmentation } from '@ohif/extension-cornerstone';
|
||||
import DynamicExport from './panels/DynamicExport';
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedDynamicDataPanel = ({}) => {
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) {
|
||||
const wrappedDynamicDataPanel = () => {
|
||||
return (
|
||||
<DynamicDataPanel
|
||||
commandsManager={commandsManager}
|
||||
@ -14,7 +15,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicToolbox = ({}) => {
|
||||
const wrappedDynamicSegmentation = () => {
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
@ -24,18 +25,17 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
buttonSectionId="dynamic-toolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicExport = () => {
|
||||
return (
|
||||
<>
|
||||
<DynamicExport
|
||||
commandsManager={commandsManager}
|
||||
<PanelSegmentation
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
configuration={configuration}
|
||||
>
|
||||
<DynamicExport
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</PanelSegmentation>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -49,18 +49,11 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
component: wrappedDynamicDataPanel,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-toolbox',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: 'Dynamic Toolbox',
|
||||
component: wrappedDynamicToolbox,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-export',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: '4D Workflow',
|
||||
component: wrappedDynamicExport,
|
||||
name: 'dynamic-segmentation',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedDynamicSegmentation,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import PanelGenerateImage from './PanelGenerateImage';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) {
|
||||
return (
|
||||
|
||||
@ -1,73 +1,52 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ActionButtons } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React from 'react';
|
||||
import { Button, Icons } from '@ohif/ui-next';
|
||||
import { useSegmentations } from '@ohif/extension-cornerstone';
|
||||
|
||||
function DynamicExport({ commandsManager, servicesManager, extensionManager }: withAppTypes) {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { t } = useTranslation('dynamicExport');
|
||||
function DynamicExport({ commandsManager, servicesManager }: withAppTypes) {
|
||||
const segmentations = useSegmentations({ servicesManager });
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Export Time Data',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
options: {
|
||||
filename: 'TimeData.csv',
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: !segmentations?.length,
|
||||
},
|
||||
{
|
||||
label: 'Export ROI Stats',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
summaryStats: true,
|
||||
options: {
|
||||
filename: 'ROIStats.csv',
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: !segmentations?.length,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Update UI based on segmentation changes (added, removed, updated)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated, removed].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
if (!segmentations?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-3 flex justify-center px-2">
|
||||
<ActionButtons
|
||||
actions={actions}
|
||||
t={t}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-8 w-full items-center rounded pr-0.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pl-1.5"
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
options: {
|
||||
filename: 'TimeData.csv',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icons.Export />
|
||||
<span className="pl-1">Time Data</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex h-8 w-full items-center rounded pr-0.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pl-1.5"
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
summaryStats: true,
|
||||
options: {
|
||||
filename: 'ROIStats.csv',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icons.Export />
|
||||
<span className="pl-1">ROI Stats</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
InputDoubleRange,
|
||||
Button,
|
||||
PanelSection,
|
||||
ButtonGroup,
|
||||
@ -10,6 +9,8 @@ import {
|
||||
Tooltip,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { DoubleSlider } from '@ohif/ui-next';
|
||||
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
|
||||
const controlClassNames = {
|
||||
@ -55,21 +56,15 @@ const DynamicVolumeControls = ({
|
||||
|
||||
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
||||
|
||||
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
|
||||
|
||||
useEffect(() => {
|
||||
setSliderRangeValues([framesLength / 4, framesLength / 2]);
|
||||
}, [framesLength]);
|
||||
const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]);
|
||||
|
||||
const handleSliderChange = newValues => {
|
||||
onDoubleRangeChange(newValues);
|
||||
|
||||
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
|
||||
return;
|
||||
}
|
||||
setSliderRangeValues(newValues);
|
||||
};
|
||||
|
||||
const formatLabel = value => Math.round(value);
|
||||
|
||||
return (
|
||||
<div className="flex select-none flex-col">
|
||||
<PanelSection
|
||||
@ -134,7 +129,7 @@ const DynamicVolumeControls = ({
|
||||
}
|
||||
/>
|
||||
<ButtonGroup
|
||||
className={`mt-2 w-full `}
|
||||
className={`mt-2 w-full`}
|
||||
separated={true}
|
||||
>
|
||||
<button
|
||||
@ -156,15 +151,15 @@ const DynamicVolumeControls = ({
|
||||
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
<div className="w-ful mt-2">
|
||||
<InputDoubleRange
|
||||
values={sliderRangeValues}
|
||||
onChange={handleSliderChange}
|
||||
minValue={0}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
maxValue={framesLength}
|
||||
<div className="mt-2 w-full">
|
||||
<DoubleSlider
|
||||
min={0}
|
||||
max={framesLength - 1}
|
||||
step={1}
|
||||
defaultValue={sliderRangeValues}
|
||||
onValueChange={handleSliderChange}
|
||||
formatLabel={formatLabel}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
@ -216,14 +211,14 @@ function FrameControls({
|
||||
>
|
||||
<Icon
|
||||
name={getPlayPauseIconName()}
|
||||
className=" active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||
className="active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||
/>
|
||||
</IconButton>
|
||||
<InputNumber
|
||||
value={currentFrameIndex}
|
||||
onChange={onFrameChange}
|
||||
minValue={0}
|
||||
maxValue={framesLength}
|
||||
maxValue={framesLength - 1}
|
||||
label="Frame"
|
||||
{...controlClassNames}
|
||||
/>
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||
import { cache, utilities as csUtils, volumeLoader, eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
|
||||
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||
import DynamicVolumeControls from './DynamicVolumeControls';
|
||||
|
||||
@ -16,8 +14,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
const [{ activeViewportId }] = useViewportGrid();
|
||||
|
||||
//
|
||||
const [timePointsRange, setTimePointsRange] = useState([]);
|
||||
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
|
||||
const [timePointsRange, setTimePointsRange] = useState([0, 0]);
|
||||
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([0, 0]);
|
||||
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
|
||||
const [dynamicVolume, setDynamicVolume] = useState(null);
|
||||
const [frameRate, setFrameRate] = useState(20);
|
||||
@ -31,47 +29,62 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
|
||||
|
||||
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 => {
|
||||
if (volumeData.volume.isDynamicVolume()) {
|
||||
if (volumeData.volume?.isDynamicVolume()) {
|
||||
setDynamicVolume(volumeData.volume);
|
||||
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 () => {
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = servicesManager.services.cineService.subscribe(
|
||||
servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED,
|
||||
evt => {
|
||||
setIsPlaying(evt.isPlaying);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [cineService]);
|
||||
|
||||
useEffect(() => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
||||
|
||||
if (!displaySetUIDs || displaySetUIDs.length === 0) {
|
||||
if (!displaySetUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetUID =>
|
||||
displaySetService.getDisplaySetByUID(displaySetUID)
|
||||
);
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
|
||||
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
|
||||
|
||||
if (!dynamicVolumeDisplaySet) {
|
||||
@ -88,23 +101,16 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
|
||||
setDynamicVolume(dynamicVolume);
|
||||
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
|
||||
setTimePointsRange([1, dynamicVolume.numTimePoints]);
|
||||
}, [activeViewportId, cornerstoneViewportService]);
|
||||
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
||||
|
||||
const callback = evt => {
|
||||
setTimePointRendered(evt.detail.timePointIndex);
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(evt, callback);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(evt, callback);
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
const newRange = [1, dynamicVolume.numTimePoints];
|
||||
setTimePointsRange(newRange);
|
||||
setTimePointsRangeToUseForGenerate(newRange);
|
||||
}, [
|
||||
activeViewportId,
|
||||
viewportGridService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
cineService,
|
||||
]);
|
||||
|
||||
function renderGeneratedImage(displaySet) {
|
||||
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
|
||||
@ -130,26 +136,19 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
let computedVolume = cache.getVolume(computedVolumeId);
|
||||
|
||||
if (!computedVolume) {
|
||||
await createComputedVolume(dynamicVolumeId, computedVolumeId);
|
||||
computedVolume = cache.getVolume(computedVolumeId);
|
||||
computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
||||
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]);
|
||||
|
||||
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);
|
||||
cstUtils.dynamicVolume.updateVolumeFromTimeData(dynamicVolume, operationName, options);
|
||||
|
||||
// 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
|
||||
@ -166,6 +165,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
madeInClient: true,
|
||||
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
|
||||
isDerived: true,
|
||||
imageIds: computedVolume.imageIds,
|
||||
};
|
||||
setComputedDisplaySet(displaySet);
|
||||
renderGeneratedImage(displaySet);
|
||||
@ -173,19 +173,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
commandsManager.runCommand('updateVolumeData', {
|
||||
volume: computedVolume,
|
||||
});
|
||||
// Check if viewport is currently displaying the computed volume, if so,
|
||||
// call render on the viewports to update the image, if not, call
|
||||
// renderGeneratedImage
|
||||
// if (!cache.getVolume(dynamicVolumeId)) {
|
||||
// for (const viewportId of viewports.keys()) {
|
||||
// const viewportForRendering =
|
||||
// cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
// viewportForRendering.render();
|
||||
// }
|
||||
// } else {
|
||||
cornerstoneViewportService.getRenderingEngine().render();
|
||||
renderGeneratedImage(computedDisplaySet);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@ -248,28 +237,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
onGenerate={onGenerateImage}
|
||||
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
|
||||
onDoubleRangeChange={handleSliderChange}
|
||||
initialRangeValues={timePointsRangeToUseForGenerate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function createComputedVolume(dynamicVolumeId, computedVolumeId) {
|
||||
if (!cache.getVolume(computedVolumeId)) {
|
||||
const computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
||||
volumeId: computedVolumeId,
|
||||
});
|
||||
return computedVolume;
|
||||
}
|
||||
}
|
||||
|
||||
PanelGenerateImage.propTypes = {
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
measurementService: PropTypes.shape({
|
||||
getMeasurements: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
VALUE_TYPES: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@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",
|
||||
"@ohif/core": "3.9.0-beta.105",
|
||||
"@ohif/ui": "3.9.0-beta.105",
|
||||
@ -55,12 +55,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.86.0",
|
||||
"@cornerstonejs/core": "^1.86.0",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
|
||||
"@cornerstonejs/tools": "^1.86.0",
|
||||
"@cornerstonejs/adapters": "^2.1.13",
|
||||
"@cornerstonejs/core": "^2.1.13",
|
||||
"@cornerstonejs/tools": "^2.1.13",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"@kitware/vtk.js": "32.1.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as cs3DTools from '@cornerstonejs/tools';
|
||||
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/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 { setEnabledElement } from '../state';
|
||||
@ -17,8 +16,10 @@ import type { Types } from '@ohif/core';
|
||||
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
|
||||
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
import { LutPresentation, PositionPresentation } from '../types/Presentation';
|
||||
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
|
||||
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
|
||||
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
|
||||
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
|
||||
|
||||
const STACK = 'stack';
|
||||
|
||||
@ -28,428 +29,401 @@ const STACK = 'stack';
|
||||
*/
|
||||
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
|
||||
// Then we don't need to worry about the re-renders if the props change.
|
||||
const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
|
||||
const {
|
||||
displaySets,
|
||||
dataSource,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
onElementEnabled,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
onElementDisabled,
|
||||
isJumpToMeasurementDisabled = false,
|
||||
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||
// to set the initial state of the viewport's first image to render
|
||||
// eslint-disable-next-line react/prop-types
|
||||
initialImageIndex,
|
||||
// if the viewport is part of a hanging protocol layout
|
||||
// we should not really rely on the old synchronizers and
|
||||
// you see below we only rehydrate the synchronizers if the viewport
|
||||
// is not part of the hanging protocol layout. HPs should
|
||||
// define their own synchronizers. Since the synchronizers are
|
||||
// viewportId dependent and
|
||||
// eslint-disable-next-line react/prop-types
|
||||
isHangingProtocolLayout,
|
||||
} = props;
|
||||
const OHIFCornerstoneViewport = React.memo(
|
||||
(
|
||||
props: withAppTypes<{
|
||||
viewportId: string;
|
||||
displaySets: AppTypes.DisplaySet[];
|
||||
viewportOptions: AppTypes.ViewportGrid.GridViewportOptions;
|
||||
initialImageIndex: number;
|
||||
}>
|
||||
) => {
|
||||
const {
|
||||
displaySets,
|
||||
dataSource,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
onElementEnabled,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
onElementDisabled,
|
||||
isJumpToMeasurementDisabled = false,
|
||||
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||
// to set the initial state of the viewport's first image to render
|
||||
// eslint-disable-next-line react/prop-types
|
||||
initialImageIndex,
|
||||
// if the viewport is part of a hanging protocol layout
|
||||
// we should not really rely on the old synchronizers and
|
||||
// you see below we only rehydrate the synchronizers if the viewport
|
||||
// is not part of the hanging protocol layout. HPs should
|
||||
// define their own synchronizers. Since the synchronizers are
|
||||
// viewportId dependent and
|
||||
// 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');
|
||||
}
|
||||
|
||||
// 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();
|
||||
if (!viewportId) {
|
||||
throw new Error('Viewport ID is required');
|
||||
}
|
||||
}, [elementRef]);
|
||||
|
||||
const cleanUpServices = useCallback(
|
||||
viewportInfo => {
|
||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||
const syncGroups = viewportInfo.getSyncGroups();
|
||||
// Make sure displaySetOptions has one object per displaySet
|
||||
while (displaySetOptions.length < displaySets.length) {
|
||||
displaySetOptions.push({});
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
[viewportId]
|
||||
);
|
||||
const {
|
||||
displaySetService,
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
syncGroupService,
|
||||
cornerstoneViewportService,
|
||||
segmentationService,
|
||||
cornerstoneCacheService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const elementEnabledHandler = useCallback(
|
||||
evt => {
|
||||
// check this is this element reference and return early if doesn't match
|
||||
if (evt.detail.element !== elementRef.current) {
|
||||
return;
|
||||
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 { viewportId, element } = evt.detail;
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
setEnabledElement(viewportId, element);
|
||||
setEnabledVPElement(element);
|
||||
const cleanUpServices = useCallback(
|
||||
viewportInfo => {
|
||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||
const syncGroups = viewportInfo.getSyncGroups();
|
||||
|
||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||
const toolGroupId = viewportInfo.getToolGroupId();
|
||||
const syncGroups = viewportInfo.getSyncGroups();
|
||||
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
||||
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
||||
|
||||
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;
|
||||
|
||||
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
|
||||
// If the viewport used to have a synchronizer, re apply it again
|
||||
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
|
||||
}
|
||||
|
||||
if (onElementEnabled) {
|
||||
onElementEnabled(evt);
|
||||
}
|
||||
},
|
||||
[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) {
|
||||
const elementEnabledHandler = useCallback(
|
||||
evt => {
|
||||
// check this is this element reference and return early if doesn't match
|
||||
if (evt.detail.element !== elementRef.current) {
|
||||
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);
|
||||
|
||||
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
|
||||
const viewportData = viewportInfo.getViewportData();
|
||||
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
|
||||
viewportData,
|
||||
invalidatedDisplaySetInstanceUID,
|
||||
dataSource,
|
||||
displaySetService
|
||||
);
|
||||
|
||||
const keepCamera = true;
|
||||
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
// handle the default viewportType to be stack
|
||||
if (!viewportOptions.viewportType) {
|
||||
viewportOptions.viewportType = STACK;
|
||||
}
|
||||
cornerstoneViewportService.storePresentation({ viewportId });
|
||||
|
||||
const loadViewportData = async () => {
|
||||
const viewportData = await cornerstoneCacheService.createViewportData(
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
dataSource,
|
||||
initialImageIndex
|
||||
);
|
||||
// This should be done after the store presentation since synchronizers
|
||||
// will get cleaned up and they need the viewportInfo to be present
|
||||
cleanUpServices(viewportInfo);
|
||||
|
||||
// The presentation state will have been stored previously by closing
|
||||
// a viewport. Otherwise, this viewport will be unchanged and the
|
||||
// presentation information will be directly carried over.
|
||||
const state = stateSyncService.getState();
|
||||
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
|
||||
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
|
||||
if (onElementDisabled && typeof onElementDisabled === 'function') {
|
||||
onElementDisabled(viewportInfo);
|
||||
}
|
||||
|
||||
const { presentationIds } = viewportOptions;
|
||||
const presentations = {
|
||||
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
||||
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
|
||||
cornerstoneViewportService.disableElement(viewportId);
|
||||
|
||||
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
||||
};
|
||||
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
|
||||
// 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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
cornerstoneViewportService.setViewportData(
|
||||
viewportId,
|
||||
viewportData,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
presentations
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
|
||||
const viewportData = viewportInfo.getViewportData();
|
||||
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
|
||||
viewportData,
|
||||
invalidatedDisplaySetInstanceUID,
|
||||
dataSource,
|
||||
displaySetService
|
||||
);
|
||||
|
||||
const keepCamera = true;
|
||||
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
|
||||
}
|
||||
}
|
||||
);
|
||||
if (measurement) {
|
||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
// handle the default viewportType to be stack
|
||||
if (!viewportOptions.viewportType) {
|
||||
viewportOptions.viewportType = STACK;
|
||||
}
|
||||
};
|
||||
|
||||
loadViewportData();
|
||||
}, [viewportOptions, displaySets, dataSource]);
|
||||
const loadViewportData = async () => {
|
||||
const viewportData = await cornerstoneCacheService.createViewportData(
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
dataSource,
|
||||
initialImageIndex
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 presentations = getViewportPresentations(viewportId, viewportOptions);
|
||||
|
||||
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
|
||||
elementRef,
|
||||
viewportId,
|
||||
servicesManager
|
||||
);
|
||||
let measurement;
|
||||
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
|
||||
measurement = cacheJumpToMeasurementEvent.measurement;
|
||||
// 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 () => {
|
||||
unsubscribeFromJumpToMeasurementEvents();
|
||||
};
|
||||
}, [displaySets, elementRef, viewportId]);
|
||||
cornerstoneViewportService.setViewportData(
|
||||
viewportId,
|
||||
viewportData,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
presentations
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
if (measurement) {
|
||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: In the future we should consider using the customization service
|
||||
// to determine if and in which corner various action components should go.
|
||||
const wlActionMenu = getWindowLevelActionMenu({
|
||||
viewportId,
|
||||
element: elementRef.current,
|
||||
loadViewportData();
|
||||
}, [viewportOptions, displaySets, dataSource]);
|
||||
|
||||
/**
|
||||
* 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,
|
||||
viewportId,
|
||||
viewportActionCornersService,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
|
||||
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
|
||||
appConfig,
|
||||
]);
|
||||
|
||||
const { ref: resizeRef } = useResizeDetector({
|
||||
onResize,
|
||||
});
|
||||
|
||||
viewportActionCornersService.setComponent({
|
||||
viewportId,
|
||||
id: 'windowLevelActionMenu',
|
||||
component: wlActionMenu,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
indexPriority: -100,
|
||||
});
|
||||
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
|
||||
|
||||
const { ref: resizeRef } = useResizeDetector({
|
||||
onResize,
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="viewport-wrapper">
|
||||
<div
|
||||
className="cornerstone-viewport-element"
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
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}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="viewport-wrapper">
|
||||
<div
|
||||
className="cornerstone-viewport-element"
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</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);
|
||||
<CinePlayer
|
||||
enabledVPElement={enabledVPElement}
|
||||
viewportId={viewportId}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
<ActiveViewportBehavior
|
||||
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) {
|
||||
const { measurementService, cornerstoneViewportService } = servicesManager.services;
|
||||
@ -542,12 +516,15 @@ function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesM
|
||||
}
|
||||
}
|
||||
|
||||
function _rehydrateSynchronizers(
|
||||
synchronizersStore: { [key: string]: unknown },
|
||||
viewportId: string,
|
||||
syncGroupService: any
|
||||
) {
|
||||
synchronizersStore[viewportId].forEach(synchronizerObj => {
|
||||
function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) {
|
||||
const { synchronizersStore } = useSynchronizersStore.getState();
|
||||
const synchronizers = synchronizersStore[viewportId];
|
||||
|
||||
if (!synchronizers) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronizers.forEach(synchronizerObj => {
|
||||
if (!synchronizerObj.id) {
|
||||
return;
|
||||
}
|
||||
@ -597,17 +574,91 @@ function _rehydrateSynchronizers(
|
||||
// Component displayName
|
||||
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
|
||||
|
||||
OHIFCornerstoneViewport.propTypes = {
|
||||
displaySets: PropTypes.array.isRequired,
|
||||
dataSource: PropTypes.object.isRequired,
|
||||
viewportOptions: PropTypes.object,
|
||||
displaySetOptions: PropTypes.arrayOf(PropTypes.any),
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
onElementEnabled: PropTypes.func,
|
||||
isJumpToMeasurementDisabled: PropTypes.bool,
|
||||
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||
// 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]),
|
||||
};
|
||||
function areEqual(prevProps, nextProps) {
|
||||
if (nextProps.needsRerendering) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change');
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -2,9 +2,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import PropTypes from 'prop-types';
|
||||
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
|
||||
import type { ImageSliceData } from '@cornerstonejs/core/types';
|
||||
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 { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||
|
||||
@ -45,14 +45,14 @@ const OverlayItemComponents = {
|
||||
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
|
||||
};
|
||||
|
||||
|
||||
const studyDateItem = {
|
||||
id: 'StudyDate',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Study date',
|
||||
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
|
||||
contentF: ({ referenceInstance, formatters: { formatDate } }) => formatDate(referenceInstance.StudyDate),
|
||||
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
|
||||
formatDate(referenceInstance.StudyDate),
|
||||
};
|
||||
|
||||
const seriesDescriptionItem = {
|
||||
@ -63,15 +63,19 @@ const seriesDescriptionItem = {
|
||||
condition: ({ referenceInstance }) => {
|
||||
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 bottomLeftItems = {
|
||||
id: 'cornerstoneOverlayBottomLeft', items: [
|
||||
id: 'cornerstoneOverlayBottomLeft',
|
||||
items: [
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
customizationType: 'ohif.overlayItem.windowLevel',
|
||||
@ -79,12 +83,12 @@ const bottomLeftItems = {
|
||||
{
|
||||
id: 'ZoomLevel',
|
||||
customizationType: 'ohif.overlayItem.zoomLevel',
|
||||
condition: (props) => {
|
||||
condition: props => {
|
||||
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
|
||||
return activeToolName === 'Zoom';
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
const bottomRightItems = {
|
||||
@ -94,7 +98,7 @@ const bottomRightItems = {
|
||||
id: 'InstanceNumber',
|
||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
@ -145,19 +149,18 @@ function CustomizableViewportOverlay({
|
||||
// append functionality. This code enables the historical usage, but
|
||||
// the recommended functionality is to append to the default values in
|
||||
// cornerstoneOverlay rather than defining individual items.
|
||||
const topLeftCustomization = customizationService.getCustomization(
|
||||
'cornerstoneOverlayTopLeft'
|
||||
) || cornerstoneOverlay?.topLeftItems;
|
||||
const topRightCustomization = customizationService.getCustomization(
|
||||
'cornerstoneOverlayTopRight'
|
||||
) || cornerstoneOverlay?.topRightItems;
|
||||
const bottomLeftCustomization = customizationService.getCustomization(
|
||||
'cornerstoneOverlayBottomLeft'
|
||||
) || cornerstoneOverlay?.bottomLeftItems;
|
||||
const bottomRightCustomization = customizationService.getCustomization(
|
||||
'cornerstoneOverlayBottomRight'
|
||||
) || cornerstoneOverlay?.bottomRightItems;
|
||||
|
||||
const topLeftCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
|
||||
cornerstoneOverlay?.topLeftItems;
|
||||
const topRightCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
|
||||
cornerstoneOverlay?.topRightItems;
|
||||
const bottomLeftCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
|
||||
cornerstoneOverlay?.bottomLeftItems;
|
||||
const bottomRightCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
|
||||
cornerstoneOverlay?.bottomRightItems;
|
||||
|
||||
const instanceNumber = useMemo(
|
||||
() =>
|
||||
@ -177,13 +180,12 @@ function CustomizableViewportOverlay({
|
||||
return {
|
||||
displaySets,
|
||||
displaySet,
|
||||
instance: instances[imageIndex],
|
||||
instance: instances?.[imageIndex],
|
||||
instances,
|
||||
referenceInstance,
|
||||
};
|
||||
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
|
||||
|
||||
|
||||
/**
|
||||
* Updating the VOI when the viewport changes its voi
|
||||
*/
|
||||
@ -307,7 +309,8 @@ function CustomizableViewportOverlay({
|
||||
<>
|
||||
{items.map((item, index) => (
|
||||
<div key={`${keyPrefix}_${index}`}>
|
||||
{(!item?.condition || item.condition(props)) && _renderOverlayItem(item, props) || null}
|
||||
{((!item?.condition || item.condition(props)) && _renderOverlayItem(item, props)) ||
|
||||
null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@ -316,7 +319,6 @@ function CustomizableViewportOverlay({
|
||||
[_renderOverlayItem]
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<ViewportOverlay
|
||||
topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
|
||||
@ -335,7 +337,9 @@ function getDisplaySets(viewportData, displaySetService) {
|
||||
if (!viewportData?.data?.length) {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@ -382,7 +386,6 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||
return parseInt(instanceNumber);
|
||||
}
|
||||
|
||||
|
||||
// Since volume viewports can be in any view direction, they can render
|
||||
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
|
||||
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
|
||||
@ -400,6 +403,11 @@ function _getInstanceNumberFromVolume(
|
||||
|
||||
// Todo: support fusion of acquisition plane which has instanceNumber
|
||||
const { volume } = volumes[0];
|
||||
|
||||
if (!volume) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { direction, imageIds } = volume;
|
||||
|
||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
@ -443,9 +451,10 @@ function OverlayItem(props) {
|
||||
style={{ color, background }}
|
||||
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>
|
||||
</div>);
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Enums, VolumeViewport3D } from '@cornerstonejs/core';
|
||||
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
||||
import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
|
||||
import { ImageScrollbar } from '@ohif/ui';
|
||||
|
||||
function CornerstoneImageScrollbar({
|
||||
@ -12,7 +11,9 @@ function CornerstoneImageScrollbar({
|
||||
setImageSliceData,
|
||||
scrollbarHeight,
|
||||
servicesManager,
|
||||
}: withAppTypes) {
|
||||
}: withAppTypes<{
|
||||
element: HTMLElement;
|
||||
}>) {
|
||||
const { cineService, cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const onImageScrollbarChange = (imageIndex, viewportId) => {
|
||||
@ -26,7 +27,7 @@ function CornerstoneImageScrollbar({
|
||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||
}
|
||||
|
||||
csToolsUtils.jumpToSlice(viewport.element, {
|
||||
csUtils.jumpToSlice(viewport.element, {
|
||||
imageIndex,
|
||||
debounceLoading: true,
|
||||
});
|
||||
@ -64,7 +65,9 @@ function CornerstoneImageScrollbar({
|
||||
|
||||
const updateIndex = event => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
if(!viewport || viewport instanceof VolumeViewport3D) return
|
||||
if (!viewport || viewport instanceof VolumeViewport3D) {
|
||||
return;
|
||||
}
|
||||
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
|
||||
const numberOfSlices = viewport.getNumberOfSlices();
|
||||
// find the index of imageId in the imageIds
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
metaData,
|
||||
Enums,
|
||||
Types,
|
||||
getEnabledElement,
|
||||
utilities as csUtils,
|
||||
} from '@cornerstonejs/core';
|
||||
import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
import PropTypes from 'prop-types';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
|
||||
import './ViewportOrientationMarkers.css';
|
||||
@ -31,8 +24,9 @@ function ViewportOrientationMarkers({
|
||||
|
||||
useEffect(() => {
|
||||
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
|
||||
const { rotation, previousCamera, camera } = evt.detail;
|
||||
const { previousCamera, camera } = evt.detail;
|
||||
|
||||
const { rotation } = camera;
|
||||
if (rotation !== undefined) {
|
||||
setRotation(rotation);
|
||||
}
|
||||
@ -74,7 +68,12 @@ function ViewportOrientationMarkers({
|
||||
return false;
|
||||
}
|
||||
|
||||
({ rowCosines, columnCosines, isDefaultValueSetForColumnCosine, isDefaultValueSetForColumnCosine } = metaData.get('imagePlaneModule', imageId) || {});
|
||||
({
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
isDefaultValueSetForColumnCosine,
|
||||
isDefaultValueSetForColumnCosine,
|
||||
} = metaData.get('imagePlaneModule', imageId) || {});
|
||||
} else {
|
||||
if (!element || !getEnabledElement(element)) {
|
||||
return '';
|
||||
@ -90,7 +89,13 @@ function ViewportOrientationMarkers({
|
||||
rowCosines = viewRight;
|
||||
}
|
||||
|
||||
if (!rowCosines || !columnCosines || rotation === undefined || isDefaultValueSetForRowCosine || isDefaultValueSetForColumnCosine) {
|
||||
if (
|
||||
!rowCosines ||
|
||||
!columnCosines ||
|
||||
rotation === undefined ||
|
||||
isDefaultValueSetForRowCosine ||
|
||||
isDefaultValueSetForColumnCosine
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@ -12,15 +12,23 @@ import {
|
||||
utilities as cstUtils,
|
||||
ReferenceLinesTool,
|
||||
} from '@cornerstonejs/tools';
|
||||
|
||||
import { Types as OhifTypes } from '@ohif/core';
|
||||
import {
|
||||
callLabelAutocompleteDialog,
|
||||
showLabelAnnotationPopup,
|
||||
createReportAsync,
|
||||
callInputDialog,
|
||||
colorPickerDialog,
|
||||
} from '@ohif/extension-default';
|
||||
import { vec3, mat4 } from 'gl-matrix';
|
||||
|
||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||
import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/callInputDialog';
|
||||
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
|
||||
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
import toggleVOISliceSync from './utils/toggleVOISliceSync';
|
||||
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
|
||||
|
||||
const toggleSyncFunctions = {
|
||||
imageSlice: toggleImageSliceSync,
|
||||
@ -29,6 +37,7 @@ const toggleSyncFunctions = {
|
||||
|
||||
function commandsModule({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
|
||||
const {
|
||||
@ -101,7 +110,49 @@ function commandsModule({
|
||||
|
||||
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 }) {
|
||||
return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates);
|
||||
},
|
||||
@ -435,9 +486,10 @@ function commandsModule({
|
||||
viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 });
|
||||
viewport.render();
|
||||
} else if (viewport.getRotation !== undefined) {
|
||||
const currentRotation = viewport.getRotation();
|
||||
const presentation = viewport.getViewPresentation();
|
||||
const { rotation: currentRotation } = presentation;
|
||||
const newRotation = (currentRotation + rotation) % 360;
|
||||
viewport.setProperties({ rotation: newRotation });
|
||||
viewport.setViewPresentation({ rotation: newRotation });
|
||||
viewport.render();
|
||||
}
|
||||
},
|
||||
@ -554,7 +606,7 @@ function commandsModule({
|
||||
|
||||
// Set slice to last slice
|
||||
const options = { imageIndex: jumpIndex };
|
||||
cstUtils.jumpToSlice(viewport.element, options);
|
||||
csUtils.jumpToSlice(viewport.element, options);
|
||||
},
|
||||
scroll: ({ direction }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement();
|
||||
@ -566,7 +618,7 @@ function commandsModule({
|
||||
const { viewport } = enabledElement;
|
||||
const options = { delta: direction };
|
||||
|
||||
cstUtils.scroll(viewport, options);
|
||||
csUtils.scroll(viewport, options);
|
||||
},
|
||||
setViewportColormap: ({
|
||||
viewportId,
|
||||
@ -577,7 +629,6 @@ function commandsModule({
|
||||
}) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
const actorEntries = viewport.getActors();
|
||||
let hpOpacity;
|
||||
// Retrieve active protocol's viewport match details
|
||||
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
|
||||
@ -596,14 +647,8 @@ function commandsModule({
|
||||
// HP takes priority over the default 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) {
|
||||
setViewportProperties(viewport, viewportId);
|
||||
viewport.setProperties({ colormap });
|
||||
}
|
||||
|
||||
if (viewport instanceof VolumeViewport) {
|
||||
@ -611,7 +656,9 @@ function commandsModule({
|
||||
const { viewports } = viewportGridService.getState();
|
||||
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
|
||||
}
|
||||
setViewportProperties(viewport, displaySetInstanceUID);
|
||||
|
||||
const volumeId = viewport.getVolumeId();
|
||||
viewport.setProperties({ colormap }, volumeId);
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
@ -827,6 +874,339 @@ function commandsModule({
|
||||
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 = {
|
||||
@ -988,6 +1368,84 @@ function commandsModule({
|
||||
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 {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { CinePlayer, useCine } from '@ohif/ui';
|
||||
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
|
||||
import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
function WrappedCinePlayer({
|
||||
@ -101,7 +100,7 @@ function WrappedCinePlayer({
|
||||
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
|
||||
enabledVPElement.addEventListener(
|
||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||
@ -111,7 +110,10 @@ function WrappedCinePlayer({
|
||||
return () => {
|
||||
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(
|
||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||
newDisplaySetHandler
|
||||
@ -181,13 +183,13 @@ function RenderCinePlayer({
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(
|
||||
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||
Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||
handleTimePointIndexChange
|
||||
);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(
|
||||
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||
Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||
handleTimePointIndexChange
|
||||
);
|
||||
};
|
||||
@ -199,7 +201,7 @@ function RenderCinePlayer({
|
||||
}
|
||||
|
||||
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
|
||||
const volume = cache.getVolume(volumeId);
|
||||
const volume = cache.getVolume(volumeId, true);
|
||||
volume.timePointIndex = timePointIndex;
|
||||
|
||||
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
||||
@ -207,7 +209,7 @@ function RenderCinePlayer({
|
||||
|
||||
const updateDynamicInfo = useCallback(props => {
|
||||
const { volumeId, timePointIndex } = props;
|
||||
const volume = cache.getVolume(volumeId);
|
||||
const volume = cache.getVolume(volumeId, true);
|
||||
volume.timePointIndex = timePointIndex;
|
||||
}, []);
|
||||
|
||||
@ -223,6 +225,7 @@ function RenderCinePlayer({
|
||||
isPlaying: false,
|
||||
});
|
||||
cineService.setIsCineEnabled(false);
|
||||
cineService.setViewportCineClosed(viewportId);
|
||||
}}
|
||||
onPlayPauseChange={isPlaying => {
|
||||
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 debounce from 'lodash.debounce';
|
||||
import { PanelSection, WindowLevel } from '@ohif/ui';
|
||||
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
|
||||
import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core';
|
||||
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
|
||||
import { Enums, eventTarget } from '@cornerstonejs/core';
|
||||
import { useActiveViewportDisplaySets } from '@ohif/core';
|
||||
import {
|
||||
getNodeOpacity,
|
||||
isPetVolumeWithDefaultOpacity,
|
||||
isVolumeWithConstantOpacity,
|
||||
getWindowLevelsData,
|
||||
} from './utils';
|
||||
|
||||
const { Events } = Enums;
|
||||
|
||||
@ -16,19 +21,16 @@ const ViewportWindowLevel = ({
|
||||
}>): ReactElement => {
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
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(
|
||||
(volumeIds: string[]) => {
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
const viewports = renderingEngine.getVolumeViewports();
|
||||
|
||||
return viewports.filter(vp => {
|
||||
const viewportVolumeIds = vp.getActors().map(actor => actor.uid);
|
||||
|
||||
const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId);
|
||||
return (
|
||||
volumeIds.length === viewportVolumeIds.length &&
|
||||
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
|
||||
@ -38,97 +40,10 @@ const ViewportWindowLevel = ({
|
||||
[cornerstoneViewportService]
|
||||
);
|
||||
|
||||
const getNodeOpacity = (volumeActor, nodeIndex) => {
|
||||
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||
const nodeValue = [];
|
||||
|
||||
volumeOpacity.getNodeValue(nodeIndex, nodeValue);
|
||||
|
||||
return nodeValue[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the opacity applied to the PET volume is something like
|
||||
* [{x: 0, y: 0}, {x: 0.1, y: [C]}, {x: [ANY], y: [C]}] where C is a
|
||||
* constant opacity value for all x's greater than 0.1
|
||||
*/
|
||||
const isPetVolumeWithDefaultOpacity = (volumeId, volumeActor) => {
|
||||
const volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (!volume) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const modality = volume.metadata.Modality;
|
||||
|
||||
if (modality !== 'PT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||
|
||||
// It must have at least two points (0 and 0.1)
|
||||
if (volumeOpacity.getSize() < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const node1Value = [];
|
||||
const node2Value = [];
|
||||
|
||||
volumeOpacity.getNodeValue(0, node1Value);
|
||||
volumeOpacity.getNodeValue(1, node2Value);
|
||||
|
||||
// First node must be (x:0, y:0} and the second one {x:0.1, y:any}
|
||||
if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedOpacity = node2Value[1];
|
||||
const opacitySize = volumeOpacity.getSize();
|
||||
const currentNodeValue = [];
|
||||
|
||||
// Any point after 0.1 must have the same opacity
|
||||
for (let i = 2; i < opacitySize; i++) {
|
||||
volumeOpacity.getNodeValue(i, currentNodeValue);
|
||||
|
||||
if (currentNodeValue[1] !== expectedOpacity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the opacity function has a constance opacity value for all x's
|
||||
*/
|
||||
const isVolumeWithConstantOpacity = volumeActor => {
|
||||
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||
const opacitySize = volumeOpacity.getSize();
|
||||
const firstNodeValue = [];
|
||||
|
||||
volumeOpacity.getNodeValue(0, firstNodeValue);
|
||||
|
||||
const firstNodeOpacity = firstNodeValue[1];
|
||||
|
||||
for (let i = 0; i < opacitySize; i++) {
|
||||
const currentNodeValue = [];
|
||||
|
||||
volumeOpacity.getNodeValue(0, currentNodeValue);
|
||||
|
||||
if (currentNodeValue[1] !== firstNodeOpacity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getVolumeOpacity = useCallback((viewport, volumeId) => {
|
||||
const volumeActor = viewport.getActor(volumeId).actor;
|
||||
const volumeActor = viewport.getActors().find(actor => actor.referencedId === volumeId)?.actor;
|
||||
|
||||
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
|
||||
// Get the opacity from the second node at 0.1
|
||||
return getNodeOpacity(volumeActor, 1);
|
||||
} else if (isVolumeWithConstantOpacity(volumeActor)) {
|
||||
return getNodeOpacity(volumeActor, 0);
|
||||
@ -137,85 +52,14 @@ const ViewportWindowLevel = ({
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
const getWindowLevelsData = useCallback(
|
||||
async (viewportId: number) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
const volumeIds = viewport.getActors().map(actor => actor.uid);
|
||||
const viewportProperties = viewport.getProperties();
|
||||
const { voiRange } = viewportProperties;
|
||||
const viewportVoi = voiRange
|
||||
? {
|
||||
windowWidth: voiRange.upper - voiRange.lower,
|
||||
windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const windowLevels = await Promise.all(
|
||||
volumeIds.map(async (volumeId, volumeIndex) => {
|
||||
const volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (!volume) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const opacity = getVolumeOpacity(viewport, volumeId);
|
||||
const { metadata, scaling } = volume;
|
||||
const modality = metadata.Modality;
|
||||
|
||||
// TODO: find a proper way to fix the histogram
|
||||
const options = {
|
||||
min: modality === 'PT' ? 0.1 : -999,
|
||||
max: modality === 'PT' ? 5 : 10000,
|
||||
};
|
||||
|
||||
const histogram =
|
||||
cachedHistograms[volumeId] ??
|
||||
(await getViewportVolumeHistogram(viewport, volume, options));
|
||||
const { voi: displaySetVOI, colormap: displaySetColormap } =
|
||||
viewportInfo.displaySetOptions[volumeIndex];
|
||||
let colormap;
|
||||
if (displaySetColormap) {
|
||||
colormap =
|
||||
csUtils.colormap.getColormap(displaySetColormap.name) ??
|
||||
vtkColorMaps.getPresetByName(displaySetColormap.name);
|
||||
}
|
||||
|
||||
const voi = !volumeIndex ? viewportVoi ?? displaySetVOI : displaySetVOI;
|
||||
|
||||
return {
|
||||
viewportId,
|
||||
modality,
|
||||
volumeId,
|
||||
volumeIndex,
|
||||
voi,
|
||||
histogram,
|
||||
colormap,
|
||||
step: scaling?.PT ? 0.05 : 1,
|
||||
opacity,
|
||||
showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const data = windowLevels.filter(Boolean);
|
||||
return data;
|
||||
},
|
||||
|
||||
[cachedHistograms, cornerstoneViewportService, getVolumeOpacity]
|
||||
);
|
||||
|
||||
const updateViewportHistograms = useCallback(() => {
|
||||
getWindowLevelsData(viewportId).then(windowLevels => {
|
||||
setWindowLevels(windowLevels);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
getWindowLevelsData(viewport, viewportInfo, getVolumeOpacity).then(data => {
|
||||
setWindowLevels(data);
|
||||
});
|
||||
}, [viewportId, getWindowLevelsData]);
|
||||
}, [viewportId, cornerstoneViewportService, getVolumeOpacity]);
|
||||
|
||||
const handleCornerstoneVOIModified = useCallback(
|
||||
e => {
|
||||
@ -252,8 +96,8 @@ const ViewportWindowLevel = ({
|
||||
[windowLevels]
|
||||
);
|
||||
|
||||
const debouncedHandleCornerstoneVOIModified = useCallback(
|
||||
debounce(handleCornerstoneVOIModified, 100),
|
||||
const debouncedHandleCornerstoneVOIModified = useMemo(
|
||||
() => debounce(handleCornerstoneVOIModified, 100),
|
||||
[handleCornerstoneVOIModified]
|
||||
);
|
||||
|
||||
@ -280,7 +124,7 @@ const ViewportWindowLevel = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportVolumeIds = viewport.getActors().map(actor => actor.uid);
|
||||
const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId);
|
||||
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
|
||||
|
||||
viewports.forEach(vp => {
|
||||
@ -291,49 +135,50 @@ const ViewportWindowLevel = ({
|
||||
[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(() => {
|
||||
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);
|
||||
eventTarget.addEventListener(
|
||||
Events.IMAGE_VOLUME_LOADING_COMPLETED,
|
||||
handleImageVolumeLoadingCompleted
|
||||
);
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
if (isLoading) {
|
||||
updateViewportHistograms();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(
|
||||
Events.IMAGE_VOLUME_LOADING_COMPLETED,
|
||||
updateViewportHistograms
|
||||
);
|
||||
|
||||
document.removeEventListener(
|
||||
Events.VOI_MODIFIED,
|
||||
debouncedHandleCornerstoneVOIModified,
|
||||
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(() => {
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(
|
||||
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 () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewportId, cornerstoneViewportService, updateViewportHistograms]);
|
||||
}, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
|
||||
|
||||
return (
|
||||
<PanelSection title="Window Level">
|
||||
|
||||
@ -20,10 +20,6 @@ const workerFn = () => {
|
||||
const getViewportVolumeHistogram = async (viewport, volume, options?) => {
|
||||
workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions);
|
||||
|
||||
if (!volume?.loadStatus.loaded) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const volumeImageData = viewport.getImageData(volume.volumeId);
|
||||
|
||||
if (!volumeImageData) {
|
||||
@ -32,12 +28,15 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
|
||||
|
||||
let scalarData = volume.scalarData;
|
||||
|
||||
let prevTimePoint;
|
||||
if (volume.numTimePoints > 1) {
|
||||
prevTimePoint = volume.timePointIndex;
|
||||
const middleTimePoint = Math.round(volume.numTimePoints / 2);
|
||||
volume.timePointIndex = middleTimePoint;
|
||||
scalarData = volume.getScalarData(middleTimePoint);
|
||||
const targetTimePoint = volume.numTimePoints - 1; // or any other time point you need
|
||||
scalarData = volume.voxelManager.getTimePointScalarData(targetTimePoint);
|
||||
} else {
|
||||
scalarData = volume.voxelManager.getCompleteScalarDataArray();
|
||||
}
|
||||
|
||||
if (!scalarData?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { dimensions, origin, direction, spacing } = volume;
|
||||
@ -50,11 +49,12 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
|
||||
scalarData,
|
||||
});
|
||||
|
||||
// after we calculate the range let's reset the timePointIndex
|
||||
if (volume.numTimePoints > 1) {
|
||||
volume.timePointIndex = prevTimePoint;
|
||||
}
|
||||
const { minimum: min, maximum: max } = range;
|
||||
|
||||
if (min === Infinity || max === -Infinity) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const calcHistOptions = {
|
||||
numBins: 256,
|
||||
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 { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui';
|
||||
import { StackViewport } from '@cornerstonejs/core';
|
||||
import { StackViewport, Types } from '@cornerstonejs/core';
|
||||
import { ColormapProps } from '../../types/Colormap';
|
||||
|
||||
export function Colormap({
|
||||
@ -54,8 +54,10 @@ export function Colormap({
|
||||
return colormap;
|
||||
}
|
||||
const actorEntries = viewport.getActors();
|
||||
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
|
||||
const { colormap } = viewport.getProperties(actorEntry.uid);
|
||||
const actorEntry = actorEntries?.find(entry =>
|
||||
entry.referencedId.includes(displaySetInstanceUID)
|
||||
);
|
||||
const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId);
|
||||
if (!colormap) {
|
||||
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
|
||||
}
|
||||
|
||||
@ -15,12 +15,12 @@ import { ViewportPreset } from '../../types/ViewportPresets';
|
||||
import { VolumeViewport3D } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
|
||||
export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
|
||||
|
||||
export type WindowLevelActionMenuProps = {
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
||||
verticalDirection: AllInOneMenu.VerticalDirection;
|
||||
horizontalDirection: AllInOneMenu.HorizontalDirection;
|
||||
colorbarProperties: ColorbarProperties;
|
||||
displaySets: Array<any>;
|
||||
volumeRenderingPresets: Array<ViewportPreset>;
|
||||
@ -53,8 +53,6 @@ export function WindowLevelActionMenu({
|
||||
const backgroundColor = viewportInfo.getViewportOptions().background;
|
||||
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
|
||||
|
||||
const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
|
||||
|
||||
const { t } = useTranslation('WindowLevelActionMenu');
|
||||
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
@ -119,7 +117,7 @@ export function WindowLevelActionMenu({
|
||||
horizontalDirection={horizontalDirection}
|
||||
iconClassName={classNames(
|
||||
// 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',
|
||||
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60'
|
||||
)}
|
||||
@ -133,7 +131,7 @@ export function WindowLevelActionMenu({
|
||||
{!is3DVolume && (
|
||||
<Colorbar
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
|
||||
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
colorbarProperties={colorbarProperties}
|
||||
@ -149,7 +147,7 @@ export function WindowLevelActionMenu({
|
||||
<Colormap
|
||||
colormaps={colormaps}
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
|
||||
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
|
||||
import { nonWLModalities, WindowLevelActionMenu } from './WindowLevelActionMenu';
|
||||
|
||||
export function getWindowLevelActionMenu({
|
||||
viewportId,
|
||||
@ -9,7 +9,11 @@ export function getWindowLevelActionMenu({
|
||||
commandsManager,
|
||||
verticalDirection,
|
||||
horizontalDirection,
|
||||
}: withAppTypes): ReactNode {
|
||||
}: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
displaySets: AppTypes.DisplaySet[];
|
||||
}>): ReactNode {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
|
||||
@ -24,6 +28,14 @@ export function getWindowLevelActionMenu({
|
||||
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 (
|
||||
<WindowLevelActionMenu
|
||||
viewportId={viewportId}
|
||||
|
||||
@ -35,8 +35,8 @@ export const ViewportActionCornersContext = createContext(DEFAULT_STATE);
|
||||
export function ViewportActionCornersProvider({ children, service }) {
|
||||
const viewportActionCornersReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_ACTION_COMPONENT': {
|
||||
const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
|
||||
case 'ADD_ACTION_COMPONENT': {
|
||||
const { viewportId, id, component, location, indexPriority } = action.payload;
|
||||
// Get the components at the specified location of the specified viewport.
|
||||
let locationComponents = state?.[viewportId]?.[location]
|
||||
? [...state[viewportId][location]]
|
||||
@ -56,27 +56,48 @@ export function ViewportActionCornersProvider({ children, service }) {
|
||||
// Insert the component from the payload but
|
||||
// do not insert an undefined or null component.
|
||||
if (component) {
|
||||
const insertionIndex = locationComponents.findIndex(
|
||||
component => indexPriority <= component.indexPriority
|
||||
);
|
||||
let insertionIndex;
|
||||
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.slice(0, insertionIndex),
|
||||
{
|
||||
id,
|
||||
component,
|
||||
indexPriority,
|
||||
indexPriority: indexPriority ?? defaultPriority,
|
||||
},
|
||||
...locationComponents.slice(insertionIndex + 1),
|
||||
...locationComponents.slice(insertionIndex),
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
...{
|
||||
[viewportId]: {
|
||||
...state[viewportId],
|
||||
[location]: locationComponents,
|
||||
},
|
||||
[viewportId]: {
|
||||
...state[viewportId],
|
||||
[location]: locationComponents,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -100,17 +121,17 @@ export function ViewportActionCornersProvider({ children, service }) {
|
||||
return viewportActionCornersState;
|
||||
}, [viewportActionCornersState]);
|
||||
|
||||
const setComponent = useCallback(
|
||||
const addComponent = useCallback(
|
||||
(actionComponentInfo: ActionComponentInfo) => {
|
||||
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo });
|
||||
dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo });
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setComponents = useCallback(
|
||||
const addComponents = useCallback(
|
||||
(actionComponentInfos: Array<ActionComponentInfo>) => {
|
||||
actionComponentInfos.forEach(actionComponentInfo =>
|
||||
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo })
|
||||
dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo })
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
@ -120,28 +141,28 @@ export function ViewportActionCornersProvider({ children, service }) {
|
||||
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation({
|
||||
getState,
|
||||
setComponent,
|
||||
setComponents,
|
||||
addComponent,
|
||||
addComponents,
|
||||
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 api = {
|
||||
const viewportCornerActions = {
|
||||
getState,
|
||||
setComponent: props => service.setComponent(props),
|
||||
setComponents: props => service.setComponents(props),
|
||||
addComponent: props => service.addComponent(props),
|
||||
addComponents: props => service.addComponents(props),
|
||||
clear: props => service.clear(props),
|
||||
};
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => [viewportActionCornersState, api],
|
||||
[viewportActionCornersState, api]
|
||||
() => [viewportActionCornersState, viewportCornerActions],
|
||||
[viewportActionCornersState, viewportCornerActions]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import { toolNames } from './initCornerstoneTools';
|
||||
import DicomUpload from './components/DicomUpload/DicomUpload';
|
||||
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
|
||||
import { colormaps } from './utils/colormaps';
|
||||
import { CONSTANTS } from '@cornerstonejs/core';
|
||||
@ -23,10 +22,12 @@ const tools = {
|
||||
toolName: toolNames.Zoom,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
|
||||
},
|
||||
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] },
|
||||
{
|
||||
toolName: toolNames.StackScroll,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
|
||||
},
|
||||
],
|
||||
enabled: [
|
||||
{ toolName: toolNames.SegmentationDisplay },
|
||||
{
|
||||
toolName: toolNames.PlanarFreehandContourSegmentation,
|
||||
configuration: {
|
||||
@ -38,13 +39,6 @@ const tools = {
|
||||
|
||||
function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: 'cornerstoneDicomUploadComponent',
|
||||
value: {
|
||||
id: 'dicomUploadComponent',
|
||||
component: DicomUpload,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
@ -120,7 +114,7 @@ function getCustomizationModule() {
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
value: 'areaUnit',
|
||||
value: 'areaUnits',
|
||||
for: ['area'],
|
||||
type: 'unit',
|
||||
},
|
||||
@ -140,7 +134,7 @@ function getCustomizationModule() {
|
||||
},
|
||||
{
|
||||
displayName: 'Unit',
|
||||
value: 'areaUnit',
|
||||
value: 'areaUnits',
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
@ -170,12 +164,12 @@ function getCustomizationModule() {
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
value: 'modalityUnit',
|
||||
value: 'pixelValueUnits',
|
||||
for: ['mean', 'max' /** 'stdDev **/],
|
||||
type: 'unit',
|
||||
},
|
||||
{
|
||||
value: 'areaUnit',
|
||||
value: 'areaUnits',
|
||||
for: ['area'],
|
||||
type: 'unit',
|
||||
},
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useAppConfig } from '@state';
|
||||
import { Toolbox } from '@ohif/ui-next';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
||||
|
||||
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const wrappedPanelSegmentation = ({ configuration }) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
@ -17,16 +13,25 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
||||
extensionManager={extensionManager}
|
||||
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 [appConfig] = useAppConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
@ -45,8 +50,6 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
disableEditing: appConfig.disableEditing,
|
||||
...customizationService.get('segmentation.panel'),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@ -54,6 +57,12 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'activeViewportWindowLevel',
|
||||
component: () => {
|
||||
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'panelSegmentation',
|
||||
iconName: 'tab-segmentation',
|
||||
@ -61,6 +70,13 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentation,
|
||||
},
|
||||
{
|
||||
name: 'panelSegmentationNoHeader',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentationNoHeader,
|
||||
},
|
||||
{
|
||||
name: 'panelSegmentationWithTools',
|
||||
iconName: 'tab-segmentation',
|
||||
@ -89,7 +89,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
const displaySet = {
|
||||
plugin: 'microscopy',
|
||||
Modality: 'SM',
|
||||
viewportType: csEnums.ViewportType.WholeSlide,
|
||||
viewportType: csEnums.ViewportType.WHOLE_SLIDE,
|
||||
altImageText: 'Microscopy',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SOPInstanceUID,
|
||||
|
||||
@ -6,6 +6,12 @@ const getToggledClassName = (isToggled: boolean) => {
|
||||
: '!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) {
|
||||
const {
|
||||
toolGroupService,
|
||||
@ -20,6 +26,52 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
return [
|
||||
// functions/helpers to be used by the toolbar buttons to decide if they should
|
||||
// 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',
|
||||
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
|
||||
@ -32,11 +84,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
const toolName = toolbarService.getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
return getDisabledState(disabledText);
|
||||
}
|
||||
|
||||
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',
|
||||
evaluate: ({ viewportId, button }) => {
|
||||
@ -254,11 +268,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
});
|
||||
|
||||
if (!areReconstructable) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
return getDisabledState(disabledText);
|
||||
}
|
||||
|
||||
const isMpr = protocol?.id === 'mpr';
|
||||
@ -288,11 +298,7 @@ function _evaluateToggle({
|
||||
const toolName = toolbarService.getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup.hasTool(toolName)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
return getDisabledState(disabledText);
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
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 = {
|
||||
id: 'mpr',
|
||||
name: 'MPR',
|
||||
@ -10,7 +30,6 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
||||
modifiedDate: '2023-08-15',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
// Unknown number of priors referenced - so just match any study
|
||||
numberOfPriorsReferenced: 0,
|
||||
protocolMatchingRules: [],
|
||||
imageLoadStrategy: 'nth',
|
||||
@ -71,17 +90,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'mpr',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
@ -98,17 +107,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'mpr',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
@ -125,17 +124,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'mpr',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
imageLoadPoolManager,
|
||||
imageRetrievalPoolManager,
|
||||
} from '@cornerstonejs/core';
|
||||
import * as csStreamingImageVolumeLoader from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||
import { Types } from '@ohif/core';
|
||||
import Enums from './enums';
|
||||
@ -33,17 +32,28 @@ import { id } from './id';
|
||||
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||
import { showLabelAnnotationPopup } from './utils/callInputDialog';
|
||||
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
||||
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
|
||||
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
||||
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
import { findNearbyToolData } from './utils/findNearbyToolData';
|
||||
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
|
||||
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 Component = React.lazy(() => {
|
||||
@ -84,9 +94,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
]);
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(segmentationService, [
|
||||
segmentationService.EVENTS.SEGMENTATION_ADDED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
segmentationService.EVENTS.SEGMENTATION_UPDATED,
|
||||
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||
]);
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
|
||||
@ -108,9 +117,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
// how to define them.
|
||||
imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions);
|
||||
},
|
||||
|
||||
getPanelModule,
|
||||
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
|
||||
// on the mode exits
|
||||
Object.values(cs3DEnums.RequestType).forEach(type => {
|
||||
@ -121,6 +130,13 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
cineService.setIsCineEnabled(false);
|
||||
|
||||
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;
|
||||
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
|
||||
servicesManager.services.customizationService.setGlobalCustomization('dicomUploadComponent', {
|
||||
component: props => <DicomUpload {...props} />,
|
||||
});
|
||||
return init.call(this, props);
|
||||
},
|
||||
|
||||
getToolbarModule,
|
||||
getPanelModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
name: 'activeViewportWindowLevel',
|
||||
component: () => {
|
||||
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
getHangingProtocolModule,
|
||||
getViewportModule({ servicesManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
@ -196,7 +204,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
getEnabledElement,
|
||||
dicomLoaderService,
|
||||
showLabelAnnotationPopup,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -235,6 +242,17 @@ export {
|
||||
ImageOverlayViewerTool,
|
||||
getSOPInstanceAttributes,
|
||||
dicomLoaderService,
|
||||
// Export all stores
|
||||
useLutPresentationStore,
|
||||
usePositionPresentationStore,
|
||||
useSegmentationPresentationStore,
|
||||
useSynchronizersStore,
|
||||
Enums,
|
||||
useMeasurements,
|
||||
useActiveViewportSegmentationRepresentations,
|
||||
useSegmentations,
|
||||
PanelSegmentation,
|
||||
PanelMeasurement,
|
||||
DicomUpload,
|
||||
};
|
||||
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 * as cornerstone from '@cornerstonejs/core';
|
||||
@ -13,12 +13,13 @@ import {
|
||||
getEnabledElement,
|
||||
Settings,
|
||||
utilities as csUtilities,
|
||||
Enums as csEnums,
|
||||
} from '@cornerstonejs/core';
|
||||
import {
|
||||
cornerstoneStreamingImageVolumeLoader,
|
||||
cornerstoneStreamingDynamicImageVolumeLoader,
|
||||
} from '@cornerstonejs/streaming-image-volume-loader';
|
||||
} from '@cornerstonejs/core/loaders';
|
||||
|
||||
import RequestTypes from '@cornerstonejs/core/enums/RequestType';
|
||||
|
||||
import initWADOImageLoader from './initWADOImageLoader';
|
||||
import initCornerstoneTools from './initCornerstoneTools';
|
||||
@ -33,6 +34,10 @@ import initContextMenu from './initContextMenu';
|
||||
import initDoubleClick from './initDoubleClick';
|
||||
import initViewTiming from './utils/initViewTiming';
|
||||
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;
|
||||
|
||||
@ -46,27 +51,12 @@ export default async function init({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
appConfig
|
||||
}: Types.Extensions.ExtensionParams): Promise<void> {
|
||||
appConfig,
|
||||
}: withAppTypes): Promise<void> {
|
||||
// Note: this should run first before initializing the cornerstone
|
||||
// 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({
|
||||
rendering: {
|
||||
preferSizeOverAccuracy: Boolean(appConfig.preferSizeOverAccuracy),
|
||||
useNorm16Texture: Boolean(appConfig.useNorm16Texture),
|
||||
},
|
||||
peerImport: appConfig.peerImport,
|
||||
});
|
||||
|
||||
@ -99,63 +89,39 @@ export default async function init({
|
||||
cornerstoneViewportService,
|
||||
hangingProtocolService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
studyPrefetcherService,
|
||||
} = servicesManager.services;
|
||||
|
||||
window.services = servicesManager.services;
|
||||
window.extensionManager = extensionManager;
|
||||
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()) {
|
||||
_showCPURenderingModal(uiModalService, hangingProtocolService);
|
||||
}
|
||||
const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState();
|
||||
|
||||
// Stores a map from `lutPresentationId` to a Presentation object so that
|
||||
// an OHIFCornerstoneViewport can be redisplayed with the same LUT
|
||||
stateSyncService.register('lutPresentationStore', { clearOnModeExit: true });
|
||||
const { getPresentationId: getSegmentationPresentationId } =
|
||||
useSegmentationPresentationStore.getState();
|
||||
|
||||
// Stores synchronizers state to be restored
|
||||
stateSyncService.register('synchronizersStore', { clearOnModeExit: true });
|
||||
const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState();
|
||||
|
||||
// Stores a map from `positionPresentationId` to a Presentation object so that
|
||||
// an OHIFCornerstoneViewport can be redisplayed with the same position
|
||||
stateSyncService.register('positionPresentationStore', {
|
||||
clearOnModeExit: true,
|
||||
});
|
||||
// register presentation id providers
|
||||
viewportGridService.addPresentationIdProvider(
|
||||
'positionPresentationId',
|
||||
getPositionPresentationId
|
||||
);
|
||||
viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId);
|
||||
viewportGridService.addPresentationIdProvider(
|
||||
'segmentationPresentationId',
|
||||
getSegmentationPresentationId
|
||||
);
|
||||
|
||||
// Stores the entire ViewportGridService getState when toggling to one up
|
||||
// (e.g. via a double click) so that it can be restored when toggling back.
|
||||
stateSyncService.register('toggleOneUpViewportGridStore', {
|
||||
clearOnModeExit: true,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
cornerstoneTools.segmentation.config.style.setStyle(
|
||||
{ type: SegmentationRepresentations.Contour },
|
||||
{
|
||||
renderFill: false,
|
||||
}
|
||||
);
|
||||
|
||||
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
|
||||
// connections.
|
||||
imageLoadPoolManager.maxNumRequests = {
|
||||
interaction: appConfig?.maxNumRequests?.interaction || 10,
|
||||
thumbnail: appConfig?.maxNumRequests?.thumbnail || 5,
|
||||
prefetch: appConfig?.maxNumRequests?.prefetch || 5,
|
||||
[RequestTypes.Interaction]: appConfig?.maxNumRequests?.interaction || 10,
|
||||
[RequestTypes.Thumbnail]: appConfig?.maxNumRequests?.thumbnail || 5,
|
||||
[RequestTypes.Prefetch]: appConfig?.maxNumRequests?.prefetch || 5,
|
||||
[RequestTypes.Compute]: appConfig?.maxNumRequests?.compute || 10,
|
||||
};
|
||||
|
||||
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
|
||||
@ -201,17 +168,23 @@ export default async function init({
|
||||
hangingProtocolService.subscribe(
|
||||
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
|
||||
volumeInputArrayMap => {
|
||||
const { lutPresentationStore } = useLutPresentationStore.getState();
|
||||
const { segmentationPresentationStore } = useSegmentationPresentationStore.getState();
|
||||
const { positionPresentationStore } = usePositionPresentationStore.getState();
|
||||
|
||||
for (const entry of volumeInputArrayMap.entries()) {
|
||||
const [viewportId, volumeInputArray] = entry;
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
|
||||
const { presentationIds } = ohifViewport.getViewportOptions();
|
||||
|
||||
const presentations = {
|
||||
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
||||
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
|
||||
segmentationPresentation:
|
||||
segmentationPresentationStore[presentationIds?.segmentationPresentationId],
|
||||
};
|
||||
|
||||
cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations);
|
||||
@ -249,10 +222,6 @@ export default async function init({
|
||||
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_ERROR, imageLoadFailedHandler);
|
||||
|
||||
@ -269,26 +238,11 @@ export default async function init({
|
||||
commandsManager.runCommand('resetCrosshairs', { viewportId });
|
||||
});
|
||||
|
||||
// eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
|
||||
|
||||
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_DISABLED, elementDisabledHandler.bind(null));
|
||||
colormaps.forEach(registerColormap);
|
||||
|
||||
// Event listener
|
||||
@ -301,8 +255,45 @@ export default async function init({
|
||||
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() {
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
import { cache, Types } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
|
||||
function _getVolumesFromViewport(viewport) {
|
||||
return viewport ? viewport.getActors().map(actor => cache.getVolume(actor.uid)) : [];
|
||||
}
|
||||
|
||||
function _getVolumeFromViewport(viewport) {
|
||||
const volumes = _getVolumesFromViewport(viewport).filter(volume => volume);
|
||||
function _getVolumeFromViewport(viewport: Types.IBaseVolumeViewport) {
|
||||
const volumeIds = viewport.getAllVolumeIds();
|
||||
const volumes = volumeIds.map(id => cache.getVolume(id));
|
||||
const dynamicVolume = volumes.find(volume => volume.isDynamicVolume());
|
||||
|
||||
return dynamicVolume ?? volumes[0];
|
||||
|
||||
@ -2,9 +2,8 @@ import {
|
||||
PanTool,
|
||||
WindowLevelTool,
|
||||
StackScrollTool,
|
||||
StackScrollMouseWheelTool,
|
||||
VolumeRotateTool,
|
||||
ZoomTool,
|
||||
VolumeRotateMouseWheelTool,
|
||||
MIPJumpToClickTool,
|
||||
LengthTool,
|
||||
RectangleROITool,
|
||||
@ -19,7 +18,6 @@ import {
|
||||
CobbAngleTool,
|
||||
MagnifyTool,
|
||||
CrosshairsTool,
|
||||
SegmentationDisplayTool,
|
||||
RectangleScissorsTool,
|
||||
SphereScissorsTool,
|
||||
CircleScissorsTool,
|
||||
@ -47,15 +45,15 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
CrosshairsTool.isAnnotation = false;
|
||||
ReferenceLinesTool.isAnnotation = false;
|
||||
AdvancedMagnifyTool.isAnnotation = false;
|
||||
PlanarFreehandContourSegmentationTool.isAnnotation = false;
|
||||
|
||||
init(configuration);
|
||||
addTool(PanTool);
|
||||
addTool(WindowLevelTool);
|
||||
addTool(StackScrollMouseWheelTool);
|
||||
addTool(StackScrollTool);
|
||||
addTool(VolumeRotateTool);
|
||||
addTool(ZoomTool);
|
||||
addTool(ProbeTool);
|
||||
addTool(VolumeRotateMouseWheelTool);
|
||||
addTool(MIPJumpToClickTool);
|
||||
addTool(LengthTool);
|
||||
addTool(RectangleROITool);
|
||||
@ -69,7 +67,6 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(CobbAngleTool);
|
||||
addTool(MagnifyTool);
|
||||
addTool(CrosshairsTool);
|
||||
addTool(SegmentationDisplayTool);
|
||||
addTool(RectangleScissorsTool);
|
||||
addTool(SphereScissorsTool);
|
||||
addTool(CircleScissorsTool);
|
||||
@ -108,9 +105,8 @@ const toolNames = {
|
||||
ArrowAnnotate: ArrowAnnotateTool.toolName,
|
||||
WindowLevel: WindowLevelTool.toolName,
|
||||
StackScroll: StackScrollTool.toolName,
|
||||
StackScrollMouseWheel: StackScrollMouseWheelTool.toolName,
|
||||
Zoom: ZoomTool.toolName,
|
||||
VolumeRotateMouseWheel: VolumeRotateMouseWheelTool.toolName,
|
||||
VolumeRotate: VolumeRotateTool.toolName,
|
||||
MipJumpToClick: MIPJumpToClickTool.toolName,
|
||||
Length: LengthTool.toolName,
|
||||
DragProbe: DragProbeTool.toolName,
|
||||
@ -124,7 +120,6 @@ const toolNames = {
|
||||
CobbAngle: CobbAngleTool.toolName,
|
||||
Magnify: MagnifyTool.toolName,
|
||||
Crosshairs: CrosshairsTool.toolName,
|
||||
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
||||
Brush: BrushTool.toolName,
|
||||
PaintFill: PaintFillTool.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 { DicomMetadataStore } from '@ohif/core';
|
||||
|
||||
@ -7,6 +7,7 @@ import { toolNames } from './initCornerstoneTools';
|
||||
import { onCompletedCalibrationLine } from './tools/CalibrationLineTool';
|
||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||
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 { removeAnnotation } = annotation.state;
|
||||
@ -344,8 +345,7 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
|
||||
return;
|
||||
}
|
||||
|
||||
const { uid, label } = measurement;
|
||||
|
||||
const { uid, label, isLocked, isVisible } = measurement;
|
||||
const sourceAnnotation = annotation.state.getAnnotation(uid);
|
||||
const { data, metadata } = sourceAnnotation;
|
||||
|
||||
@ -361,7 +361,23 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
|
||||
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 {
|
||||
cornerstoneStreamingImageVolumeLoader,
|
||||
cornerstoneStreamingDynamicImageVolumeLoader,
|
||||
} from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader';
|
||||
import dicomParser from 'dicom-parser';
|
||||
} from '@cornerstonejs/core/loaders';
|
||||
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
|
||||
import { errorHandler, utils } from '@ohif/core';
|
||||
|
||||
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(
|
||||
userAuthenticationService,
|
||||
appConfig,
|
||||
extensionManager
|
||||
) {
|
||||
dicomImageLoader.external.cornerstone = cornerstone;
|
||||
dicomImageLoader.external.dicomParser = dicomParser;
|
||||
|
||||
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
|
||||
|
||||
registerVolumeLoader(
|
||||
@ -49,17 +20,11 @@ export default function initWADOImageLoader(
|
||||
cornerstoneStreamingDynamicImageVolumeLoader
|
||||
);
|
||||
|
||||
dicomImageLoader.configure({
|
||||
decodeConfig: {
|
||||
// !! IMPORTANT !!
|
||||
// We should set this flag to false, since, by default @cornerstonejs/dicom-image-loader
|
||||
// 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),
|
||||
},
|
||||
dicomImageLoader.init({
|
||||
maxWebWorkers: Math.min(
|
||||
Math.max(navigator.hardwareConcurrency - 1, 1),
|
||||
appConfig.maxNumberOfWebWorkers
|
||||
),
|
||||
beforeSend: function (xhr) {
|
||||
//TODO should be removed in the future and request emitted by DicomWebDataSource
|
||||
const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {};
|
||||
@ -84,16 +49,8 @@ export default function initWADOImageLoader(
|
||||
errorHandler.getHTTPErrorHandler(error);
|
||||
},
|
||||
});
|
||||
|
||||
initWebWorkers(appConfig);
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
// Note: we don't want to call .terminate on the webWorkerManager since
|
||||
// that resets the config
|
||||
const webWorkers = webWorkerManager.webWorkers;
|
||||
for (let i = 0; i < webWorkers.length; i++) {
|
||||
webWorkers[i].worker.terminate();
|
||||
}
|
||||
webWorkers.length = 0;
|
||||
console.debug('Destroying WADO Image Loader');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
* 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) {
|
||||
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
|
||||
const viewport = renderingEngine.getViewport(viewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = viewport;
|
||||
const actorEntries = viewport.getActors();
|
||||
|
||||
if (!actorEntries || actorEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { position, width: thickness, activeColormapName, colormaps } = options;
|
||||
|
||||
const numContainers = displaySetInstanceUIDs.length;
|
||||
@ -74,11 +95,9 @@ export default class ColorbarService extends PubSubService {
|
||||
);
|
||||
|
||||
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
|
||||
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
|
||||
const volumeId = actorEntry?.uid;
|
||||
const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID);
|
||||
const properties = viewport?.getProperties(volumeId);
|
||||
const colormap = properties?.colormap;
|
||||
// if there's an initial colormap set, and no colormap on the viewport, set it
|
||||
if (activeColormapName && !colormap) {
|
||||
this.setViewportColormap(
|
||||
viewportId,
|
||||
@ -196,9 +215,8 @@ export default class ColorbarService extends PubSubService {
|
||||
return;
|
||||
}
|
||||
const setViewportProperties = (viewport, uid) => {
|
||||
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
|
||||
const { actor: volumeActor, uid: volumeId } = actorEntry;
|
||||
viewport.setProperties({ colormap, volumeActor }, volumeId);
|
||||
const volumeId = this.getVolumeIdForIdentifier(viewport, uid);
|
||||
viewport.setProperties({ colormap }, volumeId);
|
||||
};
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||
@ -33,27 +33,11 @@ class CornerstoneCacheService {
|
||||
|
||||
public async createViewportData(
|
||||
displaySets: Types.DisplaySet[],
|
||||
viewportOptions: Record<string, unknown>,
|
||||
viewportOptions: AppTypes.ViewportGrid.GridViewportOptions,
|
||||
dataSource: unknown,
|
||||
initialImageIndex?: number
|
||||
): Promise<StackViewportData | VolumeViewportData> {
|
||||
let 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 viewportType = viewportOptions.viewportType as string;
|
||||
|
||||
const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets);
|
||||
let viewportData: StackViewportData | VolumeViewportData;
|
||||
@ -86,7 +70,7 @@ class CornerstoneCacheService {
|
||||
}
|
||||
|
||||
public async invalidateViewportData(
|
||||
viewportData: VolumeViewportData,
|
||||
viewportData: VolumeViewportData | StackViewportData,
|
||||
invalidatedDisplaySetInstanceUID: string,
|
||||
dataSource,
|
||||
displaySetService
|
||||
@ -176,9 +160,6 @@ class CornerstoneCacheService {
|
||||
): Promise<StackViewportData> {
|
||||
const { uiNotificationService } = this.servicesManager.services;
|
||||
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) {
|
||||
if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) {
|
||||
const { userAuthenticationService } = this.servicesManager.services;
|
||||
@ -187,7 +168,7 @@ class CornerstoneCacheService {
|
||||
await overlayDisplaySet.load({ headers });
|
||||
} catch (e) {
|
||||
uiNotificationService.show({
|
||||
title: 'Error loading overlayDisplaySet',
|
||||
title: 'Error loading displaySet',
|
||||
message: e.message,
|
||||
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
|
||||
const StackViewportData = [displaySet, ...overlayDisplaySets].map(ds => {
|
||||
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = ds;
|
||||
const StackViewportData = [];
|
||||
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,
|
||||
displaySetInstanceUID,
|
||||
isCompositeStack,
|
||||
imageIds: stackImageIds,
|
||||
initialImageIndex,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
viewportType,
|
||||
@ -253,6 +234,7 @@ class CornerstoneCacheService {
|
||||
for (const displaySet of displaySets) {
|
||||
const { Modality } = displaySet;
|
||||
const isParametricMap = Modality === 'PMAP';
|
||||
const isSeg = Modality === 'SEG';
|
||||
|
||||
// Don't create volumes for the displaySets that have custom load
|
||||
// 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
|
||||
// therefore a new volume should not be created.
|
||||
if (!isParametricMap && (!volumeImageIds || !volume)) {
|
||||
if (!isParametricMap && !isSeg && (!volumeImageIds || !volume)) {
|
||||
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
|
||||
|
||||
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
||||
@ -303,6 +285,9 @@ class CornerstoneCacheService {
|
||||
});
|
||||
|
||||
this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds);
|
||||
|
||||
// Add imageIds to the displaySet for volumes
|
||||
displaySet.imageIds = volumeImageIds;
|
||||
}
|
||||
|
||||
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[] {
|
||||
return dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
}
|
||||
|
||||
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
|
||||
if (displaySet.imageIds) {
|
||||
return displaySet.imageIds;
|
||||
}
|
||||
|
||||
const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
||||
|
||||
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 { pubSubServiceInterface, Types } from '@ohif/core';
|
||||
import createHydrateSegmentationSynchronizer from './createHydrateSegmentationSynchronizer';
|
||||
|
||||
const EVENTS = {
|
||||
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
||||
@ -27,6 +28,7 @@ const VOI = 'voi';
|
||||
const ZOOMPAN = 'zoompan';
|
||||
const STACKIMAGE = 'stackimage';
|
||||
const IMAGE_SLICE = 'imageslice';
|
||||
const HYDRATE_SEG = 'hydrateseg';
|
||||
|
||||
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
|
||||
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
|
||||
@ -51,6 +53,7 @@ export default class SyncGroupService {
|
||||
// handles both stack and volume viewports
|
||||
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
|
||||
[IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer,
|
||||
[HYDRATE_SEG]: createHydrateSegmentationSynchronizer,
|
||||
};
|
||||
|
||||
synchronizersByType: { [key: string]: Synchronizer[] } = {};
|
||||
@ -66,18 +69,18 @@ export default class SyncGroupService {
|
||||
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
|
||||
// Initialize if not already done
|
||||
this.synchronizersByType[type] = this.synchronizersByType[type] || [];
|
||||
|
||||
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
|
||||
|
||||
if (syncCreator) {
|
||||
const synchronizer = syncCreator(id, options);
|
||||
// Pass the servicesManager along with other parameters
|
||||
const synchronizer = syncCreator(id, { ...options, servicesManager: this.servicesManager });
|
||||
|
||||
if (synchronizer) {
|
||||
this.synchronizersByType[type].push(synchronizer);
|
||||
return synchronizer;
|
||||
}
|
||||
} 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;
|
||||
component: ReactNode;
|
||||
location: ViewportActionCornersLocations;
|
||||
indexPriority: number;
|
||||
indexPriority?: number;
|
||||
};
|
||||
|
||||
class ViewportActionCornersService extends PubSubService {
|
||||
@ -33,21 +33,21 @@ class ViewportActionCornersService extends PubSubService {
|
||||
|
||||
public setServiceImplementation({
|
||||
getState: getStateImplementation,
|
||||
setComponent: setComponentImplementation,
|
||||
setComponents: setComponentsImplementation,
|
||||
clear: clearImplementation,
|
||||
addComponent: addComponentImplementation,
|
||||
addComponents: addComponentsImplementation,
|
||||
clear: clearComponentsImplementation,
|
||||
}): void {
|
||||
if (getStateImplementation) {
|
||||
this.serviceImplementation._getState = getStateImplementation;
|
||||
}
|
||||
if (setComponentImplementation) {
|
||||
this.serviceImplementation._setComponent = setComponentImplementation;
|
||||
if (addComponentImplementation) {
|
||||
this.serviceImplementation._addComponent = addComponentImplementation;
|
||||
}
|
||||
if (setComponentsImplementation) {
|
||||
this.serviceImplementation._setComponents = setComponentsImplementation;
|
||||
if (addComponentsImplementation) {
|
||||
this.serviceImplementation._addComponents = addComponentsImplementation;
|
||||
}
|
||||
if (clearImplementation) {
|
||||
this.serviceImplementation._clear = clearImplementation;
|
||||
if (clearComponentsImplementation) {
|
||||
this.serviceImplementation._clear = clearComponentsImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,12 +55,12 @@ class ViewportActionCornersService extends PubSubService {
|
||||
return this.serviceImplementation._getState();
|
||||
}
|
||||
|
||||
public setComponent(component: ActionComponentInfo) {
|
||||
this.serviceImplementation._setComponent(component);
|
||||
public addComponent(component: ActionComponentInfo) {
|
||||
this.serviceImplementation._addComponent(component);
|
||||
}
|
||||
|
||||
public setComponents(components: Array<ActionComponentInfo>) {
|
||||
this.serviceImplementation._setComponents(components);
|
||||
public addComponents(components: Array<ActionComponentInfo>) {
|
||||
this.serviceImplementation._addComponents(components);
|
||||
}
|
||||
|
||||
public clear(viewportId: string) {
|
||||
|
||||
@ -18,9 +18,20 @@ import { IViewportService } from './IViewportService';
|
||||
import { RENDERING_ENGINE_ID } from './constants';
|
||||
import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport';
|
||||
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 { 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 = {
|
||||
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 viewportInfo - Contains a view reference for immediate application
|
||||
*/
|
||||
public setPresentations(
|
||||
viewportId: string,
|
||||
presentations?: Presentations,
|
||||
viewportInfo?: ViewportInfo
|
||||
): void {
|
||||
public setPresentations(viewportId: string, presentations: Presentations): void {
|
||||
const viewport = this.getCornerstoneViewport(viewportId) as
|
||||
| Types.IStackViewport
|
||||
| Types.IVolumeViewport;
|
||||
@ -189,83 +196,113 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
return;
|
||||
}
|
||||
|
||||
const { lutPresentation, positionPresentation } = presentations;
|
||||
if (lutPresentation) {
|
||||
const { presentation } = lutPresentation;
|
||||
if (viewport instanceof BaseVolumeViewport) {
|
||||
if (presentation instanceof Map) {
|
||||
presentation.forEach((properties, volumeId) => {
|
||||
viewport.setProperties(properties, volumeId);
|
||||
});
|
||||
} else {
|
||||
viewport.setProperties(presentation);
|
||||
}
|
||||
} else {
|
||||
viewport.setProperties(presentation);
|
||||
}
|
||||
const { lutPresentation, positionPresentation, segmentationPresentation } = presentations;
|
||||
|
||||
// Always set the segmentation presentation first, since there might be some
|
||||
// lutpresentation states that need to be set on the segmentation
|
||||
// Todo: i think we should even await this
|
||||
this._setSegmentationPresentation(viewport, segmentationPresentation);
|
||||
|
||||
this._setLutPresentation(viewport, lutPresentation);
|
||||
this._setPositionPresentation(viewport, { ...positionPresentation, viewportId });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
if (viewRef) {
|
||||
viewport.setViewReference(viewRef);
|
||||
const { lutPresentationId, positionPresentationId, segmentationPresentationId } =
|
||||
presentationIds;
|
||||
|
||||
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.
|
||||
* @param viewportId The ID of the viewport.
|
||||
* @returns The position presentation object containing various properties
|
||||
* such as ID, viewport type, initial image index, view plane normal, view up, zoom, and pan.
|
||||
* Retrieves the presentations for a given viewport.
|
||||
* @param viewportId - The ID of the viewport.
|
||||
* @returns The presentations for the viewport.
|
||||
*/
|
||||
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);
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const presentationIds = viewportInfo.getPresentationIds();
|
||||
|
||||
if (!presentationIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { positionPresentationId } = presentationIds;
|
||||
return viewportInfo.getPresentationIds();
|
||||
}
|
||||
|
||||
private _getPositionPresentation(viewportId: string): PositionPresentation {
|
||||
const csViewport = this.getCornerstoneViewport(viewportId);
|
||||
if (!csViewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportInfo = this.viewportsById.get(viewportId);
|
||||
|
||||
return {
|
||||
id: positionPresentationId,
|
||||
viewportType: viewportInfo.getViewportType(),
|
||||
viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(),
|
||||
position: csViewport.getViewPresentation({ pan: true, zoom: true }),
|
||||
viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }),
|
||||
viewportId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
private _getLutPresentation(viewportId: string): LutPresentation {
|
||||
const csViewport = this.getCornerstoneViewport(viewportId) as
|
||||
| Types.IStackViewport
|
||||
| Types.IVolumeViewport;
|
||||
@ -275,116 +312,39 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
|
||||
const cleanProperties = properties => {
|
||||
if (properties.isComputedVOI) {
|
||||
delete properties.voiRange;
|
||||
delete properties.VOILUTFunction;
|
||||
if (properties?.isComputedVOI) {
|
||||
delete properties?.voiRange;
|
||||
delete properties?.VOILUTFunction;
|
||||
}
|
||||
return properties;
|
||||
};
|
||||
|
||||
const presentation =
|
||||
const properties =
|
||||
csViewport instanceof BaseVolumeViewport
|
||||
? new Map()
|
||||
: cleanProperties(csViewport.getProperties());
|
||||
|
||||
if (presentation instanceof Map) {
|
||||
csViewport.getActors().forEach(({ uid: volumeId }) => {
|
||||
const properties = cleanProperties(csViewport.getProperties(volumeId));
|
||||
presentation.set(volumeId, properties);
|
||||
if (properties instanceof Map) {
|
||||
const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds();
|
||||
volumeIds?.forEach(volumeId => {
|
||||
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);
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positionPresentation = this.getPositionPresentation(viewportId);
|
||||
const lutPresentation = this.getLutPresentation(viewportId);
|
||||
|
||||
return {
|
||||
positionPresentation,
|
||||
lutPresentation,
|
||||
viewportType: viewportInfo.getViewportType(),
|
||||
properties,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the presentation state for a given viewport inside the
|
||||
* 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;
|
||||
}
|
||||
private _getSegmentationPresentation(viewportId: string): SegmentationPresentation {
|
||||
const { segmentationService } = this.servicesManager.services;
|
||||
|
||||
const { stateSyncService, syncGroupService } = this.servicesManager.services;
|
||||
|
||||
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);
|
||||
const presentation = segmentationService.getPresentation(viewportId);
|
||||
return presentation;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -423,6 +383,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
// and we would lose the presentation.
|
||||
this.storePresentation({ viewportId: viewportInfo.getViewportId() });
|
||||
|
||||
// Todo: i don't like this here, move it
|
||||
this.servicesManager.services.segmentationService.clearSegmentationRepresentations(
|
||||
viewportInfo.getViewportId()
|
||||
);
|
||||
|
||||
if (!viewportInfo) {
|
||||
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
|
||||
const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
|
||||
// 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
|
||||
// for initial view, whereas the one in viewportData is a requirement based on the
|
||||
// viewport options, because the one in the viewportData is a requirement based on the
|
||||
// type of data being displayed.
|
||||
const viewportOptions = viewportInfo.setPublicViewportOptions(
|
||||
publicViewportOptions,
|
||||
@ -647,13 +611,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
if (element !== viewport.element) {
|
||||
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 });
|
||||
this.setPresentations(viewport.id, presentations, viewportInfo);
|
||||
if (displayArea) {
|
||||
@ -702,7 +674,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
const lastSliceIndex = numberOfSlices - 1;
|
||||
|
||||
if (imageIndex !== undefined) {
|
||||
return csToolsUtils.clip(imageIndex, 0, lastSliceIndex);
|
||||
return csUtils.clip(imageIndex, 0, lastSliceIndex);
|
||||
}
|
||||
|
||||
if (preset === JumpPresets.First) {
|
||||
@ -764,7 +736,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
|
||||
const displaySetOptions = displaySetOptionsArray[index];
|
||||
const { volumeId } = volume;
|
||||
|
||||
volumeInputArray.push({
|
||||
imageIds,
|
||||
volumeId,
|
||||
@ -775,8 +746,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
|
||||
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 (hangingProtocolService.getShouldPerformCustomImageLoad()) {
|
||||
// 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 => {
|
||||
if (!volume.loadStatus.loading) {
|
||||
if (!volume.loadStatus?.loading && volume.load instanceof Function) {
|
||||
volume.load();
|
||||
}
|
||||
});
|
||||
@ -798,8 +768,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
|
||||
public async setVolumesForViewport(viewport, volumeInputArray, presentations) {
|
||||
const { displaySetService, toolGroupService, viewportGridService } =
|
||||
this.servicesManager.services;
|
||||
const { displaySetService, viewportGridService } = this.servicesManager.services;
|
||||
|
||||
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||
@ -811,7 +780,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
const { volumeId } = volumeInput;
|
||||
const displaySetOption = displaySetOptions[index];
|
||||
const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
|
||||
const properties = {};
|
||||
const properties = {} as ViewportProperties;
|
||||
|
||||
if (voi && (voi.windowWidth || voi.windowCenter)) {
|
||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||
@ -836,22 +805,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
return { properties, volumeId };
|
||||
});
|
||||
|
||||
// For SEG and RT viewports
|
||||
this._processExtraDisplaySetsForViewport(viewport);
|
||||
|
||||
await viewport.setVolumes(volumeInputArray);
|
||||
|
||||
volumesProperties.forEach(({ properties, volumeId }) => {
|
||||
viewport.setProperties(properties, volumeId);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
if (imageIndex !== undefined) {
|
||||
csToolsUtils.jumpToSlice(viewport.element, {
|
||||
csUtils.jumpToSlice(viewport.element, {
|
||||
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;
|
||||
|
||||
// load any secondary displaySets
|
||||
@ -877,104 +847,41 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality)
|
||||
);
|
||||
|
||||
if (segOrRTSOverlayDisplaySet) {
|
||||
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
|
||||
} else {
|
||||
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
|
||||
// and we can look into hydrated segmentations to check if any of them are
|
||||
// associated with the primary displaySet
|
||||
|
||||
// get segmentations only returns the hydrated segmentations
|
||||
this._addSegmentationRepresentationToToolGroupIfNecessary(displaySetInstanceUIDs, viewport);
|
||||
// if it is only the overlay displaySet, then we need to get the reference
|
||||
// displaySet imageIds and set them as the imageIds for the viewport,
|
||||
// here we can do some logic if the reference is missing
|
||||
// then find the most similar match of displaySet instead
|
||||
if (!segOrRTSOverlayDisplaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const referenceDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
const imageIds = referenceDisplaySet.images.map(image => image.imageId);
|
||||
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
|
||||
return { imageIds };
|
||||
}
|
||||
|
||||
private _addSegmentationRepresentationToToolGroupIfNecessary(
|
||||
displaySetInstanceUIDs: string[],
|
||||
viewport: any
|
||||
private addOverlayRepresentationForDisplaySet(
|
||||
displaySet: OhifTypes.DisplaySet,
|
||||
viewport: Types.IViewport
|
||||
) {
|
||||
const { segmentationService, toolGroupService } = 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 { segmentationService } = this.servicesManager.services;
|
||||
const segmentationId = displaySet.displaySetInstanceUID;
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
||||
|
||||
const representationType =
|
||||
referencedVolumeId && cache.getVolume(referencedVolumeId) !== undefined
|
||||
displaySet.Modality === 'SEG'
|
||||
? csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
: csToolsEnums.SegmentationRepresentations.Contour;
|
||||
|
||||
segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroup.id,
|
||||
segmentationService.addSegmentationRepresentation(viewport.id, {
|
||||
segmentationId,
|
||||
false,
|
||||
representationType
|
||||
);
|
||||
type: 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
|
||||
@ -1032,7 +939,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
|
||||
return this._setOtherViewport(
|
||||
viewport as Types.IViewport,
|
||||
viewport,
|
||||
viewportData as StackViewportData,
|
||||
viewportInfo,
|
||||
presentations
|
||||
@ -1063,11 +970,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
// calculate the slab thickness based on the volume dimensions
|
||||
const imageVolume = cache.getVolume(volumeId);
|
||||
|
||||
const { dimensions } = imageVolume;
|
||||
const { dimensions, spacing } = imageVolume;
|
||||
const slabThickness = Math.sqrt(
|
||||
dimensions[0] * dimensions[0] +
|
||||
dimensions[1] * dimensions[1] +
|
||||
dimensions[2] * dimensions[2]
|
||||
Math.pow(dimensions[0] * spacing[0], 2) +
|
||||
Math.pow(dimensions[1] * spacing[1], 2) +
|
||||
Math.pow(dimensions[2] * spacing[2], 2)
|
||||
);
|
||||
|
||||
return slabThickness;
|
||||
@ -1128,9 +1035,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
const viewports = this.getRenderingEngine().getViewports();
|
||||
|
||||
// Store the current position presentations for each viewport.
|
||||
viewports.forEach(({ id }) => {
|
||||
const presentation = this.getPositionPresentation(id);
|
||||
this.beforeResizePositionPresentations.set(id, presentation);
|
||||
viewports.forEach(({ id: viewportId }) => {
|
||||
const presentation = this._getPositionPresentation(viewportId);
|
||||
this.beforeResizePositionPresentations.set(viewportId, presentation);
|
||||
});
|
||||
|
||||
// Resize the rendering engine and render.
|
||||
@ -1159,6 +1066,65 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
this.gridResizeTimeOut = null;
|
||||
}, 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;
|
||||
|
||||
@ -5,7 +5,6 @@ import {
|
||||
VolumeViewport,
|
||||
utilities,
|
||||
} from '@cornerstonejs/core';
|
||||
import { Types as CoreTypes } from '@ohif/core';
|
||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
|
||||
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
||||
@ -25,7 +24,7 @@ export type ViewportOptions = {
|
||||
toolGroupId: string;
|
||||
viewportId: string;
|
||||
// Presentation ID to store/load presentation state from
|
||||
presentationIds?: CoreTypes.PresentationIds;
|
||||
presentationIds?: AppTypes.PresentationIds;
|
||||
orientation?: Enums.OrientationAxis;
|
||||
background?: Types.Point3;
|
||||
displayArea?: Types.DisplayArea;
|
||||
@ -46,7 +45,7 @@ export type PublicViewportOptions = {
|
||||
id?: string;
|
||||
viewportType?: string;
|
||||
toolGroupId?: string;
|
||||
presentationIds?: CoreTypes.PresentationIds;
|
||||
presentationIds?: string[];
|
||||
viewportId?: string;
|
||||
orientation?: Enums.OrientationAxis;
|
||||
background?: Types.Point3;
|
||||
@ -283,7 +282,7 @@ class ViewportInfo {
|
||||
return this.viewportOptions;
|
||||
}
|
||||
|
||||
public getPresentationIds(): CoreTypes.PresentationIds {
|
||||
public getPresentationIds(): AppTypes.PresentationIds | null {
|
||||
const { presentationIds } = this.viewportOptions;
|
||||
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 { EVENTS, getRenderingEngine, Types } from '@cornerstonejs/core';
|
||||
import { SynchronizerManager, Synchronizer } from '@cornerstonejs/tools';
|
||||
import { EVENTS, getRenderingEngine, type Types, utilities } from '@cornerstonejs/core';
|
||||
|
||||
const frameViewSyncCallback = (
|
||||
synchronizerInstance: Synchronizer,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { LengthTool, utilities } from '@cornerstonejs/tools';
|
||||
import callInputDialog from '../utils/callInputDialog';
|
||||
import { callInputDialog } from '@ohif/extension-default';
|
||||
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
|
||||
|
||||
const { calibrateImageSpacing } = utilities;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { guid, b64toBlob } from '@ohif/core/src/utils';
|
||||
import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider';
|
||||
|
||||
@ -9,6 +9,13 @@ import ColorbarServiceType from '../services/ColorbarService';
|
||||
import * as cornerstone from '@cornerstonejs/core';
|
||||
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 {
|
||||
namespace AppTypes {
|
||||
export type CornerstoneCacheService = CornerstoneCacheServiceType;
|
||||
@ -18,6 +25,7 @@ declare global {
|
||||
export type ToolGroupService = ToolGroupServiceType;
|
||||
export type ViewportActionCornersService = ViewportActionCornersServiceType;
|
||||
export type ColorbarService = ColorbarServiceType;
|
||||
|
||||
export interface Services {
|
||||
cornerstoneViewportService?: CornerstoneViewportServiceType;
|
||||
toolGroupService?: ToolGroupServiceType;
|
||||
@ -28,6 +36,19 @@ declare global {
|
||||
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 {
|
||||
services?: Services;
|
||||
cornerstone?: typeof cornerstone;
|
||||
|
||||
@ -1,16 +1,24 @@
|
||||
import type { Types } from '@cornerstonejs/core';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
/**
|
||||
* Represents a position presentation in a viewport. This is basically
|
||||
* viewport specific camera position and zoom, and not the display set
|
||||
*/
|
||||
export type PositionPresentation = {
|
||||
id: string;
|
||||
viewportType: string;
|
||||
// The view reference has the basic information as to what image orientation/slice is shown
|
||||
viewReference: Types.ViewReference;
|
||||
// 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
|
||||
*/
|
||||
export interface LutPresentation {
|
||||
id: 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.
|
||||
*/
|
||||
type Presentation = PositionPresentation | LutPresentation;
|
||||
type Presentation = PositionPresentation | LutPresentation | SegmentationPresentation;
|
||||
|
||||
/**
|
||||
* Viewport presentations object that can contain a positionPresentation
|
||||
@ -37,6 +70,7 @@ type Presentation = PositionPresentation | LutPresentation;
|
||||
export type Presentations = {
|
||||
positionPresentation?: PositionPresentation;
|
||||
lutPresentation?: LutPresentation;
|
||||
segmentationPresentation?: SegmentationPresentation;
|
||||
};
|
||||
|
||||
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,
|
||||
} from '@cornerstonejs/core';
|
||||
import { ToolGroupManager } from '@cornerstonejs/tools';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ViewportDownloadForm } from '@ohif/ui';
|
||||
|
||||
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
|
||||
@ -22,7 +21,7 @@ const CornerstoneViewportDownloadForm = ({
|
||||
onClose,
|
||||
activeViewportId: activeViewportIdProp,
|
||||
cornerstoneViewportService,
|
||||
}) => {
|
||||
}: withAppTypes) => {
|
||||
const enabledElement = OHIFgetEnabledElement(activeViewportIdProp);
|
||||
const activeViewportElement = enabledElement?.element;
|
||||
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
||||
@ -246,9 +245,4 @@ const CornerstoneViewportDownloadForm = ({
|
||||
);
|
||||
};
|
||||
|
||||
CornerstoneViewportDownloadForm.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
activeViewportId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CornerstoneViewportDownloadForm;
|
||||
|
||||
@ -161,7 +161,7 @@ class DicomLoaderService {
|
||||
getDicomDataMethod = fetchIt.bind(this, imageId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported image type: ${loaderType} for imageId: ${imageId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
return getDicomDataMethod();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ export default function getCornerstoneViewportType(
|
||||
return Enums.ViewportType.VIDEO;
|
||||
}
|
||||
if (lowerViewportType === WHOLESLIDE) {
|
||||
return Enums.ViewportType.WholeSlide;
|
||||
return Enums.ViewportType.WHOLE_SLIDE;
|
||||
}
|
||||
|
||||
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