diff --git a/README.md b/README.md index c0b4e950f..c505c9581 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ provided by the Open Health Imaging Foundation (OHIF | Volume Rendering | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) | | PDF | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) | | RTSTRUCT | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) | -| 4D | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) | +| 4D | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) | | VIDEO | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) | | microscopy | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) | diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index ba8314d4c..d64153f02 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.13", - "@cornerstonejs/core": "^1.70.13", - "@kitware/vtk.js": "30.3.3", + "@cornerstonejs/adapters": "^1.70.14", + "@cornerstonejs/core": "^1.70.14", + "@kitware/vtk.js": "30.4.1", "react-color": "^2.19.3" } } diff --git a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts index 8bfe0c1e1..4234cf71c 100644 --- a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts @@ -1,5 +1,5 @@ -export function getToolbarModule({ commandsManager, servicesManager }) { - const { segmentationService, toolGroupService } = servicesManager.services; +export function getToolbarModule({ servicesManager }) { + const { segmentationService, toolbarService, toolGroupService } = servicesManager.services; return [ { name: 'evaluate.cornerstone.segmentation', @@ -20,12 +20,16 @@ export function getToolbarModule({ commandsManager, servicesManager }) { const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); if (!toolGroup) { - return; + return { + disabled: true, + className: '!text-common-bright ohif-disabled', + disabledText: disabledText ?? 'Not available on the current viewport', + }; } - const toolName = getToolNameForButton(button); + const toolName = toolbarService.getToolNameForButton(button); - if (!toolGroup || !toolGroup.hasTool(toolName)) { + if (!toolGroup.hasTool(toolName) && !toolNames) { return { disabled: true, className: '!text-common-bright ohif-disabled', @@ -51,19 +55,3 @@ export function getToolbarModule({ commandsManager, servicesManager }) { }, ]; } - -// 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 (firstCommand?.commandOptions) { - return firstCommand.commandOptions.toolName ?? props?.id ?? button.id; - } - - // use id as a fallback for toolName - return props?.id ?? button.id; -} diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 3413a3f73..589744a26 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.13", - "@cornerstonejs/core": "^1.70.13", - "@cornerstonejs/tools": "^1.70.13", + "@cornerstonejs/adapters": "^1.70.14", + "@cornerstonejs/core": "^1.70.14", + "@cornerstonejs/tools": "^1.70.14", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json index cc790219b..431ee553d 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.13", - "@cornerstonejs/streaming-image-volume-loader": "^1.70.13", - "@cornerstonejs/tools": "^1.70.13", + "@cornerstonejs/core": "^1.70.14", + "@cornerstonejs/streaming-image-volume-loader": "^1.70.14", + "@cornerstonejs/tools": "^1.70.14", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index fe0d74e1c..4db91ed48 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.13", + "@cornerstonejs/dicom-image-loader": "^1.70.14", "@icr/polyseg-wasm": "^0.4.0", "@ohif/core": "3.8.0-beta.92", "@ohif/ui": "3.8.0-beta.92", @@ -55,12 +55,12 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.70.13", - "@cornerstonejs/core": "^1.70.13", - "@cornerstonejs/streaming-image-volume-loader": "^1.70.13", - "@cornerstonejs/tools": "^1.70.13", + "@cornerstonejs/adapters": "^1.70.14", + "@cornerstonejs/core": "^1.70.14", + "@cornerstonejs/streaming-image-volume-loader": "^1.70.14", + "@cornerstonejs/tools": "^1.70.14", "@icr/polyseg-wasm": "^0.4.0", - "@kitware/vtk.js": "30.3.3", + "@kitware/vtk.js": "30.4.1", "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 cd0734ecc..b74bfde6a 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -485,7 +485,10 @@ function _subscribeToJumpToMeasurementEvents( cornerstoneViewportService.getViewportIdToJump( jumpId, measurement.displaySetInstanceUID, - { referencedImageId: measurement.referencedImageId } + { + referencedImageId: + measurement.referencedImageId || measurement.metadata?.referencedImageId, + } ); } if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) { @@ -594,6 +597,9 @@ function _jumpToMeasurement( i => i.SOPInstanceUID === SOPInstanceUID ); + // the index is reversed in the volume viewport + // imageIdIndex = referencedDisplaySet.images.length - 1 - imageIdIndex; + const { viewPlaneNormal: viewportViewPlane } = viewport.getCamera(); // should compare abs for both planes since the direction can be flipped diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx index 2f7b71a45..eb7fe2af3 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx @@ -23,7 +23,7 @@ function CornerstoneImageScrollbar({ if (isCineEnabled) { // on image scrollbar change, stop the CINE if it is playing - cineService.stopClip(element); + cineService.stopClip(element, { viewportId }); cineService.setCine({ id: viewportId, isPlaying: false }); } diff --git a/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx b/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx index 795ec5529..1ab5a47e3 100644 --- a/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx +++ b/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx @@ -66,7 +66,7 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) { } cineService.setCine({ id: viewportId, isPlaying, frameRate }); setNewStackFrameRate(frameRate); - }, [displaySetService, viewportId, viewportGridService, cines, isCineEnabled]); + }, [displaySetService, viewportId, viewportGridService, cines, isCineEnabled, enabledVPElement]); useEffect(() => { isMountedRef.current = true; @@ -78,6 +78,14 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) { }; }, [isCineEnabled, newDisplaySetHandler]); + useEffect(() => { + if (!isCineEnabled) { + return; + } + + cineHandler(); + }, [isCineEnabled, cineHandler, enabledVPElement]); + /** * Use effect for handling new display set */ @@ -112,7 +120,7 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) { cineHandler(); return () => { - cineService.stopClip(enabledVPElement); + cineService.stopClip(enabledVPElement, { viewportId }); }; }, [cines, viewportId, cineService, enabledVPElement, cineHandler]); diff --git a/extensions/cornerstone/src/getToolbarModule.tsx b/extensions/cornerstone/src/getToolbarModule.tsx index 2f2de9466..61a6428ed 100644 --- a/extensions/cornerstone/src/getToolbarModule.tsx +++ b/extensions/cornerstone/src/getToolbarModule.tsx @@ -9,6 +9,7 @@ const getToggledClassName = (isToggled: boolean) => { export default function getToolbarModule({ commandsManager, servicesManager }) { const { toolGroupService, + toolbarService, syncGroupService, cornerstoneViewportService, hangingProtocolService, @@ -21,16 +22,16 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { // enabled or not { name: 'evaluate.cornerstoneTool', - evaluate: ({ viewportId, button, disabledText }) => { + evaluate: ({ viewportId, button, toolNames, disabledText }) => { const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); if (!toolGroup) { return; } - const toolName = getToolNameForButton(button); + const toolName = toolbarService.getToolNameForButton(button); - if (!toolGroup || !toolGroup.hasTool(toolName)) { + if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) { return { disabled: true, className: '!text-common-bright ohif-disabled', @@ -38,7 +39,9 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { }; } - const isPrimaryActive = toolGroup.getActivePrimaryMouseButtonTool() === toolName; + const isPrimaryActive = toolNames + ? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool()) + : toolGroup.getActivePrimaryMouseButtonTool() === toolName; return { disabled: false, @@ -71,7 +74,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { // check if the active toolName is part of the items then we need // to move it to the primary button const activeToolIndex = items.findIndex(item => { - const toolName = getToolNameForButton(item); + const toolName = toolbarService.getToolNameForButton(item); return toolName === activeToolName; }); @@ -114,6 +117,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { _evaluateToggle({ viewportId, button, + toolbarService, disabledText, offModes: [Enums.ToolModes.Disabled], toolGroupService, @@ -125,6 +129,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { _evaluateToggle({ viewportId, button, + toolbarService, disabledText, offModes: [Enums.ToolModes.Disabled, Enums.ToolModes.Passive], toolGroupService, @@ -267,13 +272,20 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { ]; } -function _evaluateToggle({ viewportId, button, disabledText, offModes, toolGroupService }) { +function _evaluateToggle({ + viewportId, + toolbarService, + button, + disabledText, + offModes, + toolGroupService, +}) { const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); if (!toolGroup) { return; } - const toolName = getToolNameForButton(button); + const toolName = toolbarService.getToolNameForButton(button); if (!toolGroup.hasTool(toolName)) { return { @@ -289,19 +301,3 @@ function _evaluateToggle({ viewportId, button, disabledText, offModes, toolGroup 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 (firstCommand?.commandOptions) { - return firstCommand.commandOptions.toolName ?? props?.id ?? button.id; - } - - // use id as a fallback for toolName - return props?.id ?? button.id; -} diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index ae58d69f7..9fd4f3ce5 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -532,6 +532,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi cameraProps: unknown ): string { const viewportInfo = this.getViewportInfo(activeViewportId); + + if (viewportInfo.getViewportType() === csEnums.ViewportType.VOLUME_3D) { + return null; + } + const { referencedImageId } = cameraProps; if (viewportInfo?.contains(displaySetInstanceUID, referencedImageId)) { return activeViewportId; diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts index 2ec852492..247b4d3bd 100644 --- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -1,4 +1,10 @@ -import { Types, Enums } from '@cornerstonejs/core'; +import { + Types, + Enums, + getEnabledElementByViewportId, + VolumeViewport, + utilities, +} from '@cornerstonejs/core'; import { Types as CoreTypes } from '@ohif/core'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode'; @@ -89,13 +95,30 @@ const DEFAULT_TOOLGROUP_ID = 'default'; // Return true if the data contains the given display set UID OR the imageId // if it is a composite object. -const dataContains = (data, displaySetUID: string, imageId?: string): boolean => { - if (data.displaySetInstanceUID === displaySetUID) { - return true; - } +const dataContains = ({ data, displaySetUID, imageId, viewport }): boolean => { if (imageId && data.isCompositeStack && data.imageIds) { return !!data.imageIds.find(dataId => dataId === imageId); } + + if (imageId && (data.volumeId || viewport instanceof VolumeViewport)) { + const isAcquisition = !!viewport.getCurrentImageId(); + + if (!isAcquisition) { + return false; + } + + const imageURI = utilities.imageIdToURI(imageId); + const hasImageId = viewport.hasImageURI(imageURI); + + if (hasImageId) { + return true; + } + } + + if (data.displaySetInstanceUID === displaySetUID) { + return true; + } + return false; }; @@ -122,10 +145,20 @@ class ViewportInfo { return false; } + const { viewport } = getEnabledElementByViewportId(this.viewportId) || {}; + if (this.viewportData.data.length) { - return !!this.viewportData.data.find(data => dataContains(data, displaySetUID, imageId)); + return !!this.viewportData.data.find(data => + dataContains({ data, displaySetUID, imageId, viewport }) + ); } - return dataContains(this.viewportData.data, displaySetUID, imageId); + + return dataContains({ + data: this.viewportData.data, + displaySetUID, + imageId, + viewport, + }); } public destroy = (): void => { diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts index 4718e617a..fe94bfa27 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts @@ -114,6 +114,7 @@ function getMappedAnnotations(annotation, DisplaySetService) { unit: modalityUnit, mean, stdDev, + metadata, max, area, areaUnit, diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index acc9ee41d..bb7d0d836 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,8 +32,8 @@ "start": "yarn run dev" }, "peerDependencies": { - "@cornerstonejs/core": "^1.70.13", - "@cornerstonejs/tools": "^1.70.13", + "@cornerstonejs/core": "^1.70.14", + "@cornerstonejs/tools": "^1.70.14", "@ohif/core": "3.8.0-beta.92", "@ohif/extension-cornerstone-dicom-sr": "3.8.0-beta.92", "@ohif/ui": "3.8.0-beta.92", diff --git a/modes/preclinical-4d/src/segmentationButtons.ts b/modes/preclinical-4d/src/segmentationButtons.ts index ecc028a1f..a179dfecc 100644 --- a/modes/preclinical-4d/src/segmentationButtons.ts +++ b/modes/preclinical-4d/src/segmentationButtons.ts @@ -1,16 +1,5 @@ import type { Button } from '@ohif/core/types'; -function _createSetToolActiveCommands(toolName) { - return [ - { - commandName: 'setToolActive', - commandOptions: { - toolName, - }, - }, - ]; -} - const toolbarButtons: Button[] = [ { id: 'BrushTools', @@ -26,7 +15,6 @@ const toolbarButtons: Button[] = [ name: 'evaluate.cornerstone.segmentation', toolNames: ['CircularBrush', 'SphereBrush'], }, - commands: _createSetToolActiveCommands('CircularBrush'), options: [ { name: 'Size (mm)', @@ -62,7 +50,6 @@ const toolbarButtons: Button[] = [ name: 'evaluate.cornerstone.segmentation', toolNames: ['CircularEraser', 'SphereEraser'], }, - commands: _createSetToolActiveCommands('CircularEraser'), options: [ { name: 'Radius (mm)', @@ -98,7 +85,6 @@ const toolbarButtons: Button[] = [ name: 'evaluate.cornerstone.segmentation', toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'], }, - commands: _createSetToolActiveCommands('ThresholdCircularBrush'), options: [ { name: 'Radius (mm)', @@ -156,7 +142,6 @@ const toolbarButtons: Button[] = [ toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'], }, icon: 'icon-tool-shape', - commands: _createSetToolActiveCommands('CircleScissor'), options: [ { name: 'Shape', diff --git a/modes/segmentation/src/segmentationButtons.ts b/modes/segmentation/src/segmentationButtons.ts index 800ea99fd..448267f02 100644 --- a/modes/segmentation/src/segmentationButtons.ts +++ b/modes/segmentation/src/segmentationButtons.ts @@ -1,16 +1,5 @@ import type { Button } from '@ohif/core/types'; -function _createSetToolActiveCommands(toolName) { - return [ - { - commandName: 'setToolActive', - commandOptions: { - toolName, - }, - }, - ]; -} - const toolbarButtons: Button[] = [ { id: 'BrushTools', @@ -27,7 +16,6 @@ const toolbarButtons: Button[] = [ toolNames: ['CircularBrush', 'SphereBrush'], disabledText: 'Create new segmentation to enable this tool.', }, - commands: _createSetToolActiveCommands('CircularBrush'), options: [ { name: 'Radius (mm)', @@ -63,7 +51,6 @@ const toolbarButtons: Button[] = [ name: 'evaluate.cornerstone.segmentation', toolNames: ['CircularEraser', 'SphereEraser'], }, - commands: _createSetToolActiveCommands('CircularEraser'), options: [ { name: 'Radius (mm)', @@ -99,7 +86,6 @@ const toolbarButtons: Button[] = [ name: 'evaluate.cornerstone.segmentation', toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'], }, - commands: _createSetToolActiveCommands('ThresholdCircularBrush'), options: [ { name: 'Radius (mm)', @@ -130,22 +116,29 @@ const toolbarButtons: Button[] = [ { value: 'ThresholdDynamic', label: 'Dynamic' }, { value: 'ThresholdRange', label: 'Range' }, ], - commands: ({ value, commandsManager }) => { + commands: ({ value, commandsManager, options }) => { if (value === 'ThresholdDynamic') { commandsManager.run('setToolActive', { toolName: 'ThresholdCircularBrushDynamic', }); - } else { - commandsManager.run('setToolActive', { - toolName: 'ThresholdCircularBrush', - }); + + return; } + + // check the condition of the threshold-range option + const thresholdRangeOption = options.find( + option => option.id === 'threshold-shape' + ); + + commandsManager.run('setToolActiveToolbar', { + toolName: thresholdRangeOption.value, + }); }, }, { name: 'Shape', type: 'radio', - id: 'eraser-mode', + id: 'threshold-shape', value: 'ThresholdCircularBrush', values: [ { value: 'ThresholdCircularBrush', label: 'Circle' }, @@ -162,7 +155,7 @@ const toolbarButtons: Button[] = [ min: -1000, max: 1000, step: 1, - values: [100, 600], + value: [100, 600], condition: ({ options }) => options.find(option => option.id === 'dynamic-mode').value === 'ThresholdRange', commands: { @@ -187,7 +180,6 @@ const toolbarButtons: Button[] = [ toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'], }, icon: 'icon-tool-shape', - commands: _createSetToolActiveCommands('CircleScissor'), options: [ { name: 'Shape', diff --git a/modes/tmtv/src/toolbarButtons.js b/modes/tmtv/src/toolbarButtons.js index b6a14fc64..b180d974e 100644 --- a/modes/tmtv/src/toolbarButtons.js +++ b/modes/tmtv/src/toolbarButtons.js @@ -8,18 +8,6 @@ const setToolActiveToolbar = { }, }; -function _createSetToolActiveCommands(toolName) { - return [ - { - commandName: 'setToolActiveToolbar', - commandOptions: { - toolName, - toolGroupIds: [toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion], - }, - }, - ]; -} - const toolbarButtons = [ { id: 'MeasurementTools', @@ -144,7 +132,6 @@ const toolbarButtons = [ toolNames: ['CircularBrush', 'SphereBrush'], disabledText: 'Create new segmentation to enable this tool.', }, - commands: _createSetToolActiveCommands('CircularBrush'), options: [ { name: 'Radius (mm)', @@ -180,7 +167,6 @@ const toolbarButtons = [ name: 'evaluate.cornerstone.segmentation', toolNames: ['CircularEraser', 'SphereEraser'], }, - commands: _createSetToolActiveCommands('CircularEraser'), options: [ { name: 'Radius (mm)', @@ -216,7 +202,6 @@ const toolbarButtons = [ name: 'evaluate.cornerstone.segmentation', toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'], }, - commands: _createSetToolActiveCommands('ThresholdCircularBrush'), options: [ { name: 'Radius (mm)', diff --git a/package.json b/package.json index 199db41f3..c52519e0e 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@kitware/vtk.js": "30.3.3", + "@kitware/vtk.js": "30.4.1", "core-js": "^3.2.1" }, "peerDependencies": { diff --git a/platform/app/package.json b/platform/app/package.json index 5037ab84c..4b8ecf809 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -54,7 +54,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^1.70.13", + "@cornerstonejs/dicom-image-loader": "^1.70.14", "@emotion/serialize": "^1.1.3", "@ohif/core": "3.8.0-beta.92", "@ohif/extension-cornerstone": "3.8.0-beta.92", diff --git a/platform/app/src/routes/Local/Local.tsx b/platform/app/src/routes/Local/Local.tsx index bea60c268..1804cd7fe 100644 --- a/platform/app/src/routes/Local/Local.tsx +++ b/platform/app/src/routes/Local/Local.tsx @@ -125,11 +125,12 @@ function Local({ modePath }: LocalProps) { >
- OHIF +
+ +
{dropInitiated ? (
diff --git a/platform/core/package.json b/platform/core/package.json index 351817eef..707847105 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -37,7 +37,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.13", + "@cornerstonejs/dicom-image-loader": "^1.70.14", "@ohif/ui": "3.8.0-beta.92", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21" diff --git a/platform/core/src/classes/CommandsManager.ts b/platform/core/src/classes/CommandsManager.ts index 820554ca2..e6ef1e912 100644 --- a/platform/core/src/classes/CommandsManager.ts +++ b/platform/core/src/classes/CommandsManager.ts @@ -204,7 +204,8 @@ export class CommandsManager { // Execute each command in the array let result: unknown; - commands.forEach(({ commandName, commandOptions, context }) => { + commands.forEach(command => { + const { commandName, commandOptions, context } = command; if (commandName) { result = this.runCommand( commandName, @@ -215,7 +216,11 @@ export class CommandsManager { context ); } else { - console.warn('No command name supplied in', toRun); + if (typeof command === 'function') { + result = command(); + } else { + console.warn('No command name supplied in', toRun); + } } }); diff --git a/platform/core/src/services/ToolBarService/ToolbarService.ts b/platform/core/src/services/ToolBarService/ToolbarService.ts index d5a0b2828..a31421f2b 100644 --- a/platform/core/src/services/ToolBarService/ToolbarService.ts +++ b/platform/core/src/services/ToolBarService/ToolbarService.ts @@ -156,7 +156,7 @@ export default class ToolbarService extends PubSubService { const itemId = interaction.itemId ?? interaction.id; interaction.itemId = itemId; - const commands = Array.isArray(interaction.commands) + let commands = Array.isArray(interaction.commands) ? interaction.commands : [interaction.commands]; @@ -170,6 +170,27 @@ export default class ToolbarService extends PubSubService { const commandOptions = { ...options, ...interaction }; + commands = commands.map(command => { + if (typeof command === 'function') { + return () => { + command({ + ...commandOptions, + commandsManager: this._commandsManager, + servicesManager: this._servicesManager, + }); + }; + } + + return command; + }); + + // if still no commands, return + commands = commands.filter(Boolean); + + if (!commands.length) { + return; + } + // Loop through commands and run them with the combined options this._commandsManager.run(commands, commandOptions); @@ -377,6 +398,26 @@ export default class ToolbarService extends PubSubService { ); } + /** + * Retrieves the tool name for a given button. + * @param button - The button object. + * @returns The tool name associated with the button. + */ + getToolNameForButton(button) { + const { props } = button; + + const commands = props?.commands || button.commands; + const commandsArray = Array.isArray(commands) ? commands : [commands]; + const firstCommand = commandsArray[0]; + + if (firstCommand?.commandOptions) { + return firstCommand.commandOptions.toolName ?? props?.id ?? button.id; + } + + // use id as a fallback for toolName + return props?.id ?? button.id; + } + /** * * @param {*} btn diff --git a/platform/docs/docs/README.md b/platform/docs/docs/README.md index f5fb55b90..9b5a6f7d5 100644 --- a/platform/docs/docs/README.md +++ b/platform/docs/docs/README.md @@ -44,7 +44,7 @@ Key features: | Volume Rendering | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) | | PDF | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) | | RTSTRUCT | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) | -| 4D | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) | +| 4D | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) | | VIDEO | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) | | microscopy | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) | diff --git a/platform/docs/docs/assets/img/demo-pdf.webp b/platform/docs/docs/assets/img/demo-pdf.webp index 7c9442730..3119a12b5 100644 Binary files a/platform/docs/docs/assets/img/demo-pdf.webp and b/platform/docs/docs/assets/img/demo-pdf.webp differ diff --git a/platform/docs/docs/platform/extensions/modules/toolbar.md b/platform/docs/docs/platform/extensions/modules/toolbar.md index e10fe36ec..94fdb22fc 100644 --- a/platform/docs/docs/platform/extensions/modules/toolbar.md +++ b/platform/docs/docs/platform/extensions/modules/toolbar.md @@ -91,7 +91,7 @@ Let's look at one of the evaluators (for `evaluate.cornerstoneTool`) return; } - const toolName = getToolNameForButton(button); + const toolName = toolbarService.getToolNameForButton(button); if (!toolGroup || !toolGroup.hasTool(toolName)) { return { @@ -405,7 +405,12 @@ state will get synchronized with the toolbar service automatically. Your toolbox toolbar buttons can have options, this is really useful for advanced tools that require to change some parameters. For example, the brush tool that requires the brush size to change or the mode (2D or 3D). -currently we support three types of options +:::note +Toolbox with options will run the options commands +on the mount of the toolbox component. This is useful for setting the initial state of the toolbox. +::: + +Currently we support three types of options. ### Radio option @@ -423,7 +428,6 @@ three different modes toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'], }, icon: 'icon-tool-shape', - commands: _createSetToolActiveCommands('CircleScissor'), options: [ { name: 'Shape', @@ -456,7 +460,6 @@ We use this for brush radius change toolNames: ['CircularBrush', 'SphereBrush'], disabledText: 'Create new segmentation to enable this tool.', }, - commands: _createSetToolActiveCommands('CircularBrush'), options: [ { name: 'Radius (mm)', diff --git a/platform/docs/docs/platform/services/data/MeasurementService.md b/platform/docs/docs/platform/services/data/MeasurementService.md index f4ecd51f6..f5ede2f14 100644 --- a/platform/docs/docs/platform/services/data/MeasurementService.md +++ b/platform/docs/docs/platform/services/data/MeasurementService.md @@ -160,3 +160,23 @@ const _initMeasurementService = (MeasurementService, DisplaySetService) => { return csToolsVer4MeasurementSource; }; ``` + + +## Auto complete +Use a customization service to add more customizations for measurement labels. Later, when adding a measurement, the user will be prompted to choose from a list of labels. + +```js +customizationService.addModeCustomizations([ + { + id: 'measurementLabels', + labelOnMeasure: true, + exclusive: true, + items: [ + { value: 'Head', label: 'Head' }, + { value: 'Neck', label: 'Neck' }, + { value: 'Knee', label: 'Knee' }, + { value: 'Toe', label: 'Toe' }, + ], + }, +]); +``` diff --git a/platform/ui/src/assets/icons/ohif-logo-color-darkbg.svg b/platform/ui/src/assets/icons/ohif-logo-color-darkbg.svg new file mode 100644 index 000000000..545838127 --- /dev/null +++ b/platform/ui/src/assets/icons/ohif-logo-color-darkbg.svg @@ -0,0 +1,20 @@ + + + ohif-logo-color-darkbg + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/ui/src/components/AdvancedToolbox/ToolSettings.tsx b/platform/ui/src/components/AdvancedToolbox/ToolSettings.tsx index ebf6e2411..0d5d339b5 100644 --- a/platform/ui/src/components/AdvancedToolbox/ToolSettings.tsx +++ b/platform/ui/src/components/AdvancedToolbox/ToolSettings.tsx @@ -103,7 +103,7 @@ const renderDoubleRangeSetting = option => { key={option.id} > { const currentButtonIdsStr = JSON.stringify( @@ -33,11 +34,24 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ... }) ); - if (prevButtonIdsRef.current === currentButtonIdsStr) { + const currentToolBoxStateStr = JSON.stringify( + Object.keys(toolboxState.toolOptions).map(tool => { + const options = toolboxState.toolOptions[tool]; + if (Array.isArray(options)) { + return options?.map(option => `${option.id}-${option.value}`); + } + }) + ); + + if ( + prevButtonIdsRef.current === currentButtonIdsStr && + prevToolboxStateRef.current === currentToolBoxStateStr + ) { return; } prevButtonIdsRef.current = currentButtonIdsStr; + prevToolboxStateRef.current = currentToolBoxStateStr; const initializeOptionsWithEnhancements = toolbarButtons.reduce( (accumulator, toolbarButton) => { @@ -51,11 +65,15 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ... return option; } + const value = + toolboxState.toolOptions?.[parentId]?.find(prop => prop.id === option.id)?.value ?? + option.value; + + const updatedOptions = toolboxState.toolOptions?.[parentId]; + return { ...option, - value: - toolboxState.toolOptions?.[parentId]?.find(prop => prop.id === option.id)?.value ?? - option.value, + value, commands: value => { api.handleToolOptionChange(parentId, option.id, value); @@ -72,10 +90,15 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ... } else if (isObject) { commandsManager.run({ ...command, - commandOptions: { ...command.commandOptions, ...option, value }, + commandOptions: { + ...command.commandOptions, + ...option, + value, + options: updatedOptions, + }, }); } else if (isFunction) { - command({ value, commandsManager, servicesManager }); + command({ value, commandsManager, servicesManager, options: updatedOptions }); } }); }, @@ -101,7 +124,7 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ... ); api.initializeToolOptions(initializeOptionsWithEnhancements); - }, [toolbarButtons, api]); + }, [toolbarButtons, api, toolboxState]); const handleToolOptionChange = (toolName, optionName, newValue) => { api.handleToolOptionChange(toolName, optionName, newValue); diff --git a/platform/ui/src/components/Toolbox/ToolboxUI.tsx b/platform/ui/src/components/Toolbox/ToolboxUI.tsx index 6a102a195..5755de911 100644 --- a/platform/ui/src/components/Toolbox/ToolboxUI.tsx +++ b/platform/ui/src/components/Toolbox/ToolboxUI.tsx @@ -1,9 +1,17 @@ -import React from 'react'; -import { PanelSection, ToolSettings, Tooltip } from '../../components'; +import React, { useEffect, useRef } from 'react'; +import { PanelSection, ToolSettings } from '../../components'; import classnames from 'classnames'; const ItemsPerRow = 4; +function usePrevious(value) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} + /** * Just refactoring from the toolbox component to make it more readable */ @@ -18,6 +26,27 @@ function ToolboxUI(props) { useCollapsedPanel = true, } = props; + const prevToolOptions = usePrevious(activeToolOptions); + + useEffect(() => { + if (!activeToolOptions) { + return; + } + + activeToolOptions.forEach((option, index) => { + const prevOption = prevToolOptions ? prevToolOptions[index] : undefined; + if (!prevOption || option.value !== prevOption.value) { + const isOptionValid = option.condition + ? option.condition({ options: activeToolOptions }) + : true; + if (isOptionValid) { + const { commands } = option; + commands(option.value); + } + } + }); + }, [activeToolOptions]); + const render = () => { return ( <> @@ -34,7 +63,8 @@ function ToolboxUI(props) { const toolClasses = `ml-1 ${isLastRow ? '' : 'mb-2'}`; const onInteraction = ({ itemId, id, commands }) => { - handleToolSelect(itemId || id); + const idToUse = itemId || id; + handleToolSelect(idToUse); props.onInteraction({ itemId, commands, diff --git a/platform/ui/src/components/Tooltip/Tooltip.tsx b/platform/ui/src/components/Tooltip/Tooltip.tsx index 88f5f5ee5..630814cdc 100644 --- a/platform/ui/src/components/Tooltip/Tooltip.tsx +++ b/platform/ui/src/components/Tooltip/Tooltip.tsx @@ -78,6 +78,13 @@ const Tooltip = ({ handleMouseOutDebounced(); }; + useEffect(() => { + return () => { + handleMouseOverDebounced.cancel(); + handleMouseOutDebounced.cancel(); + }; + }, [handleMouseOverDebounced, handleMouseOutDebounced]); + useEffect(() => { if (!isOpen && onHide) { onHide(); diff --git a/yarn.lock b/yarn.lock index 58add2869..c6343a1a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1497,13 +1497,13 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cornerstonejs/adapters@^1.70.13": - version "1.70.13" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.70.13.tgz#195023f5600272f6ee70090b3c61060d13b9cec7" - integrity sha512-PSitPx/A5aP0VhVTC72trwf3CfVGAXYvcAfAvuk3WzSONhCJ6cg5Xsxpl0NtEt7prsT3u60Bgr94Ea+9VJVdog== +"@cornerstonejs/adapters@^1.70.14": + version "1.70.14" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.70.14.tgz#1eba07366a66f4d261f3a1e3769260ccf74660af" + integrity sha512-jqMIpvqDH1CPO6rPa7P9CtINOOinwfZpPU6UAuoV8zSJtwOpwViP0Xp9rx9smSmzCc3l3ZZcQ9Wq1DnuzpgPJw== dependencies: "@babel/runtime-corejs2" "^7.17.8" - "@cornerstonejs/tools" "^1.70.13" + "@cornerstonejs/tools" "^1.70.14" buffer "^6.0.3" dcmjs "^0.29.8" gl-matrix "^3.4.3" @@ -1550,45 +1550,45 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0" integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg== -"@cornerstonejs/core@^1.70.13": - version "1.70.13" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.70.13.tgz#a1976588b93461e2350866ecd761660ee1b74c8d" - integrity sha512-Bi+ErK6rHNx46CVQiUiw/00ipOWdy7EdyC4U0S30oiLE1Jz1PegIniVDogleXtO1jDkTkNnuWnPCoNhpUgNNuQ== +"@cornerstonejs/core@^1.70.14": + version "1.70.14" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.70.14.tgz#f19c9c3da793513b0f6163661f98c633a83ee2b9" + integrity sha512-DrhjlbtGiV9EyHUiGh2RhPzhFQWxZwDCqqkhxBOLl5QiUalByZNyZyN1OHtX0zOyIwVsFw58yvVKYX7s3bpqOw== dependencies: - "@kitware/vtk.js" "30.3.3" + "@kitware/vtk.js" "30.4.1" comlink "^4.4.1" detect-gpu "^5.0.22" gl-matrix "^3.4.3" lodash.clonedeep "4.5.0" -"@cornerstonejs/dicom-image-loader@^1.70.13": - version "1.70.13" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.70.13.tgz#cf2aea8b6e538b6f005106ef6401869abde226d2" - integrity sha512-PXe9/xmv5vq1raREL9qsD3Ci68pWwCpjVxmyIigpV7lOdtziivvMVNyNSDZCjexBpbsB5a0fKcZG4jl82Jz1HA== +"@cornerstonejs/dicom-image-loader@^1.70.14": + version "1.70.14" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.70.14.tgz#3f776bc92b627ec1a80aec0581ee40d06f1ff815" + integrity sha512-bpYf9s/jhTegxCrrgvvtfJoFDOltkEWAKxqaUjt01g7Z6lnkC6IUaH3XoMZy/0n4un28tTk3xVHF1vAw0upCig== dependencies: "@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" "@cornerstonejs/codec-openjpeg" "^1.2.2" "@cornerstonejs/codec-openjph" "^2.4.5" - "@cornerstonejs/core" "^1.70.13" + "@cornerstonejs/core" "^1.70.14" dicom-parser "^1.8.9" pako "^2.0.4" uuid "^9.0.0" -"@cornerstonejs/streaming-image-volume-loader@^1.70.13": - version "1.70.13" - resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.70.13.tgz#77a3751ddc5ab17cff050007c7e3d7e0b8c58c13" - integrity sha512-Pd90y2JFbQ4LriRkkYXXvD3mhIWyiVdGDznhrJN/8ZC+4m5XusOlvOk2Ia/85e+WAgIUlHyDCgOuzewG8of+6w== +"@cornerstonejs/streaming-image-volume-loader@^1.70.14": + version "1.70.14" + resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.70.14.tgz#e46a4152f6b5ac9a1ce7079d765b8ce9bdcc91ef" + integrity sha512-yObvU8CB/2F0gLyeRzM5NIxNLQimNHEfJnXuv/EYNATq+wOeKWle3jfUm9xN/ZTiSp1MrBMaG7g26OlyIjEp4Q== dependencies: - "@cornerstonejs/core" "^1.70.13" + "@cornerstonejs/core" "^1.70.14" comlink "^4.4.1" -"@cornerstonejs/tools@^1.70.13": - version "1.70.13" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.70.13.tgz#fca6abc331841a2ac1a4403b5522cdcc6cacc708" - integrity sha512-Eza/4A2p51YPqZbvP6HwZC0yJCkogsda39aB+Eh5VTPQSqHn+VCBmZ9U9Qi9+0cpaye53A5OhjXAOoR6pZlqRQ== +"@cornerstonejs/tools@^1.70.14": + version "1.70.14" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.70.14.tgz#7083459c3e206c280cf728d44fe9f4ca09874d2b" + integrity sha512-NI4yLC9PNHwa2KO0DqjZET2vZz8VfW095m637gk5E5wojq3dvSJot7w4y+zTCiyaxXjUUoMzc0TJYmmqtdbWPg== dependencies: - "@cornerstonejs/core" "^1.70.13" + "@cornerstonejs/core" "^1.70.14" "@icr/polyseg-wasm" "0.4.0" "@types/offscreencanvas" "2019.7.3" comlink "^4.4.1" @@ -2844,10 +2844,10 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== -"@kitware/vtk.js@30.3.3": - version "30.3.3" - resolved "https://registry.yarnpkg.com/@kitware/vtk.js/-/vtk.js-30.3.3.tgz#348e17fdc896c912eca7036f607d21ef6128bca1" - integrity sha512-es9I5LLlg+TpaIXk5aHSPzyI/YnCI4egHA1cbG98IP7t9W4KODUcJjyrXFAa7aSvfXZ8y2jhD9qrsaEnstEkJA== +"@kitware/vtk.js@30.4.1": + version "30.4.1" + resolved "https://registry.yarnpkg.com/@kitware/vtk.js/-/vtk.js-30.4.1.tgz#ce8a50012e56341d2d01708a32a2ac3afa675b67" + integrity sha512-jBJFm8AyWpJjNFFBadXyvBwegdD9M6WRdxmIb+x/MVpCyA5lEZSMemhiMn71oKsznaEe5Pjv2VDVJWmwK0vhUg== dependencies: "@babel/runtime" "7.22.11" "@types/webxr" "^0.5.5"