diff --git a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx index bef64b145..1b2b42b22 100644 --- a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx +++ b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx @@ -116,7 +116,10 @@ function OHIFCornerstoneRTViewport(props) { orientation: viewportOptions.orientation, viewportId: viewportOptions.viewportId, }} - onElementEnabled={onElementEnabled} + onElementEnabled={evt => { + props.onElementEnabled?.(evt); + onElementEnabled(evt); + }} onElementDisabled={onElementDisabled} > ); diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index 8c0e004ba..dd88915f0 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -46,9 +46,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.70.5", - "@cornerstonejs/core": "^1.70.5", - "@kitware/vtk.js": "30.3.1", + "@cornerstonejs/adapters": "^1.70.6", + "@cornerstonejs/core": "^1.70.6", + "@kitware/vtk.js": "30.3.3", "react-color": "^2.19.3" } } diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 5bff8aeef..4a770d7e1 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -434,30 +434,6 @@ const commandsModule = ({ }); }); }, - toggleThresholdRangeAndDynamic() { - const toolGroupIds = toolGroupService.getToolGroupIds(); - - if (!toolGroupIds) { - return; - } - - toolGroupIds.forEach(toolGroupId => { - const toolGroup = toolGroupService.getToolGroup(toolGroupId); - const brushInstances = segmentationUtils.getBrushToolInstances(toolGroup.id); - - brushInstances.forEach(({ configuration }) => { - const { activeStrategy, strategySpecificConfiguration } = configuration; - - if (activeStrategy.startsWith('THRESHOLD')) { - const thresholdConfig = strategySpecificConfiguration.THRESHOLD; - - if (thresholdConfig) { - thresholdConfig.isDynamic = !thresholdConfig.isDynamic; - } - } - }); - }); - }, }; const definitions = { @@ -491,9 +467,6 @@ const commandsModule = ({ setThresholdRange: { commandFn: actions.setThresholdRange, }, - toggleThresholdRangeAndDynamic: { - commandFn: actions.toggleThresholdRangeAndDynamic, - }, }; return { diff --git a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts index 0a06cf7eb..8bfe0c1e1 100644 --- a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts @@ -59,12 +59,8 @@ function getToolNameForButton(button) { const commands = props?.commands || button.commands; const commandsArray = Array.isArray(commands) ? commands : [commands]; const firstCommand = commandsArray[0]; - if (typeof firstCommand === 'string') { - // likely not a cornerstone tool - return null; - } - if ('commandOptions' in firstCommand) { + if (firstCommand?.commandOptions) { return firstCommand.commandOptions.toolName ?? props?.id ?? button.id; } diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index c7f0232c9..e640ca309 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -18,11 +18,13 @@ export default function PanelSegmentation({ extensionManager, configuration, }) { - const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services; + const { segmentationService, viewportGridService, uiDialogService, displaySetService } = + servicesManager.services; const { t } = useTranslation('PanelSegmentation'); const [selectedSegmentationId, setSelectedSegmentationId] = useState(null); + const [addSegmentationClassName, setAddSegmentationClassName] = useState(''); const [segmentationConfiguration, setSegmentationConfiguration] = useState( segmentationService.getConfiguration() ); @@ -52,6 +54,52 @@ export default function PanelSegmentation({ }; }, []); + // temporary measure to not allow add segmentation when the selected viewport + // is stack viewport + useEffect(() => { + const handleActiveViewportChange = viewportId => { + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport( + viewportId || viewportGridService.getActiveViewportId() + ); + + if (!displaySetUIDs) { + return; + } + + const isReconstructable = + displaySetUIDs?.some(displaySetUID => { + const displaySet = displaySetService.getDisplaySetByUID(displaySetUID); + return displaySet?.isReconstructable; + }) || false; + + if (isReconstructable) { + setAddSegmentationClassName(''); + } else { + setAddSegmentationClassName('ohif-disabled'); + } + }; + + // Handle initial state + handleActiveViewportChange(); + + const changed = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED; + const ready = viewportGridService.EVENTS.VIEWPORTS_READY; + + const subs = []; + [ready, changed].forEach(evt => { + const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => { + handleActiveViewportChange(viewportId); + }); + + subs.push(unsubscribe); + }); + + // Clean up + return () => { + subs.forEach(unsub => unsub()); + }; + }, []); + const getToolGroupIds = segmentationId => { const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId); @@ -152,6 +200,7 @@ export default function PanelSegmentation({ segmentationService.removeSegment(segmentationId, segmentIndex); }; + // segment hide const onToggleSegmentVisibility = (segmentationId, segmentIndex) => { const segmentation = segmentationService.getSegmentation(segmentationId); const segmentInfo = segmentation.segments[segmentIndex]; @@ -257,6 +306,7 @@ export default function PanelSegmentation({ disableEditing={configuration.disableEditing} activeSegmentationId={selectedSegmentationId || ''} onSegmentationAdd={onSegmentationAddWrapper} + addSegmentationClassName={addSegmentationClassName} showAddSegment={allowAddSegment} onSegmentationClick={onSegmentationClick} onSegmentationDelete={onSegmentationDelete} diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index d14e720de..894a4c407 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -112,7 +112,10 @@ function OHIFCornerstoneSEGViewport(props) { orientation: viewportOptions.orientation, viewportId: viewportOptions.viewportId, }} - onElementEnabled={onElementEnabled} + onElementEnabled={evt => { + props.onElementEnabled?.(evt); + onElementEnabled(evt); + }} onElementDisabled={onElementDisabled} > ); diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 120ce6049..2b4a1ec11 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -46,9 +46,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.70.5", - "@cornerstonejs/core": "^1.70.5", - "@cornerstonejs/tools": "^1.70.5", + "@cornerstonejs/adapters": "^1.70.6", + "@cornerstonejs/core": "^1.70.6", + "@cornerstonejs/tools": "^1.70.6", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts index 278eed828..eaf3bf6ea 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -65,7 +65,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { // Filter toolData to only render the data for the active SR. const filteredAnnotations = annotations.filter(annotation => - trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier) + trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier) ); if (!viewport._actors?.size) { @@ -82,8 +82,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { for (let i = 0; i < filteredAnnotations.length; i++) { const annotation = filteredAnnotations[i]; const annotationUID = annotation.annotationUID; - const { renderableData } = annotation.data.cachedStats; - const { cachedStats } = annotation.data; + const { renderableData, TrackingUniqueIdentifier } = annotation.data; const { referencedImageId } = annotation.metadata; styleSpecifier.annotationUID = annotationUID; @@ -95,7 +94,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation); const lineDash = this.getStyle('lineDash', styleSpecifier, annotation); const color = - cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier + TrackingUniqueIdentifier === activeTrackingUniqueIdentifier ? 'rgb(0, 255, 0)' : this.getStyle('color', styleSpecifier, annotation); diff --git a/extensions/cornerstone-dicom-sr/src/utils/addDICOMSRDisplayAnnotation.ts b/extensions/cornerstone-dicom-sr/src/utils/addDICOMSRDisplayAnnotation.ts index fc0eda76c..585a9183a 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addDICOMSRDisplayAnnotation.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addDICOMSRDisplayAnnotation.ts @@ -51,10 +51,9 @@ export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameN handles: { textBox: measurement.textBox ?? {}, }, - cachedStats: { - TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier, - renderableData: measurementData.renderableData, - }, + cachedStats: {}, + TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier, + renderableData: measurementData.renderableData, frameNumber, }, }; diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx index a725d6c50..6158df331 100644 --- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx @@ -8,6 +8,7 @@ import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSR import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui'; import hydrateStructuredReport from '../utils/hydrateStructuredReport'; import { useAppConfig } from '@state'; +import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet'; const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'; @@ -203,7 +204,10 @@ function OHIFCornerstoneSRViewport(props) { // The positionIds for the viewport aren't meaningful for the child display sets positionIds: null, }} - onElementEnabled={onElementEnabled} + onElementEnabled={evt => { + props.onElementEnabled?.(evt); + onElementEnabled(evt); + }} initialImageIndex={initialImageIndex} isJumpToMeasurementDisabled={true} > @@ -378,6 +382,10 @@ async function _getViewportReferencedDisplaySetData( measurementSelected, displaySetService ) { + const { measurements } = displaySet; + const measurement = measurements[measurementSelected]; + + const { displaySetInstanceUID } = measurement; if (!displaySet.keyImageDisplaySet) { // Create a new display set, and preserve a reference to it here, // so that it can be re-displayed and shown inside the SR viewport. diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json index 337f58e67..044a3923b 100644 --- a/extensions/cornerstone-dynamic-volume/package.json +++ b/extensions/cornerstone-dynamic-volume/package.json @@ -42,9 +42,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/core": "^1.70.5", - "@cornerstonejs/streaming-image-volume-loader": "^1.70.5", - "@cornerstonejs/tools": "^1.70.5", + "@cornerstonejs/core": "^1.70.6", + "@cornerstonejs/streaming-image-volume-loader": "^1.70.6", + "@cornerstonejs/tools": "^1.70.6", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx b/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx index b780669f9..143b55620 100644 --- a/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx +++ b/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx @@ -88,7 +88,7 @@ const DynamicVolumeControls = ({ className="w-1/2" onClick={() => { setComputedView(false); - onDynamicClick(); + onDynamicClick?.(); }} > 4D diff --git a/extensions/cornerstone-dynamic-volume/src/panels/PanelGenerateImage.tsx b/extensions/cornerstone-dynamic-volume/src/panels/PanelGenerateImage.tsx index f11e6838a..9f8d821fd 100644 --- a/extensions/cornerstone-dynamic-volume/src/panels/PanelGenerateImage.tsx +++ b/extensions/cornerstone-dynamic-volume/src/panels/PanelGenerateImage.tsx @@ -202,7 +202,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }) } const { element } = viewportInfo; - cineService.playClip(element, { framesPerSecond: frameRate }); + cineService.playClip(element, { framesPerSecond: frameRate, viewportId: activeViewportId }); }; const handleStop = () => { diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index b9f01bf73..1332c6206 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -38,7 +38,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.2", - "@cornerstonejs/dicom-image-loader": "^1.70.5", + "@cornerstonejs/dicom-image-loader": "^1.70.6", "@icr/polyseg-wasm": "^0.4.0", "@ohif/core": "3.8.0-beta.80", "@ohif/ui": "3.8.0-beta.80", @@ -55,12 +55,12 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.70.5", - "@cornerstonejs/core": "^1.70.5", - "@cornerstonejs/streaming-image-volume-loader": "^1.70.5", - "@cornerstonejs/tools": "^1.70.5", + "@cornerstonejs/adapters": "^1.70.6", + "@cornerstonejs/core": "^1.70.6", + "@cornerstonejs/streaming-image-volume-loader": "^1.70.6", + "@cornerstonejs/tools": "^1.70.6", "@icr/polyseg-wasm": "^0.4.0", - "@kitware/vtk.js": "30.3.1", + "@kitware/vtk.js": "30.3.3", "html2canvas": "^1.4.1", "lodash.debounce": "4.0.8", "lodash.merge": "^4.6.2", diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index 12577147c..50dfd8c49 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -114,10 +114,21 @@ const OHIFCornerstoneViewport = React.memo(props => { // of the imageData in the OHIFCornerstoneViewport. This prop is used // to set the initial state of the viewport's first image to render initialImageIndex, + // if the viewport is part of a hanging protocol layout + // we should not really rely on the old synchronizers and + // you see below we only rehydrate the synchronizers if the viewport + // is not part of the hanging protocol layout. HPs should + // define their own synchronizers. Since the synchronizers are + // viewportId dependent and + isHangingProtocolLayout, } = props; const viewportId = viewportOptions.viewportId; + if (!viewportId) { + throw new Error('Viewport ID is required'); + } + // Since we only have support for dynamic data in volume viewports, we should // handle this case here and set the viewportType to volume if any of the // displaySets are dynamic volumes @@ -194,7 +205,7 @@ const OHIFCornerstoneViewport = React.memo(props => { const synchronizersStore = stateSyncService.getState().synchronizersStore; - if (synchronizersStore?.[viewportId]?.length) { + if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) { // If the viewport used to have a synchronizer, re apply it again _rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService); } diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 0223afa05..f243a0ec7 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -51,6 +51,12 @@ function commandsModule({ function _getActiveViewportEnabledElement() { return getActiveViewportEnabledElement(viewportGridService); } + + function _getActiveViewportToolGroupId() { + const viewport = _getActiveViewportEnabledElement(); + return toolGroupService.getToolGroupForViewport(viewport.id); + } + const actions = { /** * Generates the selector props for the context menu, specific to @@ -300,42 +306,45 @@ function commandsModule({ const renderingEngine = cornerstoneViewportService.getRenderingEngine(); renderingEngine.render(); }, - toggleEnabledDisabledToolbar({ value, itemId, toolGroupIds = [] }) { + toggleEnabledDisabledToolbar({ value, itemId, toolGroupId }) { const toolName = itemId || value; - toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds(); - toolGroupIds.forEach(toolGroupId => { - const toolGroup = toolGroupService.getToolGroup(toolGroupId); - if (!toolGroup || !toolGroup.hasTool(toolName)) { - return; - } + toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId(); - const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled; + const toolGroup = toolGroupService.getToolGroup(toolGroupId); + if (!toolGroup || !toolGroup.hasTool(toolName)) { + return; + } - toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName); - }); + const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled; + + toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName); }, - toggleActiveDisabledToolbar({ value, itemId, toolGroupIds = [] }) { + toggleActiveDisabledToolbar({ value, itemId, toolGroupId }) { const toolName = itemId || value; - toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds(); - toolGroupIds.forEach(toolGroupId => { - const toolGroup = toolGroupService.getToolGroup(toolGroupId); - if (!toolGroup || !toolGroup.hasTool(toolName)) { - return; - } + toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId(); + const toolGroup = toolGroupService.getToolGroup(toolGroupId); + if (!toolGroup || !toolGroup.hasTool(toolName)) { + return; + } - const toolIsActive = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Active; + const toolIsActive = [ + Enums.ToolModes.Active, + Enums.ToolModes.Enabled, + Enums.ToolModes.Passive, + ].includes(toolGroup.getToolOptions(toolName).mode); - toolIsActive - ? toolGroup.setToolDisabled(toolName) - : actions.setToolActive({ toolName, toolGroupId }); + toolIsActive + ? toolGroup.setToolDisabled(toolName) + : actions.setToolActive({ toolName, toolGroupId }); - // we should set the previously active tool to active after we set the - // current tool disabled - if (toolIsActive) { - const prevToolName = toolGroup.getPrevActivePrimaryToolName(); + // we should set the previously active tool to active after we set the + // current tool disabled + if (toolIsActive) { + const prevToolName = toolGroup.getPrevActivePrimaryToolName(); + if (prevToolName !== toolName) { actions.setToolActive({ toolName: prevToolName, toolGroupId }); } - }); + } }, setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => { // Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons) @@ -441,11 +450,9 @@ function commandsModule({ const { viewport } = enabledElement; - if (viewport instanceof StackViewport) { - const { flipHorizontal } = viewport.getCamera(); - viewport.setCamera({ flipHorizontal: !flipHorizontal }); - viewport.render(); - } + const { flipHorizontal } = viewport.getCamera(); + viewport.setCamera({ flipHorizontal: !flipHorizontal }); + viewport.render(); }, flipViewportVertical: () => { const enabledElement = _getActiveViewportEnabledElement(); @@ -456,11 +463,9 @@ function commandsModule({ const { viewport } = enabledElement; - if (viewport instanceof StackViewport) { - const { flipVertical } = viewport.getCamera(); - viewport.setCamera({ flipVertical: !flipVertical }); - viewport.render(); - } + const { flipVertical } = viewport.getCamera(); + viewport.setCamera({ flipVertical: !flipVertical }); + viewport.render(); }, invertViewport: ({ element }) => { let enabledElement; @@ -819,7 +824,7 @@ function commandsModule({ } crosshairInstances.forEach(ins => { - ins.resetCrosshairs(); + ins?.resetCrosshairs(); }); }, }; diff --git a/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx b/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx index 28414877b..795ec5529 100644 --- a/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx +++ b/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx @@ -21,7 +21,7 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) { const validFrameRate = Math.max(frameRate, 1); return isPlaying - ? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate }) + ? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate, viewportId }) : cineService.stopClip(enabledVPElement); }; diff --git a/extensions/cornerstone/src/getToolbarModule.tsx b/extensions/cornerstone/src/getToolbarModule.tsx index 74a116269..2f2de9466 100644 --- a/extensions/cornerstone/src/getToolbarModule.tsx +++ b/extensions/cornerstone/src/getToolbarModule.tsx @@ -108,32 +108,27 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { }; }, }, + { + name: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled', + evaluate: ({ viewportId, button, disabledText }) => + _evaluateToggle({ + viewportId, + button, + disabledText, + offModes: [Enums.ToolModes.Disabled], + toolGroupService, + }), + }, { name: 'evaluate.cornerstoneTool.toggle', - evaluate: ({ viewportId, button, disabledText }) => { - const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); - - if (!toolGroup) { - return; - } - const toolName = getToolNameForButton(button); - - if (!toolGroup || !toolGroup.hasTool(toolName)) { - return { - disabled: true, - className: '!text-common-bright ohif-disabled', - disabledText: disabledText ?? 'Not available on the current viewport', - }; - } - - const isOff = [Enums.ToolModes.Disabled, Enums.ToolModes.Passive].includes( - toolGroup.getToolOptions(toolName).mode - ); - - return { - className: getToggledClassName(!isOff), - }; - }, + evaluate: ({ viewportId, button, disabledText }) => + _evaluateToggle({ + viewportId, + button, + disabledText, + offModes: [Enums.ToolModes.Disabled, Enums.ToolModes.Passive], + toolGroupService, + }), }, { name: 'evaluate.cornerstone.synchronizer', @@ -146,7 +141,11 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { }; } - const synchronizerType = button?.commands?.[0]?.commandOptions?.type; + const isArray = Array.isArray(button.commands); + + const synchronizerType = isArray + ? button.commands?.[0].commandOptions.type + : button.commands?.commandOptions.type; synchronizers = syncGroupService.getSynchronizersOfType(synchronizerType); @@ -268,18 +267,38 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { ]; } +function _evaluateToggle({ viewportId, button, disabledText, offModes, toolGroupService }) { + const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); + + if (!toolGroup) { + return; + } + const toolName = getToolNameForButton(button); + + if (!toolGroup.hasTool(toolName)) { + return { + disabled: true, + className: '!text-common-bright ohif-disabled', + disabledText: disabledText ?? 'Not available on the current viewport', + }; + } + + const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode); + + return { + className: getToggledClassName(!isOff), + }; +} + +// Todo: this is duplicate, we should move it to a shared location function getToolNameForButton(button) { const { props } = button; const commands = props?.commands || button.commands; const commandsArray = Array.isArray(commands) ? commands : [commands]; const firstCommand = commandsArray[0]; - if (typeof firstCommand === 'string') { - // likely not a cornerstone tool - return null; - } - if ('commandOptions' in firstCommand) { + if (firstCommand?.commandOptions) { return firstCommand.commandOptions.toolName ?? props?.id ?? button.id; } diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index 83e463a2a..a8ef8ac47 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -61,7 +61,8 @@ const cornerstoneExtension: Types.Extensions.Extension = { */ id, - onModeExit: (): void => { + onModeExit: ({ servicesManager }): void => { + const { cineService } = servicesManager.services; // Empty out the image load and retrieval pools to prevent memory leaks // on the mode exits Object.values(cs3DEnums.RequestType).forEach(type => { @@ -69,6 +70,8 @@ const cornerstoneExtension: Types.Extensions.Extension = { imageRetrievalPoolManager.clearRequestStack(type); }); + cineService.setIsCineEnabled(false); + enabledElementReset(); }, diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index 2282abb5b..b7ec7abde 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -34,6 +34,8 @@ import { CornerstoneServices } from './types'; import initViewTiming from './utils/initViewTiming'; import { colormaps } from './utils/colormaps'; +import { debounce } from 'lodash'; + const { registerColormap } = csUtilities.colormap; // TODO: Cypress tests are currently grabbing this from the window? @@ -291,8 +293,21 @@ export default async function init({ eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null)); eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null)); - colormaps.forEach(registerColormap); + + // Create a debounced function that shows the notification + const debouncedShowNotification = debounce(detail => { + uiNotificationService.show({ + title: detail.type, + message: detail.message, + type: 'error', + }); + }, 300); + + // Event listener + eventTarget.addEventListener(EVENTS.ERROR_EVENT, ({ detail }) => { + debouncedShowNotification(detail); + }); } function CPUModal() { diff --git a/extensions/cornerstone/src/initCineService.ts b/extensions/cornerstone/src/initCineService.ts index bbd04d3df..5e3246135 100644 --- a/extensions/cornerstone/src/initCineService.ts +++ b/extensions/cornerstone/src/initCineService.ts @@ -59,8 +59,8 @@ function initCineService(servicesManager) { return utilities.cine.playClip(element, playClipOptions); }; - const stopClip = element => { - return utilities.cine.stopClip(element); + const stopClip = (element, stopClipOptions) => { + return utilities.cine.stopClip(element, stopClipOptions); }; cineService.setServiceImplementation({ diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts index 63d66fc55..a8d40b3d0 100644 --- a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts +++ b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts @@ -45,7 +45,11 @@ class CornerstoneCacheService { // as a reference volume, if so, we should hang a volume viewport // instead of a stack viewport if (this._shouldRenderSegmentation(displaySets)) { - viewportType = 'volume'; + // 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; diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 319884d62..3f63bcdcd 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -183,7 +183,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi const { lutPresentation, positionPresentation } = presentations; if (lutPresentation) { const { presentation } = lutPresentation; - if (viewport instanceof VolumeViewport) { + if (viewport instanceof BaseVolumeViewport) { if (presentation instanceof Map) { presentation.forEach((properties, volumeId) => { viewport.setProperties(properties, volumeId); diff --git a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx index 68a6ada3d..f4eaa13e8 100644 --- a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx +++ b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx @@ -27,7 +27,11 @@ const CornerstoneViewportDownloadForm = ({ const activeViewportElement = enabledElement?.element; const activeViewportEnabledElement = getEnabledElement(activeViewportElement); - const { viewportId: activeViewportId, renderingEngineId } = activeViewportEnabledElement; + const { + viewportId: activeViewportId, + renderingEngineId, + viewport: activeViewport, + } = activeViewportEnabledElement; const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId); @@ -93,7 +97,7 @@ const CornerstoneViewportDownloadForm = ({ renderingEngine.resize(); // Trigger the render on the viewport to update the on screen - downloadViewport.resetCamera(); + // downloadViewport.resetCamera(); downloadViewport.render(); downloadViewportElement.addEventListener( @@ -120,6 +124,12 @@ const CornerstoneViewportDownloadForm = ({ resolve({ dataUrl, width: newWidth, height: newHeight }); downloadViewportElement.removeEventListener(Enums.Events.IMAGE_RENDERED, updateViewport); + + // for some reason we need a reset camera here, and I don't know why + downloadViewport.resetCamera(); + const presentation = activeViewport.getViewPresentation(); + downloadViewport.setView(activeViewport.getViewReference(), presentation); + downloadViewport.render(); } ); }); @@ -161,7 +171,6 @@ const CornerstoneViewportDownloadForm = ({ downloadViewport.addActor(actor); }); - downloadViewport.setCamera(viewport.getCamera()); downloadViewport.render(); const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE); diff --git a/extensions/cornerstone/src/utils/interleaveCenterLoader.ts b/extensions/cornerstone/src/utils/interleaveCenterLoader.ts index d8f64b5d8..16ed7eeeb 100644 --- a/extensions/cornerstone/src/utils/interleaveCenterLoader.ts +++ b/extensions/cornerstone/src/utils/interleaveCenterLoader.ts @@ -51,20 +51,27 @@ export default function interleaveCenterLoader({ * listen to it and as the other viewports are created we can set the volumes for them * since volumes are already started loading. */ - if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) { + const uniqueViewportVolumeDisplaySetUIDs = new Set(); + viewportIdVolumeInputArrayMap.forEach((volumeInputArray, viewportId) => { + volumeInputArray.forEach(volumeInput => { + const { volumeId } = volumeInput; + uniqueViewportVolumeDisplaySetUIDs.add(volumeId); + }); + }); + + const uniqueMatchedDisplaySetUIDs = new Set(); + + matchDetails.forEach(matchDetail => { + const { displaySetsInfo } = matchDetail; + displaySetsInfo.forEach(({ displaySetInstanceUID }) => { + uniqueMatchedDisplaySetUIDs.add(displaySetInstanceUID); + }); + }); + + if (uniqueViewportVolumeDisplaySetUIDs.size !== uniqueMatchedDisplaySetUIDs.size) { return; } - // Check if all the matched volumes are loaded - for (const [_, details] of displaySetsMatchDetails.entries()) { - const { SeriesInstanceUID } = details; - - // HangingProtocol has matched, but don't have all the volumes created yet, so return - if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) { - return; - } - } - const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice(); // get volumes from cache const volumes = volumeIds.map(volumeId => { diff --git a/extensions/cornerstone/src/utils/interleaveTopToBottom.ts b/extensions/cornerstone/src/utils/interleaveTopToBottom.ts index a0d1e4fb6..fc5447a42 100644 --- a/extensions/cornerstone/src/utils/interleaveTopToBottom.ts +++ b/extensions/cornerstone/src/utils/interleaveTopToBottom.ts @@ -75,23 +75,27 @@ export default function interleaveTopToBottom({ * listen to it and as the other viewports are created we can set the volumes for them * since volumes are already started loading. */ - if (filteredMatchDetails.length !== viewportIdVolumeInputArrayMap.size) { + const uniqueViewportVolumeDisplaySetUIDs = new Set(); + viewportIdVolumeInputArrayMap.forEach((volumeInputArray, viewportId) => { + volumeInputArray.forEach(volumeInput => { + const { volumeId } = volumeInput; + uniqueViewportVolumeDisplaySetUIDs.add(volumeId); + }); + }); + + const uniqueMatchedDisplaySetUIDs = new Set(); + + matchDetails.forEach(matchDetail => { + const { displaySetsInfo } = matchDetail; + displaySetsInfo.forEach(({ displaySetInstanceUID }) => { + uniqueMatchedDisplaySetUIDs.add(displaySetInstanceUID); + }); + }); + + if (uniqueViewportVolumeDisplaySetUIDs.size !== uniqueMatchedDisplaySetUIDs.size) { return; } - // Check if all the matched volumes are loaded - for (const [_, details] of displaySetsMatchDetails.entries()) { - const { SeriesInstanceUID, displaySetInstanceUID } = details; - - // HangingProtocol has matched, but don't have all the volumes created yet, so return - if ( - displaySetsToLoad.has(displaySetInstanceUID) && - !Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID) - ) { - return; - } - } - const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice(); // get volumes from cache const volumes = volumeIds.map(volumeId => { diff --git a/extensions/default/src/CustomizableContextMenu/defaultContextMenu.ts b/extensions/default/src/CustomizableContextMenu/defaultContextMenu.ts index 29a760c79..486530a09 100644 --- a/extensions/default/src/CustomizableContextMenu/defaultContextMenu.ts +++ b/extensions/default/src/CustomizableContextMenu/defaultContextMenu.ts @@ -12,6 +12,9 @@ const defaultContextMenu = { commands: [ { commandName: 'deleteMeasurement', + // we only have support for cornerstoneTools context menu since + // they are svg based + context: 'CORNERSTONE', }, ], }, diff --git a/extensions/default/src/Panels/PanelMeasurementTable.tsx b/extensions/default/src/Panels/PanelMeasurementTable.tsx index 57e8decb8..7cb491660 100644 --- a/extensions/default/src/Panels/PanelMeasurementTable.tsx +++ b/extensions/default/src/Panels/PanelMeasurementTable.tsx @@ -227,7 +227,7 @@ export default function PanelMeasurementTable({