diff --git a/README.md b/README.md index c0b4e950f5c..c505c95817d 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 ba8314d4c78..d64153f0295 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 8bfe0c1e16c..4234cf71cf8 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 3413a3f7300..589744a26b1 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 cc790219b14..431ee553d55 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 fe0d74e1c58..4db91ed48b9 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 cd0734ecc81..b74bfde6a24 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 2f7b71a45da..eb7fe2af308 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 795ec552959..1ab5a47e361 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 2f2de94664e..61a6428ed9c 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 ae58d69f737..9fd4f3ce5f2 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 2ec852492aa..247b4d3bd83 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 4718e617af3..fe94bfa27f8 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 acc9ee41d9b..bb7d0d836cb 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 ecc028a1f8a..a179dfecc2d 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 800ea99fd1f..448267f0294 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 b6a14fc6461..b180d974ec8 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 199db41f3fc..c52519e0e8f 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 5037ab84c8a..4b8ecf8095b 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 2e17b0535da..c368b4d91ca 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 351817eef2b..70784710549 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 820554ca2a8..e6ef1e91236 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 d5a0b28283b..a31421f2ba8 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 f5fb55b905c..9b5a6f7d5da 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 7c94427306d..3119a12b553 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 e10fe36ec32..94fdb22fc8e 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 f4ecd51f62c..f5ede2f14f3 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 00000000000..5458381274b --- /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 ebf6e241138..0d5d339b5c9 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 6a102a19512..5755de9114c 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 88f5f5ee51f..630814cdcc5 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 58add28698e..c6343a1a776 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"