diff --git a/extensions/cornerstone-3d/package.json b/extensions/cornerstone-3d/package.json index 803466ad6d7..8cd8b4bfe6c 100644 --- a/extensions/cornerstone-3d/package.json +++ b/extensions/cornerstone-3d/package.json @@ -30,7 +30,7 @@ "@ohif/core": "^3.0.0", "@ohif/ui": "^2.0.0", "cornerstone-wado-image-loader": "^4.1.2", - "dcmjs": "0.22.0", + "dcmjs": "^0.22.1", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", @@ -45,9 +45,9 @@ "@babel/runtime": "7.17.9", "lodash.merge": "^4.6.2", "lodash.debounce": "4.0.8", - "@cornerstonejs/core": "^0.8.1", - "@cornerstonejs/tools": "^0.15.0", - "@cornerstonejs/streaming-image-volume-loader": "^0.2.26", + "@cornerstonejs/core": "^0.10.2", + "@cornerstonejs/tools": "^0.17.2", + "@cornerstonejs/streaming-image-volume-loader": "^0.3.2", "shader-loader": "^1.3.1", "@kitware/vtk.js": "^24.10.0", "dom-to-image": "^2.6.0", diff --git a/extensions/cornerstone-3d/src/OHIFCornerstone3DViewport.tsx b/extensions/cornerstone-3d/src/OHIFCornerstone3DViewport.tsx deleted file mode 100644 index 6f7c1ef8826..00000000000 --- a/extensions/cornerstone-3d/src/OHIFCornerstone3DViewport.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import React, { useEffect, useRef, useCallback } from 'react'; -import { utilities } from '@cornerstonejs/tools'; -import ReactResizeDetector from 'react-resize-detector'; -import { useViewportGrid } from '@ohif/ui'; - -function areEqual(prevProps, nextProps) { - if (nextProps.needsRerendering) { - return false; - } - - if (prevProps.displaySets.length !== nextProps.displaySets.length) { - return false; - } - - // Todo: handle fusion - // Todo: handle orientation - const prevDisplaySets = prevProps.displaySets[0]; - const nextDisplaySets = nextProps.displaySets[0]; - - if (prevDisplaySets && nextDisplaySets) { - return ( - prevDisplaySets.displaySetInstanceUID === - nextDisplaySets.displaySetInstanceUID && - prevDisplaySets.images.length === nextDisplaySets.images.length && - prevDisplaySets.images.every( - (prevImage, index) => - prevImage.imageId === nextDisplaySets.images[index].imageId - ) - ); - } - return false; -} - -// 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 => { - const { - displaySets, - viewportIndex, - dataSource, - viewportOptions, - displaySetOptions, - servicesManager, - } = props; - - const [_, viewportGridService] = useViewportGrid(); - - const elementRef = useRef(); - const { - ViewportService, - MeasurementService, - DisplaySetService, - } = servicesManager.services; - - // useCallback for onResize - const onResize = useCallback( - props => { - if (elementRef.current) { - const element = elementRef.current; - ViewportService.resize(); - } - }, - [elementRef] - ); - - // disable the element upon unmounting - useEffect(() => { - // setElementRef(targetRef.current); - ViewportService.enableElement(viewportIndex, elementRef.current); - return () => { - ViewportService.disableElement(viewportIndex); - }; - }, []); - - // Todo: Use stackManager to handle the stack creation and imageId get, problem - // would be what to do with the volumes which needs different schema for loading: streaming-wadors - // Stack manager shouldn't care about the type of volume. Maybe add a postImageId creation callback? - // Maybe we should need a volumeManager later on. - useEffect(() => { - ViewportService.setViewportDisplaySets( - viewportIndex, - displaySets, - viewportOptions, - displaySetOptions, - dataSource - ); - }, [ - viewportIndex, - viewportOptions, - displaySetOptions, - displaySets, - dataSource, - ViewportService, - ]); - - /** - * 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(() => { - const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents( - MeasurementService, - DisplaySetService, - elementRef, - viewportIndex, - displaySets, - viewportGridService - ); - - _checkForCachedJumpToMeasurementEvents( - MeasurementService, - DisplaySetService, - elementRef, - viewportIndex, - displaySets, - viewportGridService - ); - - return () => { - unsubscribeFromJumpToMeasurementEvents(); - }; - }, [ - displaySets, - elementRef, - viewportIndex, - viewportGridService, - MeasurementService, - DisplaySetService, - ]); - - return ( - - -
e.preventDefault()} - onMouseDown={e => e.preventDefault()} - ref={elementRef} - >
-
- ); -}, areEqual); - -function _subscribeToJumpToMeasurementEvents( - MeasurementService, - DisplaySetService, - elementRef, - viewportIndex, - displaySets, - viewportGridService -) { - const displaysUIDs = displaySets.map( - displaySet => displaySet.displaySetInstanceUID - ); - const { unsubscribe } = MeasurementService.subscribe( - MeasurementService.EVENTS.JUMP_TO_MEASUREMENT, - ({ measurement }) => { - if (!measurement) return; - - // Jump the the measurement if the viewport contains the displaySetUID (fusion) - if (displaysUIDs.includes(measurement.displaySetInstanceUID)) { - _jumpToMeasurement( - measurement, - elementRef, - viewportIndex, - MeasurementService, - DisplaySetService, - viewportGridService - ); - } - } - ); - - return unsubscribe; -} - -// Check if there is a queued jumpToMeasurement event -function _checkForCachedJumpToMeasurementEvents( - MeasurementService, - DisplaySetService, - elementRef, - viewportIndex, - displaySets, - viewportGridService -) { - const displaysUIDs = displaySets.map( - displaySet => displaySet.displaySetInstanceUID - ); - - const measurementIdToJumpTo = MeasurementService.getJumpToMeasurement( - viewportIndex - ); - - if (measurementIdToJumpTo && elementRef) { - // Jump to measurement if the measurement exists - const measurement = MeasurementService.getMeasurement( - measurementIdToJumpTo - ); - - if (displaysUIDs.includes(measurement.displaySetInstanceUID)) { - _jumpToMeasurement( - measurement, - elementRef, - viewportIndex, - MeasurementService, - DisplaySetService, - viewportGridService - ); - } - } -} - -function _jumpToMeasurement( - measurement, - targetElementRef, - viewportIndex, - MeasurementService, - DisplaySetService, - viewportGridService -) { - const targetElement = targetElementRef.current; - const { displaySetInstanceUID, SOPInstanceUID } = measurement; - - if (!SOPInstanceUID) { - console.warn('cannot jump in a non-acquisition plane measurements yet'); - } - - const referencedDisplaySet = DisplaySetService.getDisplaySetByUID( - displaySetInstanceUID - ); - - const imageIdIndex = referencedDisplaySet.images.findIndex( - i => i.SOPInstanceUID === SOPInstanceUID - ); - - // Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager - // to set it properly - // setCornerstoneMeasurementActive(measurement); - - viewportGridService.setActiveViewportIndex(viewportIndex); - - if (targetElement !== null) { - const metadata = { - ...measurement.metadata, - imageIdIndex, - }; - utilities.jumpToSlice(targetElement, metadata); - - // Jump to measurement consumed, remove. - MeasurementService.removeJumpToMeasurement(viewportIndex); - } -} - -// Component displayName -OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport'; - -export default OHIFCornerstoneViewport; diff --git a/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx b/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx index eb3e6ea9857..6620b1b070a 100644 --- a/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx +++ b/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx @@ -437,11 +437,9 @@ function _jumpToMeasurement( viewportGridService.setActiveViewportIndex(viewportIndex); if (targetElement !== null) { - const metadata = { - ...measurement.metadata, - imageIdIndex, - }; - cs3DTools.utilities.jumpToSlice(targetElement, metadata); + cs3DTools.utilities.jumpToSlice(targetElement, { + imageIndex: imageIdIndex, + }); cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid); // Jump to measurement consumed, remove. diff --git a/extensions/cornerstone-3d/src/commandsModule.js b/extensions/cornerstone-3d/src/commandsModule.js index f3402af9dc8..15e23569a81 100644 --- a/extensions/cornerstone-3d/src/commandsModule.js +++ b/extensions/cornerstone-3d/src/commandsModule.js @@ -6,6 +6,7 @@ import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownload import { Enums } from '@cornerstonejs/tools'; import { getEnabledElement } from './state'; +import callInputDialog from './utils/callInputDialog'; const commandsModule = ({ servicesManager }) => { const { @@ -13,6 +14,7 @@ const commandsModule = ({ servicesManager }) => { ToolGroupService, CineService, ToolBarService, + UIDialogService, } = servicesManager.services; function _getActiveViewportEnabledElement() { @@ -26,6 +28,9 @@ const commandsModule = ({ servicesManager }) => { getActiveViewportEnabledElement: () => { return _getActiveViewportEnabledElement(); }, + arrowTextCallback: ({ callback, data }) => { + callInputDialog(UIDialogService, data, callback); + }, toggleCine: () => { const { viewports } = ViewportGridService.getState(); const { isCineEnabled } = CineService.getState(); @@ -35,11 +40,10 @@ const commandsModule = ({ servicesManager }) => { CineService.setCine({ id: index, isPlaying: false }) ); }, - setWindowLevel({ windowLevel, toolGroupId }) { - const { window: windowWidth, level: windowCenter } = windowLevel; + setWindowLevel({ window, level, toolGroupId }) { // convert to numbers - const windowWidthNum = Number(windowWidth); - const windowCenterNum = Number(windowCenter); + const windowWidthNum = Number(window); + const windowCenterNum = Number(level); const { viewportId } = _getActiveViewportEnabledElement(); const viewportToolGroupId = ToolGroupService.getToolGroupForViewport( @@ -268,12 +272,7 @@ const commandsModule = ({ servicesManager }) => { const { viewport } = enabledElement; - let options = {}; - if (viewport instanceof cornerstone3D.StackViewport) { - options = { direction }; - } else { - throw new Error('scroll: volume viewport is not supported yet'); - } + const options = { delta: direction }; cornerstone3DTools.utilities.stackScrollTool.scrollThroughStack( viewport, @@ -358,6 +357,11 @@ const commandsModule = ({ servicesManager }) => { storeContexts: [], options: {}, }, + arrowTextCallback: { + commandFn: actions.arrowTextCallback, + storeContexts: [], + options: {}, + }, }; return { diff --git a/extensions/cornerstone-3d/src/init.js b/extensions/cornerstone-3d/src/init.js index b4d0e6d084c..bea513adc16 100644 --- a/extensions/cornerstone-3d/src/init.js +++ b/extensions/cornerstone-3d/src/init.js @@ -7,6 +7,7 @@ import { eventTarget, EVENTS, imageLoadPoolManager, + Settings, } from '@cornerstonejs/core'; import { Enums, utilities } from '@cornerstonejs/tools'; @@ -15,7 +16,7 @@ import Cornerstone3DViewportService from './services/ViewportService/Cornerstone import initCornerstoneTools from './initCornerstoneTools'; import { connectToolsToMeasurementService } from './initMeasurementService'; -import callInputDialog from './callInputDialog'; +import callInputDialog from './utils/callInputDialog'; import initCineService from './initCineService'; const cs3DToolsEvents = Enums.Events; @@ -36,6 +37,10 @@ export default async function init({ await cs3DInit(); initCornerstoneTools(); + // Don't use cursors in viewports + // Todo: this should come from extension/app configuration + Settings.getRuntimeSettings().set('useCursors', false); + const { UserAuthenticationService, ToolGroupService, diff --git a/extensions/cornerstone-3d/src/initCornerstoneTools.js b/extensions/cornerstone-3d/src/initCornerstoneTools.js index d8ea721ba70..e0bb399f641 100644 --- a/extensions/cornerstone-3d/src/initCornerstoneTools.js +++ b/extensions/cornerstone-3d/src/initCornerstoneTools.js @@ -37,6 +37,20 @@ export default function initCornerstone3DTools(configuration = {}) { cornerstone3DTools.addTool(AngleTool); cornerstone3DTools.addTool(MagnifyTool); // cornerstone3DTools.addTool(CrosshairsTool); + + // Modify annotation tools to use dashed lines on SR + const annotationStyle = { + textBoxFontSize: '15px', + lineWidth: '1.5', + }; + + const defaultStyles = cornerstone3DTools.annotation.config.style.getDefaultToolStyles(); + cornerstone3DTools.annotation.config.style.setDefaultToolStyles({ + global: { + ...defaultStyles.global, + ...annotationStyle, + }, + }); } const toolNames = { diff --git a/extensions/cornerstone-3d/src/initMeasurementService.js b/extensions/cornerstone-3d/src/initMeasurementService.js index 893a1d84031..2a9b2aa2908 100644 --- a/extensions/cornerstone-3d/src/initMeasurementService.js +++ b/extensions/cornerstone-3d/src/initMeasurementService.js @@ -212,12 +212,12 @@ const connectMeasurementServiceToTools = ( return; } - const { id, label } = measurement; + const { uid, label } = measurement; - const sourceAnnotation = annotation.state.getAnnotation(id); + const sourceAnnotation = annotation.state.getAnnotation(uid); if (sourceAnnotation) { - sourceAnnotation.label = label; + sourceAnnotation.data.label = label; if (sourceAnnotation.hasOwnProperty('text')) { // Deal with the weird case of ArrowAnnotate. sourceAnnotation.text = label; diff --git a/extensions/cornerstone-3d/src/callInputDialog.tsx b/extensions/cornerstone-3d/src/utils/callInputDialog.tsx similarity index 100% rename from extensions/cornerstone-3d/src/callInputDialog.tsx rename to extensions/cornerstone-3d/src/utils/callInputDialog.tsx diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/ArrowAnnotate.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/ArrowAnnotate.js index 054bd094107..96eea1bddd5 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/ArrowAnnotate.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/ArrowAnnotate.js @@ -60,7 +60,7 @@ const Length = { DisplaySetService ); - const displayText = getDisplayText(mappedAnnotations); + const displayText = getDisplayText(mappedAnnotations, displaySet); return { uid: annotationUID, @@ -104,6 +104,7 @@ function getMappedAnnotations(annotation, DisplaySetService) { annotations.push({ SeriesInstanceUID, + SOPInstanceUID, SeriesNumber, text, }); @@ -111,7 +112,7 @@ function getMappedAnnotations(annotation, DisplaySetService) { return annotations; } -function getDisplayText(mappedAnnotations) { +function getDisplayText(mappedAnnotations, displaySet) { if (!mappedAnnotations) { return ''; } @@ -119,8 +120,22 @@ function getDisplayText(mappedAnnotations) { const displayText = []; // Area is the same for all series - const { SeriesNumber } = mappedAnnotations[0]; - displayText.push(`(S: ${SeriesNumber})`); + const { SeriesNumber, SOPInstanceUID } = mappedAnnotations[0]; + + const instance = displaySet.images.find( + image => image.SOPInstanceUID === SOPInstanceUID + ); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + displayText.push( + InstanceNumber + ? `(S: ${SeriesNumber} I: ${InstanceNumber})` + : `(S: ${SeriesNumber})` + ); return displayText; } diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js index 904338515b6..93913330751 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js @@ -55,7 +55,7 @@ const Bidirectional = { DisplaySetService ); - const displayText = getDisplayText(mappedAnnotations); + const displayText = getDisplayText(mappedAnnotations, displaySet); const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID); @@ -69,7 +69,7 @@ const Bidirectional = { referenceStudyUID: StudyInstanceUID, toolName: metadata.toolName, displaySetInstanceUID: displaySet.displaySetInstanceUID, - label: metadata.label, + label: data.label, displayText: displayText, data: data.cachedStats, type: getValueTypeFromToolType(toolName), @@ -94,10 +94,11 @@ function getMappedAnnotations(annotation, DisplaySetService) { let displaySet; + let SeriesInstanceUID, SOPInstanceUID; if (targetId.startsWith('imageId:')) { - const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( + ({ SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( referencedImageId - ); + )); displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, @@ -109,12 +110,13 @@ function getMappedAnnotations(annotation, DisplaySetService) { throw new Error('Not implemented'); } - const { SeriesNumber, SeriesInstanceUID } = displaySet; + const { SeriesNumber } = displaySet; const { length, width } = targetStats; const unit = 'mm'; annotations.push({ SeriesInstanceUID, + SOPInstanceUID, SeriesNumber, unit, length, @@ -163,7 +165,7 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { }; } -function getDisplayText(mappedAnnotations) { +function getDisplayText(mappedAnnotations, displaySet) { if (!mappedAnnotations || !mappedAnnotations.length) { return ''; } @@ -171,11 +173,24 @@ function getDisplayText(mappedAnnotations) { const displayText = []; // Area is the same for all series - const { length, width, SeriesNumber } = mappedAnnotations[0]; - const roundedLength = utils.roundNumber(length, 2); - const roundedWidth = utils.roundNumber(width, 2); + const { length, width, SeriesNumber, SOPInstanceUID } = mappedAnnotations[0]; + const roundedLength = utils.roundNumber(length, 1); + const roundedWidth = utils.roundNumber(width, 1); + + const instance = displaySet.images.find( + image => image.SOPInstanceUID === SOPInstanceUID + ); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } - displayText.push(`L: ${roundedLength} mm (S: ${SeriesNumber})`); + displayText.push( + InstanceNumber + ? `L: ${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})` + : `L: ${roundedLength} mm (S: ${SeriesNumber})` + ); displayText.push(`W: ${roundedWidth} mm`); return displayText; diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js index c1d1d57f81d..57cb1a5ca3a 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js @@ -56,7 +56,7 @@ const EllipticalROI = { DisplaySetService ); - const displayText = getDisplayText(mappedAnnotations); + const displayText = getDisplayText(mappedAnnotations, displaySet); const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID); @@ -70,7 +70,7 @@ const EllipticalROI = { referenceStudyUID: StudyInstanceUID, toolName: metadata.toolName, displaySetInstanceUID: displaySet.displaySetInstanceUID, - label: metadata.label, + label: data.label, displayText: displayText, data: data.cachedStats, type: getValueTypeFromToolType(toolName), @@ -95,10 +95,11 @@ function getMappedAnnotations(annotation, DisplaySetService) { let displaySet; + let SeriesInstanceUID, SOPInstanceUID; if (targetId.startsWith('imageId:')) { - const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( + ({ SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( referencedImageId - ); + )); displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, @@ -110,12 +111,13 @@ function getMappedAnnotations(annotation, DisplaySetService) { throw new Error('Not implemented'); } - const { SeriesNumber, SeriesInstanceUID } = displaySet; + const { SeriesNumber } = displaySet; const { mean, stdDev, max, area, Modality } = targetStats; const unit = getModalityUnit(Modality); annotations.push({ SeriesInstanceUID, + SOPInstanceUID, SeriesNumber, Modality, unit, @@ -177,7 +179,7 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { }; } -function getDisplayText(mappedAnnotations) { +function getDisplayText(mappedAnnotations, displaySet) { if (!mappedAnnotations || !mappedAnnotations.length) { return ''; } @@ -185,23 +187,31 @@ function getDisplayText(mappedAnnotations) { const displayText = []; // Area is the same for all series - const { area } = mappedAnnotations[0]; + const { area, SOPInstanceUID } = mappedAnnotations[0]; + + const instance = displaySet.images.find( + image => image.SOPInstanceUID === SOPInstanceUID + ); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + const roundedArea = utils.roundNumber(area, 2); - displayText.push(`Area: ${roundedArea} mm2`); + displayText.push(`${roundedArea} mm2`); + // Todo: we need a better UI for displaying all these information mappedAnnotations.forEach(mappedAnnotation => { - const { mean, unit, max, SeriesNumber } = mappedAnnotation; + const { unit, max, SeriesNumber } = mappedAnnotation; - if (mean && max) { - const roundedMean = utils.roundNumber(mean, 2); + if (max) { const roundedMax = utils.roundNumber(max, 2); - // const roundedStdDev = utils.roundNumber(stdDev, 2); displayText.push( - `S:${SeriesNumber} - max: ${roundedMax} ${unit}` - ); - displayText.push( - `S:${SeriesNumber} - mean: ${roundedMean} ${unit}` + InstanceNumber + ? `Max: ${roundedMax} ${unit} (S:${SeriesNumber} I:${InstanceNumber})` + : `Max: ${roundedMax} ${unit} (S:${SeriesNumber})` ); } }); diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js index 522f5362022..87a688fcc06 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js @@ -1,6 +1,6 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; -import { utils } from '@ohif/core'; +import { DisplaySetService, utils } from '@ohif/core'; const Length = { toAnnotation: measurement => {}, @@ -60,7 +60,7 @@ const Length = { DisplaySetService ); - const displayText = getDisplayText(mappedAnnotations); + const displayText = getDisplayText(mappedAnnotations, displaySet); const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID); @@ -74,7 +74,7 @@ const Length = { referenceStudyUID: StudyInstanceUID, toolName: metadata.toolName, displaySetInstanceUID: displaySet.displaySetInstanceUID, - label: metadata.label, + label: data.label, displayText: displayText, data: data.cachedStats, type: getValueTypeFromToolType(toolName), @@ -99,10 +99,11 @@ function getMappedAnnotations(annotation, DisplaySetService) { let displaySet; + let SeriesInstanceUID, SOPInstanceUID; if (targetId.startsWith('imageId:')) { - const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( + ({ SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( referencedImageId - ); + )); displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, @@ -114,12 +115,13 @@ function getMappedAnnotations(annotation, DisplaySetService) { throw new Error('Not implemented'); } - const { SeriesNumber, SeriesInstanceUID } = displaySet; + const { SeriesNumber } = displaySet; const { length } = targetStats; const unit = 'mm'; annotations.push({ SeriesInstanceUID, + SOPInstanceUID, SeriesNumber, unit, length, @@ -167,7 +169,7 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { }; } -function getDisplayText(mappedAnnotations) { +function getDisplayText(mappedAnnotations, displaySet) { if (!mappedAnnotations || !mappedAnnotations.length) { return ''; } @@ -175,9 +177,23 @@ function getDisplayText(mappedAnnotations) { const displayText = []; // Area is the same for all series - const { length, SeriesNumber } = mappedAnnotations[0]; + const { length, SeriesNumber, SOPInstanceUID } = mappedAnnotations[0]; + + const instance = displaySet.images.find( + image => image.SOPInstanceUID === SOPInstanceUID + ); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + const roundedLength = utils.roundNumber(length, 2); - displayText.push(`${roundedLength} mm (S: ${SeriesNumber})`); + displayText.push( + InstanceNumber + ? `${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})` + : `${roundedLength} mm (S: ${SeriesNumber})` + ); return displayText; } diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js index 6df0f3216f6..63b8b8395db 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js @@ -22,6 +22,7 @@ const measurementServiceMappingsFactory = ( ELLIPSE, RECTANGLE, BIDIRECTIONAL, + POINT, } = MeasurementService.VALUE_TYPES; // TODO -> I get why this was attempted, but its not nearly flexible enough. @@ -32,6 +33,7 @@ const measurementServiceMappingsFactory = ( EllipticalROI: ELLIPSE, RectangleROI: RECTANGLE, Bidirectional: BIDIRECTIONAL, + ArrowAnnotate: POINT, }; return TOOL_TYPE_TO_VALUE_TYPE[toolType]; diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index abeb6ed474a..9e1d4b06400 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -34,7 +34,7 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "@ohif/ui": "^2.0.0", - "dcmjs": "0.22.0", + "dcmjs": "^0.22.1", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", @@ -45,7 +45,7 @@ "dependencies": { "@babel/runtime": "7.16.3", "classnames": "^2.2.6", - "@cornerstonejs/core": "^0.8.1", - "@cornerstonejs/tools": "^0.15.0" + "@cornerstonejs/core": "^0.10.2", + "@cornerstonejs/tools": "^0.17.2" } } diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.js b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.js index 5a5b4c136a1..723256b38f7 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.js @@ -36,7 +36,10 @@ const CodeNameCodeSequenceValues = { const CodingSchemeDesignators = { SRT: 'SRT', - cornerstone3DTools1: Cornerstone3DCodeScheme.CodingSchemeDesignator, + CornerstoneCodeSchemes: [ + Cornerstone3DCodeScheme.CodingSchemeDesignator, + 'CST4', + ], }; const RELATIONSHIP_TYPE = { @@ -463,8 +466,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { if ( Finding && - Finding.ConceptCodeSequence.CodingSchemeDesignator === - CodingSchemeDesignators.cornerstone3DTools1 && + CodingSchemeDesignators.CornerstoneCodeSchemes.includes( + Finding.ConceptCodeSequence.CodingSchemeDesignator + ) && Finding.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText ) { @@ -478,8 +482,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { if (FindingSites.length) { const cornerstoneFreeTextFindingSite = FindingSites.find( FindingSite => - FindingSite.ConceptCodeSequence.CodingSchemeDesignator === - CodingSchemeDesignators.cornerstone3DTools1 && + CodingSchemeDesignators.CornerstoneCodeSchemes.includes( + Finding.ConceptCodeSequence.CodingSchemeDesignator + ) && FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText ); diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts index ffa3f896653..809abea6e4a 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -1,4 +1,4 @@ -import { Types } from '@cornerstonejs/core'; +import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core'; import { AnnotationTool, annotation, @@ -100,6 +100,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { const annotationUID = annotation.annotationUID; const { renderableData } = annotation.data.cachedStats; const { label, cachedStats } = annotation.data; + const { referencedImageId } = annotation.metadata; styleSpecifier.annotationUID = annotationUID; @@ -146,6 +147,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { viewport, renderableDataForGraphicType, annotationUID, + referencedImageId, options ); @@ -212,6 +214,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { viewport, renderableData, annotationUID, + referencedImageId, options ) { // Todo: this needs to use the drawPolyLine from cs3D since it is implemented @@ -247,6 +250,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { viewport, renderableData, annotationUID, + referencedImageId, options ) { let canvasCoordinates; @@ -270,12 +274,41 @@ export default class DICOMSRDisplayTool extends AnnotationTool { viewport, renderableData, annotationUID, + referencedImageId, options ) { - let canvasCoordinates; + const canvasCoordinates = []; renderableData.map((data, index) => { - canvasCoordinates = data.map(p => viewport.worldToCanvas(p)); + const point = data[0]; + // This gives us one point for arrow + canvasCoordinates.push(viewport.worldToCanvas(point)); + + // We get the other point for the arrow by using the image size + const imagePixelModule = metaData.get( + 'imagePixelModule', + referencedImageId + ); + + let xOffset = 10; + let yOffset = 10; + + if (imagePixelModule) { + const { columns, rows } = imagePixelModule; + xOffset = columns / 10; + yOffset = rows / 10; + } + + const imagePoint = csUtils.worldToImageCoords(referencedImageId, point); + const arrowEnd = csUtils.imageToWorldCoords(referencedImageId, [ + imagePoint[0] + xOffset, + imagePoint[1] + yOffset, + ]); + + canvasCoordinates.push(viewport.worldToCanvas(arrowEnd)); + const arrowUID = `${index}`; + + // Todo: handle drawing probe as probe, currently we are drawing it as an arrow drawing.drawArrow( svgDrawingHelper, annotationUID, @@ -297,6 +330,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { viewport, renderableData, annotationUID, + referencedImageId, options ) { let canvasCoordinates; diff --git a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts index 117e115f171..4146e2f7150 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts @@ -6,13 +6,14 @@ import SCOORD_TYPES from '../constants/scoordTypes'; const EPSILON = 1e-4; +const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; + export default function addMeasurement( measurement, imageId, displaySetInstanceUID ) { // TODO -> Render rotated ellipse . - const toolName = toolNames.DICOMSRDisplay; const measurementData = { @@ -30,7 +31,12 @@ export default function addMeasurement( } measurementData.renderableData[GraphicType].push( - _getRenderableData(GraphicType, GraphicData, imageId) + _getRenderableData( + GraphicType, + GraphicData, + imageId, + measurement.TrackingIdentifier + ) ); }); @@ -75,7 +81,14 @@ export default function addMeasurement( delete measurement.coords; } -function _getRenderableData(GraphicType, GraphicData, imageId) { +function _getRenderableData( + GraphicType, + GraphicData, + imageId, + TrackingIdentifier +) { + const [cornerstoneTag, toolName] = TrackingIdentifier.split(':'); + let renderableData: cornerstone3D.Types.Point3[]; switch (GraphicType) { @@ -92,6 +105,7 @@ function _getRenderableData(GraphicType, GraphicData, imageId) { renderableData.push(worldPos); } + break; case SCOORD_TYPES.CIRCLE: { const pointsWorld = []; diff --git a/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js index 14ee4377825..44af7d27604 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js +++ b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js @@ -2,6 +2,9 @@ import { adapters } from 'dcmjs'; const cornerstoneAdapters = adapters.Cornerstone3D; +const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; +const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG; + /** * Checks if the given `displaySet`can be rehydrated into the `MeasurementService`. * @@ -35,9 +38,18 @@ export default function isRehydratable(displaySet, mappings) { for (let i = 0; i < measurements.length; i++) { const TrackingIdentifier = measurements[i].TrackingIdentifier; - const hydratable = adapters.some(adapter => - adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier) - ); + const hydratable = adapters.some(adapter => { + let [cornerstoneTag, toolName] = TrackingIdentifier.split(':'); + if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) { + cornerstoneTag = CORNERSTONE_3D_TAG; + } + + const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`; + + return adapter.isValidCornerstoneTrackingIdentifier( + mappedTrackingIdentifier + ); + }); if (hydratable) { return true; diff --git a/extensions/default/package.json b/extensions/default/package.json index 78dd7aa16be..29365890f84 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -32,7 +32,7 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "@ohif/i18n": "^1.0.0", - "dcmjs": "0.22.0", + "dcmjs": "^0.22.1", "dicomweb-client": "^0.6.0", "prop-types": "^15.6.2", "react": "^17.0.2", diff --git a/extensions/default/src/Panels/PanelMeasurementTable.tsx b/extensions/default/src/Panels/PanelMeasurementTable.tsx index 73856e99a35..c076692ca80 100644 --- a/extensions/default/src/Panels/PanelMeasurementTable.tsx +++ b/extensions/default/src/Panels/PanelMeasurementTable.tsx @@ -199,12 +199,12 @@ function _getMappedMeasurements(MeasurementService) { } function _mapMeasurementToDisplay(measurement, index, types) { - const { displayText } = measurement; + const { displayText, uid, label, type } = measurement; return { - uid: measurement.uid, - label: measurement.label || '(empty)', - measurementType: measurement.type, + uid, + label: label || '(empty)', + measurementType: type, displayText: displayText || [], // TODO: handle one layer down isActive: false, // activeMeasurementItem === i + 1, diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx index 063f6ee7dbb..e9f240f0187 100644 --- a/extensions/default/src/ViewerLayout/index.tsx +++ b/extensions/default/src/ViewerLayout/index.tsx @@ -58,7 +58,7 @@ function Toolbar({ servicesManager }) { } // Also need... to filter list for splitButton, and set primary based on most recently clicked // Also need to kill the radioGroup button's magic logic - // Everything should be reactive off these props, so commands can inform ToolBarService + // Everything should be reactive off these props, so commands can inform ToolbarService // These can... Trigger toolbar events based on updates? // Then sync using useEffect, or simply modify the state here? @@ -162,11 +162,6 @@ function ViewerLayout({ const getPanelData = id => { const entry = extensionManager.getModuleEntry(id); - - if (!entry) { - throw new Error(`Could not find module entry for id: ${id}`); - } - // TODO, not sure why sidepanel content has to be JSX, and not a children prop? const content = entry.component; @@ -206,11 +201,11 @@ function ViewerLayout({
{/* LEFT SIDEPANELS */} - {leftPanelComponents.length && ( + {leftPanelComponents.length ? ( - )} + ) : null} {/* TOOLBAR + GRID */} -
+
- {rightPanelComponents.length && ( + {rightPanelComponents.length ? ( - )} + ) : null}
); diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js index 9a16e66dd38..468d14887ae 100644 --- a/extensions/default/src/getHangingProtocolModule.js +++ b/extensions/default/src/getHangingProtocolModule.js @@ -47,241 +47,6 @@ const defaultProtocol = { numberOfPriorsReferenced: -1, }; -const testProtocol = { - id: 'test', - locked: true, - hasUpdatedPriorsInformation: false, - name: 'Default', - createdDate: '2021-02-23T19:22:08.894Z', - modifiedDate: '2021-02-23T19:22:08.894Z', - availableTo: {}, - editableBy: {}, - protocolMatchingRules: [ - { - id: 'wauZK2QNEfDPwcAQo', - weight: 1, - attribute: 'StudyInstanceUID', - constraint: { - contains: { - value: '1.3.6.1.4.', - }, - }, - required: false, - }, - { - id: 'wauZK2QNEfDPwcAQo', - weight: 1, - attribute: 'StudyDescription', - constraint: { - contains: { - value: 'PETCT', - }, - }, - required: false, - }, - { - id: 'wauZK2QNEfDPwcAQo', - weight: 1, - attribute: 'StudyDescription', - constraint: { - contains: { - value: 'PET/CT', - }, - }, - required: false, - }, - ], - toolGroupIds: ['default'], - stages: [ - { - id: 'hYbmMy3b7pz7GLiaT', - name: 'default', - viewportStructure: { - layoutType: 'grid', - properties: { - rows: 1, - columns: 1, - viewportOptions: [], - }, - }, - displaySets: [ - { - id: 'ctDisplaySet', - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'GPEYqFLv2dwzCM322', - weight: 1, - attribute: 'Modality', - constraint: { - equals: { - value: 'CT', - }, - }, - required: true, - }, - { - id: 'vSjk7NCYjtdS3XZAw', - weight: 1, - attribute: 'SeriesNumber', - constraint: { - equals: { - value: '4', - }, - }, - required: false, - }, - { - id: 'vSjk7NCYjtdS3XZAw', - weight: 1, - attribute: 'SeriesDescription', - constraint: { - contains: { - value: 'CT', - }, - }, - required: false, - }, - { - id: 'vSjk7NCYjtdS3XZAw', - weight: 1, - attribute: 'SeriesDescription', - constraint: { - contains: { - value: 'CT WB', - }, - }, - required: false, - }, - ], - studyMatchingRules: [], - }, - ], - viewports: [ - { - id: 'ptACDisplaySet', - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'GPEYqFLv2dwzCM322', - weight: 1, - attribute: 'Modality', - constraint: { - equals: { - value: 'PT', - }, - }, - required: true, - }, - { - id: 'GPEYqFLv2dwzCM322', - weight: 1, - attribute: 'SeriesDescription', - constraint: { - contains: { - value: 'Corrected', - }, - }, - required: false, - }, - { - id: 'GPEYqFLv2dwzCM322', - weight: 1, - attribute: 'SeriesDescription', - constraint: { - contains: { - value: 'AC', - }, - }, - required: false, - }, - ], - studyMatchingRules: [], - }, - { - id: 'ptNACDisplaySet', - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'GPEYqFLv2dwzCM322', - weight: 1, - attribute: 'Modality', - constraint: { - equals: { - value: 'PT', - }, - }, - required: true, - }, - { - id: 'GPEYqFLv2dwzCM322', - weight: 1, - attribute: 'SeriesDescription', - constraint: { - contains: { - value: 'Uncorrected', - }, - }, - required: false, - }, - { - id: 'GPEYqFLv2dwzCM322', - weight: 1, - attribute: 'SeriesDescription', - constraint: { - contains: { - value: 'NAC', - }, - }, - required: false, - }, - ], - studyMatchingRules: [], - }, - ], - viewports: [ - { - viewportOptions: { - viewportId: 'ctAxial', - viewportType: 'stack', - background: [0, 0, 0], - orientation: 'AXIAL', - toolGroupId: 'default', - }, - displaySets: [ - { - id: 'ctDisplaySet', - }, - ], - }, - { - viewportOptions: { - viewportId: 'ptAxial', - viewportType: 'stack', - background: [1, 1, 1], - orientation: 'AXIAL', - toolGroupId: 'default', - }, - displaySets: [ - { - options: { - voi: { - windowWidth: 5, - windowCenter: 2.5, - }, - voiInverted: true, - }, - id: 'ptACDisplaySet', - }, - ], - }, - ], - createdDate: '2021-02-23T18:32:42.850Z', - }, - ], - numberOfPriorsReferenced: -1, -}; - function getHangingProtocolModule() { return [ { diff --git a/extensions/dicom-pdf/package.json b/extensions/dicom-pdf/package.json index ea053b263af..a3a761f95c4 100644 --- a/extensions/dicom-pdf/package.json +++ b/extensions/dicom-pdf/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "@ohif/ui": "^2.0.0", - "dcmjs": "0.22.0", + "dcmjs": "^0.22.1", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/extensions/dicom-video/package.json b/extensions/dicom-video/package.json index 4c3515de683..2053e50a685 100644 --- a/extensions/dicom-video/package.json +++ b/extensions/dicom-video/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "@ohif/ui": "^2.0.0", - "dcmjs": "0.22.0", + "dcmjs": "^0.22.1", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index c4315981c15..7d0560e81e4 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,10 +32,10 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "classnames": "^2.2.6", - "@cornerstonejs/core": "^0.8.1", - "@cornerstonejs/tools": "^0.15.0", + "@cornerstonejs/core": "^0.10.2", + "@cornerstonejs/tools": "^0.17.2", "@ohif/extension-cornerstone-dicom-sr": "^3.0.0", - "dcmjs": "0.22.0", + "dcmjs": "^0.22.1", "prop-types": "^15.6.2", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js index 1266ce7f1c4..f87f2ea5061 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js @@ -4,11 +4,13 @@ import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImported import { adapters } from 'dcmjs'; const { guid } = OHIF.utils; -const { MeasurementReport } = adapters.Cornerstone3D; +const { MeasurementReport, CORNERSTONE_3D_TAG } = adapters.Cornerstone3D; const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; +const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; + /** * */ @@ -52,14 +54,16 @@ export default function _hydrateStructuredReport( } }); + const datasetToUse = _mapLegacyDataSet(instance); + // Use dcmjs to generate toolState. const storedMeasurementByAnnotationType = MeasurementReport.generateToolState( - instance, + datasetToUse, // NOTE: we need to pass in the imageIds to dcmjs since the we use them // for the imageToWorld transformation. The following assumes that the order // that measurements were added to the display set are the same order as // the measurementGroups in the instance. - imageIdsForToolState, + sopInstanceUIDToImageId, cornerstone3D.utilities.imageToWorldCoords, cornerstone3D.metaData ); @@ -134,13 +138,12 @@ export default function _hydrateStructuredReport( } = instance; const annotation = { - annotationUID: toolData.uid, - data: toolData.data, + annotationUID: toolData.annotation.annotationUID, + data: toolData.annotation.data, metadata: { toolName: annotationType, referencedImageId: imageId, FrameOfReferenceUID, - label: '', }, }; @@ -148,7 +151,7 @@ export default function _hydrateStructuredReport( CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION ); - annotation.label = getLabelFromDCMJSImportedToolData(toolData); + annotation.data.label = getLabelFromDCMJSImportedToolData(toolData); const matchingMapping = mappings.find( m => m.annotationType === annotationType @@ -175,3 +178,66 @@ export default function _hydrateStructuredReport( SeriesInstanceUIDs, }; } + +function _mapLegacyDataSet(dataset) { + const REPORT = 'Imaging Measurements'; + const GROUP = 'Measurement Group'; + const TRACKING_IDENTIFIER = 'Tracking Identifier'; + + // Identify the Imaging Measurements + const imagingMeasurementContent = toArray(dataset.ContentSequence).find( + codeMeaningEquals(REPORT) + ); + + // Retrieve the Measurements themselves + const measurementGroups = toArray( + imagingMeasurementContent.ContentSequence + ).filter(codeMeaningEquals(GROUP)); + + // For each of the supported measurement types, compute the measurement data + const measurementData = {}; + + const cornerstoneToolClasses = + MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE; + + const registeredToolClasses = []; + + Object.keys(cornerstoneToolClasses).forEach(key => { + registeredToolClasses.push(cornerstoneToolClasses[key]); + measurementData[key] = []; + }); + + measurementGroups.forEach((measurementGroup, index) => { + const measurementGroupContentSequence = toArray( + measurementGroup.ContentSequence + ); + + const TrackingIdentifierGroup = measurementGroupContentSequence.find( + contentItem => + contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER + ); + + const TrackingIdentifier = TrackingIdentifierGroup.TextValue; + + let [cornerstoneTag, toolName] = TrackingIdentifier.split(':'); + if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) { + cornerstoneTag = CORNERSTONE_3D_TAG; + } + + const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`; + + TrackingIdentifierGroup.TextValue = mappedTrackingIdentifier; + }); + + return dataset; +} + +const toArray = function(x) { + return Array.isArray(x) ? x : [x]; +}; + +const codeMeaningEquals = codeMeaningName => { + return contentItem => { + return contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName; + }; +}; diff --git a/modes/basic-dev-mode/src/toolbarButtons.js b/modes/basic-dev-mode/src/toolbarButtons.js index 3f1cc3d2b3a..2c751204ead 100644 --- a/modes/basic-dev-mode/src/toolbarButtons.js +++ b/modes/basic-dev-mode/src/toolbarButtons.js @@ -46,7 +46,7 @@ function _createWwwcPreset(preset, title, subtitle) { { commandName: 'setWindowLevel', commandOptions: { - windowLevel: windowLevelPresets[preset], + ...windowLevelPresets[preset], }, context: 'CORNERSTONE3D', }, diff --git a/modes/longitudinal/README.md b/modes/longitudinal/README.md new file mode 100644 index 00000000000..4b6b907f463 --- /dev/null +++ b/modes/longitudinal/README.md @@ -0,0 +1,60 @@ +# Measurement Tracking Mode + + + +## Introduction +Measurement tracking mode allows you to: + +- Draw annotations and have them shown in the measurement panel +- Create a report from the tracked measurement and export them as DICOM SR +- Use already exported DICOM SR to re-hydrate the measurements in the viewer + +![preview](https://user-images.githubusercontent.com/7490180/171255703-e6d46da8-8d12-4685-b358-0c8d4d5cb5fe.png) + +## Workflow + + +### Status Icon +Each viewport has a left icon indicating whether the series within the viewport contains: + +- tracked measurement OR +- untracked measurement OR +- Structured Report OR +- Locked (uneditable) Structured Report + +In the following, we will discuss each category. + +![tracked](https://user-images.githubusercontent.com/7490180/171255750-c6903338-c295-4553-b8aa-8cb6a8d63943.png) + +### Tracked vs Untracked Measurements + +OHIF-v3 implements a workflow for measurement tracking that can be seen below. +In summary, when you create an annotation, a prompt will be shown whether to start tracking or not. If you start the tracking, the annotation style will change to a solid line, and annotation details get displayed on the measurement panel. On the other hand, if you decline the tracking prompt, the measurement will be considered "temporary," and annotation style remains as a dashed line and not shown on the right panel, and cannot be exported. + +Below, you can see different icons that appear for a tracked vs. untracked series in OHIF-v3. + + +![workflow](https://user-images.githubusercontent.com/7490180/171255780-dd249cbf-dd61-4e02-8d46-b91e01d53529.png) + + +### Reading and Writing DICOM SR +OHIF-v3 provides full support for reading, writing and mapping the DICOM Structured Report (SR) to interactable Cornerstone Tools. When you load an already exported DICOM SR into the viewer, you will be prompted whether to track the measurements for the series or not. + + +![preview](https://user-images.githubusercontent.com/7490180/171255797-6c374780-8e94-4a7f-a125-69b67c18c18c.png) + +If you click Yes, DICOM SR measurements gets re-hydrated into the viewer and the series become a tracked series. However, If you say no and later decide to say track the measurements, you can always click on the SR button that will prompt you with the same message again. + + +![restore](https://user-images.githubusercontent.com/7490180/171255813-8d460bd7-e64d-4bce-9467-ad5cf2615c56.png) + +The full workflow for saving measurements to SR and loading SR into the viewer is shown below. + +![sr-import](https://user-images.githubusercontent.com/7490180/171255826-c308ead6-9dad-4e91-9411-df62658cc839.png) + + +### Loading DICOM SR into an Already Tracked Series + +If you have an already tracked series and try to load a DICOM SR measurements, you will be shown the following lock icon. This means that, you can review the DICOM SR measurement, manipulate image and draw "temporary" measurements; however, you cannot edit the DICOM SR measurement. + +![locked](https://user-images.githubusercontent.com/7490180/171255842-91b84f91-4e1c-4a20-b4a2-cf9653560c43.png) diff --git a/modes/longitudinal/assets/locked.png b/modes/longitudinal/assets/locked.png new file mode 100644 index 00000000000..40e782045b0 Binary files /dev/null and b/modes/longitudinal/assets/locked.png differ diff --git a/modes/longitudinal/assets/preview.png b/modes/longitudinal/assets/preview.png new file mode 100644 index 00000000000..2b8cfb39321 Binary files /dev/null and b/modes/longitudinal/assets/preview.png differ diff --git a/modes/longitudinal/assets/restore.png b/modes/longitudinal/assets/restore.png new file mode 100644 index 00000000000..cfd6622d2ba Binary files /dev/null and b/modes/longitudinal/assets/restore.png differ diff --git a/modes/longitudinal/assets/sr-import.png b/modes/longitudinal/assets/sr-import.png new file mode 100644 index 00000000000..0d31e4c8dab Binary files /dev/null and b/modes/longitudinal/assets/sr-import.png differ diff --git a/modes/longitudinal/assets/tracked.png b/modes/longitudinal/assets/tracked.png new file mode 100644 index 00000000000..7da69fbaa48 Binary files /dev/null and b/modes/longitudinal/assets/tracked.png differ diff --git a/modes/longitudinal/assets/workflow.png b/modes/longitudinal/assets/workflow.png new file mode 100644 index 00000000000..2291ac3ad87 Binary files /dev/null and b/modes/longitudinal/assets/workflow.png differ diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index c6edca659ac..6891f8d6e4f 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -54,11 +54,11 @@ function modeFactory({ modeConfiguration }) { /** * Lifecycle hooks */ - onModeEnter: ({ servicesManager, extensionManager }) => { + onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => { const { ToolBarService, ToolGroupService } = servicesManager.services; // Init Default and SR ToolGroups - initToolGroups(extensionManager, ToolGroupService); + initToolGroups(extensionManager, ToolGroupService, commandsManager); let unsubscribe; diff --git a/modes/longitudinal/src/initToolGroups.js b/modes/longitudinal/src/initToolGroups.js index e30743bd7d4..3b4b9fdf7c3 100644 --- a/modes/longitudinal/src/initToolGroups.js +++ b/modes/longitudinal/src/initToolGroups.js @@ -1,4 +1,8 @@ -function initDefaultToolGroup(extensionManager, ToolGroupService) { +function initDefaultToolGroup( + extensionManager, + ToolGroupService, + commandsManager +) { const utilityModule = extensionManager.getModuleEntry( '@ohif/extension-cornerstone-3d.utilityModule.tools' ); @@ -36,11 +40,28 @@ function initDefaultToolGroup(extensionManager, ToolGroupService) { // disabled }; + const toolsConfig = { + [toolNames.ArrowAnnotate]: { + getTextCallback: (callback, eventDetails) => + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }), + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }; + const toolGroupId = 'default'; - ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {}); + ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig); } -function initSRToolGroup(extensionManager, ToolGroupService) { +function initSRToolGroup(extensionManager, ToolGroupService, commandsManager) { const SRUtilityModule = extensionManager.getModuleEntry( '@ohif/extension-cornerstone-dicom-sr.utilityModule.tools' ); @@ -96,13 +117,31 @@ function initSRToolGroup(extensionManager, ToolGroupService) { ], // disabled }; + + const toolsConfig = { + [toolNames.ArrowAnnotate]: { + getTextCallback: (callback, eventDetails) => + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }), + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }; + const toolGroupId = 'SRToolGroup'; - ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {}); + ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig); } -function initToolGroups(extensionManager, ToolGroupService) { - initDefaultToolGroup(extensionManager, ToolGroupService); - initSRToolGroup(extensionManager, ToolGroupService); +function initToolGroups(extensionManager, ToolGroupService, commandsManager) { + initDefaultToolGroup(extensionManager, ToolGroupService, commandsManager); + initSRToolGroup(extensionManager, ToolGroupService, commandsManager); } export default initToolGroups; diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index f7e03445489..3abf30e84d0 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -46,7 +46,7 @@ function _createWwwcPreset(preset, title, subtitle) { { commandName: 'setWindowLevel', commandOptions: { - windowLevel: windowLevelPresets[preset], + ...windowLevelPresets[preset], }, context: 'CORNERSTONE3D', }, diff --git a/platform/core/package.json b/platform/core/package.json index 7be27b9a783..863589c0c6e 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@babel/runtime": "7.16.3", - "dcmjs": "0.22.0", + "dcmjs": "^0.22.1", "dicomweb-client": "^0.6.0", "isomorphic-base64": "^1.0.2", "lodash.merge": "^4.6.1", diff --git a/platform/core/src/services/HangingProtocolService/ProtocolEngine.js b/platform/core/src/services/HangingProtocolService/ProtocolEngine.js index 59775f40b09..7649c2e5d82 100644 --- a/platform/core/src/services/HangingProtocolService/ProtocolEngine.js +++ b/platform/core/src/services/HangingProtocolService/ProtocolEngine.js @@ -96,7 +96,10 @@ export default class ProtocolEngine { // We clone it so that we don't accidentally add the // numberOfPriorsReferenced rule to the Protocol itself. let rules = protocol.protocolMatchingRules.slice(); - if (!rules) { + if (!rules || !rules.length) { + console.warn( + 'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules' + ); return; } diff --git a/platform/ui/src/components/ViewportGrid/ViewportGrid.tsx b/platform/ui/src/components/ViewportGrid/ViewportGrid.tsx index 71548ad42c0..79f3ceb2921 100644 --- a/platform/ui/src/components/ViewportGrid/ViewportGrid.tsx +++ b/platform/ui/src/components/ViewportGrid/ViewportGrid.tsx @@ -10,7 +10,6 @@ function ViewportGrid({ numRows, numCols, layoutType, children }) { height: '100%', width: '100%', }} - className="p-2" > {children} diff --git a/platform/ui/src/components/ViewportPane/ViewportPane.tsx b/platform/ui/src/components/ViewportPane/ViewportPane.tsx index eaa20a5b6fe..39023564c14 100644 --- a/platform/ui/src/components/ViewportPane/ViewportPane.tsx +++ b/platform/ui/src/components/ViewportPane/ViewportPane.tsx @@ -64,23 +64,29 @@ function ViewportPane({ onScroll={onInteractionHandler} onWheel={onInteractionHandler} className={classnames( - 'flex flex-col w-full h-full', - 'rounded-lg hover:border-primary-light transition duration-300 outline-none overflow-hidden', + 'w-full h-full rounded-lg overflow-hidden hover:border-primary-light transition duration-300 group', { - 'border border-primary-light': isActive, - 'border border-secondary-light': !isActive, + 'border-2 border-primary-light': isActive, + 'border-2 border-transparent': !isActive, }, className )} - // Normally, we'd use tailwindcss classes here, but margin and border classes use different units - // m-# (rem), border-# (px). To make sure we don't change the box size of our viewports - // and trigger a canvas resize, we have to use this little trick for margin. - // Assumes a :root font-fize of `16px` style={{ ...customStyle, }} > - {children} +
+ {children} +
); } diff --git a/platform/viewer/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js b/platform/viewer/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js index 5838465f611..8d79e9007bf 100644 --- a/platform/viewer/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js +++ b/platform/viewer/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js @@ -91,7 +91,7 @@ describe('OHIF Cornerstone Toolbar', () => { .trigger('mousemove', 'right', { buttons: 1, force: true }) .trigger('mouseup', { buttons: 1 }); - const expectedText = 'W:1926L:479'; + const expectedText = 'W:1930L:479'; cy.get('@viewportInfoTopLeft').should('have.text', expectedText); }); diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 25edfb3570f..9196bb9e339 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -1,7 +1,7 @@ { "name": "@ohif/viewer", "version": "5.0.0", - "productVersion": "3.0.8", + "productVersion": "3.1.0", "description": "OHIF Viewer", "author": "OHIF Contributors", "license": "MIT", @@ -62,7 +62,7 @@ "core-js": "^3.16.1", "cornerstone-math": "^0.1.9", "cornerstone-wado-image-loader": "^4.1.2", - "dcmjs": "0.22.0", + "dcmjs": "^0.22.1", "detect-gpu": "^4.0.16", "dicom-parser": "^1.8.9", "dotenv-webpack": "^1.7.0", diff --git a/platform/viewer/src/App.tsx b/platform/viewer/src/App.tsx index 47a04e50554..9b483af2326 100644 --- a/platform/viewer/src/App.tsx +++ b/platform/viewer/src/App.tsx @@ -59,6 +59,7 @@ function App({ config, defaultExtensions, defaultModes }) { dataSources, extensionManager, servicesManager, + commandsManager, hotkeysManager, routerBasename, }); diff --git a/platform/viewer/src/components/ViewportGrid.tsx b/platform/viewer/src/components/ViewportGrid.tsx index 4c2120c4b52..eeac0775cda 100644 --- a/platform/viewer/src/components/ViewportGrid.tsx +++ b/platform/viewer/src/components/ViewportGrid.tsx @@ -163,15 +163,7 @@ function ViewerViewportGrid(props) { * Loading indicator until numCols and numRows are gotten from the HangingProtocolService */ if (!numRows || !numCols) { - return ( -
-
-
-
-
-
-
- ); + return null; } /** @@ -309,10 +301,10 @@ function ViewerViewportGrid(props) { onInteraction={onInteractionHandler} customStyle={{ position: 'absolute', - top: viewportY * 100 + 0.5 + '%', - left: viewportX * 100 + 0.5 + '%', - width: viewportWidth * 100 - 0.5 + '%', - height: viewportHeight * 100 - 0.5 + '%', + top: viewportY * 100 + 0.2 + '%', + left: viewportX * 100 + 0.2 + '%', + width: viewportWidth * 100 - 0.3 + '%', + height: viewportHeight * 100 - 0.3 + '%', }} isActive={isActive} > diff --git a/platform/viewer/src/routes/CallbackPage.tsx b/platform/viewer/src/routes/CallbackPage.tsx index 085dbd10a65..74474552fb6 100644 --- a/platform/viewer/src/routes/CallbackPage.tsx +++ b/platform/viewer/src/routes/CallbackPage.tsx @@ -11,8 +11,7 @@ function CallbackPage({ userManager, onRedirectSuccess }) { .then(user => onRedirectSuccess(user)) .catch(error => onRedirectError(error)); - // todo: add i18n (or return null?) - return
Redirecting...
; + return null; } CallbackPage.propTypes = { diff --git a/platform/viewer/src/routes/Mode/Mode.tsx b/platform/viewer/src/routes/Mode/Mode.tsx index 18ccecd74f6..97e8f4f0010 100644 --- a/platform/viewer/src/routes/Mode/Mode.tsx +++ b/platform/viewer/src/routes/Mode/Mode.tsx @@ -61,6 +61,7 @@ export default function ModeRoute({ dataSourceName, extensionManager, servicesManager, + commandsManager, hotkeysManager, }) { // Parse route params/querystring @@ -206,7 +207,7 @@ export default function ModeRoute({ DisplaySetService.init(extensionManager, sopClassHandlers); extensionManager.onModeEnter(); - mode?.onModeEnter({ servicesManager, extensionManager }); + mode?.onModeEnter({ servicesManager, extensionManager, commandsManager }); // Adding hanging protocols of extensions after onModeEnter since // it will reset the protocols diff --git a/platform/viewer/src/routes/SignoutCallbackComponent.tsx b/platform/viewer/src/routes/SignoutCallbackComponent.tsx index 4af3467d0a1..c24a0e0bdda 100644 --- a/platform/viewer/src/routes/SignoutCallbackComponent.tsx +++ b/platform/viewer/src/routes/SignoutCallbackComponent.tsx @@ -22,8 +22,7 @@ function SignoutCallbackComponent({ userManager }) { .then(user => onRedirectSuccess(user)) .catch(error => onRedirectError(error)); - // todo: add i18n - return
Redirecting...
; + return null; } SignoutCallbackComponent.propTypes = { diff --git a/platform/viewer/src/routes/buildModeRoutes.tsx b/platform/viewer/src/routes/buildModeRoutes.tsx index ea72a873991..1447dff8990 100644 --- a/platform/viewer/src/routes/buildModeRoutes.tsx +++ b/platform/viewer/src/routes/buildModeRoutes.tsx @@ -27,6 +27,7 @@ export default function buildModeRoutes({ dataSources, extensionManager, servicesManager, + commandsManager, hotkeysManager, }) { const routes = []; @@ -58,6 +59,7 @@ export default function buildModeRoutes({ dataSourceName={dataSourceName} extensionManager={extensionManager} servicesManager={servicesManager} + commandsManager={commandsManager} hotkeysManager={hotkeysManager} /> ); @@ -80,6 +82,7 @@ export default function buildModeRoutes({ dataSourceName={defaultDataSourceName} extensionManager={extensionManager} servicesManager={servicesManager} + commandsManager={commandsManager} hotkeysManager={hotkeysManager} /> ); diff --git a/platform/viewer/src/routes/index.tsx b/platform/viewer/src/routes/index.tsx index 8426bea990c..2b1570f673a 100644 --- a/platform/viewer/src/routes/index.tsx +++ b/platform/viewer/src/routes/index.tsx @@ -33,6 +33,7 @@ const createRoutes = ({ dataSources, extensionManager, servicesManager, + commandsManager, hotkeysManager, routerBasename, }) => { @@ -42,6 +43,7 @@ const createRoutes = ({ dataSources, extensionManager, servicesManager, + commandsManager, hotkeysManager, }) || []; diff --git a/yarn.lock b/yarn.lock index 800920efb03..ef5d4aeabc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1313,28 +1313,28 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-0.1.1.tgz#5bd1c52a33a425299299e970312731fa0cc2711b" integrity sha512-HOMMOLV6xy8O/agNGGvrl0a8DwShpBvWxAzEzv2pqq12d3r5z/3MyIgNA3Oj/8bIBVvvVXxh9RX7rMDRHJdowg== -"@cornerstonejs/core@^0.8.1": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.8.1.tgz#27f578a19e12f037bee55aa40b60f626eb26e6a0" - integrity sha512-ZNQ+uHo8eLi0JTQ5/7MxtX2Joittk7gLZY1BJ5FpPbM4rX+eCoJYNajMz0XybB9Lz89jwGX/3/9i+RGIX3Z7rA== +"@cornerstonejs/core@^0.10.2", "@cornerstonejs/core@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.10.3.tgz#5f024bd69ae431c39b40e98c860351f583362df7" + integrity sha512-wsTLvRyAxffLBShnNhmFYBuYgUs4uKes9xFzsCiXQfN/bXAV9FWxuvVQLTPL588vp5jju0JHFmwpBXCyaBFnYw== dependencies: detect-gpu "^4.0.7" lodash.clonedeep "4.5.0" -"@cornerstonejs/streaming-image-volume-loader@^0.2.26": - version "0.2.26" - resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.2.26.tgz#b7d8ef8e2429647ec7b82522ba52e9ef388e7871" - integrity sha512-b4WpxA8EkssKr4/yAVW2utZURSWJec+xoaHK2Qlj67+slRKhbSeyaWI2QaPd3exIWEXs0095XN3tKBhoU2Yxlg== +"@cornerstonejs/streaming-image-volume-loader@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.3.3.tgz#728bc42b3eeca653c0feffb650617f20f4d6d40f" + integrity sha512-XSvbL8kWtwmhwWO3vflaQZfhCnPZyZ/8PAFPypPhIMVbfEqUI5HUMRs4UCPecETyYtF/PfCLjM5AkJNwti7GcA== dependencies: - "@cornerstonejs/core" "^0.8.1" + "@cornerstonejs/core" "^0.10.3" cornerstone-wado-image-loader "^4.1.2" -"@cornerstonejs/tools@^0.15.0": - version "0.15.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.15.2.tgz#743dc6a0b5229ddbf58c6f35ac408aba5672f786" - integrity sha512-cGdOMpRynIMqGOP4q/MpDnClNhmoqbpzkFoPzUnZdP8wxRAObYb70wBTsrg3+3BM+L1GEspVpcZ+KsHX8gM8Fg== +"@cornerstonejs/tools@^0.17.2": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.17.3.tgz#90a1121de3bc509a7f2de8680a3025d8ca27f9b9" + integrity sha512-ejB/fdPQrfCRPyMOTgd52aQpbLRKnhrMAOQioHOKniQRmzzkZDfEXcgNQYYmVcHin70ytYRMjawUtvOSf6etRg== dependencies: - "@cornerstonejs/core" "^0.8.1" + "@cornerstonejs/core" "^0.10.3" lodash.clonedeep "4.5.0" "@csstools/postcss-color-function@^1.1.0": @@ -8978,10 +8978,10 @@ dayjs@^1.10.4: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5" integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw== -dcmjs@0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.22.0.tgz#2b3ce332d0b546461c69f94ada168b2eac726e5a" - integrity sha512-yIptgOUJyoXqp179OGDq05uJI7d06jb+M7aKunPctaiG/FPPmfkaEXokdJyTV0MwsdRqQr4c7Q/bA8dTCUo9Kg== +dcmjs@^0.22.1: + version "0.22.1" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.22.1.tgz#45c845f228e5cf00415a5d3470715060bdcb23ae" + integrity sha512-pLYxMPx83fChiZrP3XER1ui2a6rKwZFFdlrkO6hdXUvfpossAdkeMuhTJ938usr/O2RZtWqPMBMGKW8YXzG2PA== dependencies: "@babel/runtime-corejs2" "^7.17.8" gl-matrix "^3.1.0"