fix(toolbox): Preserve user-specified tool state and streamline command execution (#4063)

This commit is contained in:
Alireza 2024-04-29 11:58:03 -04:00 committed by GitHub
parent 4784638717
commit f1a736d193
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 332 additions and 181 deletions

View File

@ -47,7 +47,7 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-volume-rendering.webp?raw=true" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-pdf.webp?raw=true" alt="PDF" width="350"/> | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-rtstruct.webp?raw=true" alt="RTSTRUCT" width="350"/> | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4D.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4d.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-video.webp?raw=true" alt="VIDEO" width="350"/> | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/microscopy.webp?raw=true" alt="microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) |

View File

@ -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"
}
}

View File

@ -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;
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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",

View File

@ -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

View File

@ -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 });
}

View File

@ -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]);

View File

@ -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;
}

View File

@ -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;

View File

@ -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 => {

View File

@ -114,6 +114,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
unit: modalityUnit,
mean,
stdDev,
metadata,
max,
area,
areaUnit,

View File

@ -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",

View File

@ -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',

View File

@ -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',

View File

@ -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)',

View File

@ -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": {

View File

@ -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",

View File

@ -125,11 +125,12 @@ function Local({ modePath }: LocalProps) {
>
<div className="flex h-screen w-screen items-center justify-center ">
<div className="bg-secondary-dark mx-auto space-y-2 rounded-lg py-8 px-8 drop-shadow-md">
<img
className="mx-auto block h-14"
src="./ohif-logo.svg"
alt="OHIF"
/>
<div className="flex items-center justify-center">
<Icon
name="logo-dark-background"
className="h-28"
/>
</div>
<div className="space-y-2 pt-4 text-center">
{dropInitiated ? (
<div className="flex flex-col items-center justify-center pt-48">

View File

@ -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"

View File

@ -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);
}
}
});

View File

@ -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

View File

@ -44,7 +44,7 @@ Key features:
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-volume-rendering.webp?raw=true" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-pdf.webp?raw=true" alt="PDF" width="350"/> | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-rtstruct.webp?raw=true" alt="RTSTRUCT" width="350"/> | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4D.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4d.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-video.webp?raw=true" alt="VIDEO" width="350"/> | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/microscopy.webp?raw=true" alt="microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -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)',

View File

@ -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' },
],
},
]);
```

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -103,7 +103,7 @@ const renderDoubleRangeSetting = option => {
key={option.id}
>
<InputDoubleRange
values={option.values}
values={option.value}
onChange={option.commands}
minValue={option.min}
maxValue={option.max}

View File

@ -34,6 +34,7 @@ import link from './../../assets/icons/tool-stack-image-sync.svg';
import listBullets from './../../assets/icons/list-bullets.svg';
import lock from './../../assets/icons/lock.svg';
import logoOhifSmall from './../../assets/icons/logo-ohif-small.svg';
import logoDarkBackGround from './../../assets/icons/ohif-logo-color-darkbg.svg';
import magnifier from './../../assets/icons/magnifier.svg';
import notificationwarningDiamond from './../../assets/icons/notificationwarning-diamond.svg';
import pencil from './../../assets/icons/pencil.svg';
@ -264,6 +265,7 @@ const ICONS = {
'list-bullets': listBullets,
lock: lock,
'logo-ohif-small': logoOhifSmall,
'logo-dark-background': logoDarkBackGround,
magnifier: magnifier,
exclamation: exclamation,
'notificationwarning-diamond': notificationwarningDiamond,

View File

@ -21,6 +21,7 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ...
});
const prevButtonIdsRef = useRef();
const prevToolboxStateRef = useRef();
useEffect(() => {
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);

View File

@ -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,

View File

@ -78,6 +78,13 @@ const Tooltip = ({
handleMouseOutDebounced();
};
useEffect(() => {
return () => {
handleMouseOverDebounced.cancel();
handleMouseOutDebounced.cancel();
};
}, [handleMouseOverDebounced, handleMouseOutDebounced]);
useEffect(() => {
if (!isOpen && onHide) {
onHide();

View File

@ -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"