feat(toolbar): new Toolbar to enable reactive state synchronization (#3983)

This commit is contained in:
Alireza authored and GitHub committed 2024-03-27 16:01:32 -04:00
1 parent 79d5c36bda
commit 566b25a544
155 files changed
+6311 -6577

No files matched your search

@@ -109,6 +109,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIndex,
onReady,
} = props;
const viewportId = viewportOptions.viewportId;
@@ -184,6 +185,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
if (onElementEnabled) {
onElementEnabled(evt);
onReady?.(evt);
}
},
[viewportId, onElementEnabled, toolGroupService]
@@ -2,21 +2,25 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { InstanceMetadata } from 'platform/core/src/types';
import { ServicesManager } from '@ohif/core';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
import { ViewportOverlay } from '@ohif/ui';
import { ServicesManager } from '@ohif/core';
import { InstanceMetadata } from '@ohif/core/src/types';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import './CustomizableViewportOverlay.css';
const EPSILON = 1e-4;
type ViewportData = StackViewportData | VolumeViewportData;
interface OverlayItemProps {
element: any;
viewportData: any;
element: HTMLElement;
viewportData: ViewportData;
imageSliceData: ImageSliceData;
servicesManager: ServicesManager;
viewportId: string;
instance: InstanceMetadata;
customization: any;
formatters: {
@@ -35,67 +39,11 @@ interface OverlayItemProps {
scale?: number;
}
/**
* Window Level / Center Overlay item
*/
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi;
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 shrink-0 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1 shrink-0">L:</span>
<span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}
/**
* Zoom Level Overlay item
*/
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
/**
* Instance Number Overlay Item
*/
function InstanceNumberOverlayItem({
instanceNumber,
imageSliceData,
customization,
}: OverlayItemProps) {
const { imageIndex, numberOfSlices } = imageSliceData;
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">I:</span>
<span className="font-light">
{instanceNumber !== undefined && instanceNumber !== null
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}
const OverlayItemComponents = {
'ohif.overlayItem.windowLevel': VOIOverlayItem,
'ohif.overlayItem.zoomLevel': ZoomOverlayItem,
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
};
/**
* Customizable Viewport Overlay
@@ -106,12 +54,17 @@ function CustomizableViewportOverlay({
imageSliceData,
viewportId,
servicesManager,
}: {
element: HTMLElement;
viewportData: ViewportData;
imageSliceData: ImageSliceData;
viewportId: string;
servicesManager: ServicesManager;
}) {
const { toolbarService, cornerstoneViewportService, customizationService } =
const { cornerstoneViewportService, customizationService, toolGroupService } =
servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
const { imageIndex } = imageSliceData;
const topLeftCustomization = customizationService.getModeCustomization(
@@ -127,27 +80,18 @@ function CustomizableViewportOverlay({
'cornerstoneOverlayBottomRight'
);
const instance = useMemo(() => {
if (viewportData != null) {
return _getViewportInstance(viewportData, imageIndex);
} else {
return null;
}
}, [viewportData, imageIndex]);
const instance = useMemo(
() => (viewportData ? getViewportInstance(viewportData, imageIndex) : null),
[viewportData, imageIndex]
);
const instanceNumber = useMemo(() => {
if (viewportData != null) {
return _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService);
}
return null;
}, [viewportData, viewportId, imageIndex, cornerstoneViewportService]);
/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(toolbarService.getActiveTools());
}, []);
const instanceNumber = useMemo(
() =>
viewportData
? getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService)
: null,
[viewportData, viewportId, imageIndex, cornerstoneViewportService]
);
/**
* Updating the VOI when the viewport changes its voi
@@ -215,26 +159,9 @@ function CustomizableViewportOverlay({
};
}, [viewportId, viewportData, cornerstoneViewportService, element]);
/**
* Updating the active tools when the toolbar changes
*/
// Todo: this should act on the toolGroups instead of the toolbar state
useEffect(() => {
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
setActiveTools(toolbarService.getActiveTools());
}
);
return () => {
unsubscribe();
};
}, [toolbarService]);
const _renderOverlayItem = useCallback(
item => {
const overlayItemProps: OverlayItemProps = {
const overlayItemProps = {
element,
viewportData,
imageSliceData,
@@ -242,24 +169,26 @@ function CustomizableViewportOverlay({
servicesManager,
customization: item,
formatters: {
formatPN: formatPN,
formatPN,
formatDate: formatDICOMDate,
formatTime: formatDICOMTime,
formatNumberPrecision: formatNumberPrecision,
formatNumberPrecision,
},
instance,
// calculated
voi,
scale,
instanceNumber,
};
if (item.customizationType === 'ohif.overlayItem.windowLevel') {
return <VOIOverlayItem {...overlayItemProps} />;
} else if (item.customizationType === 'ohif.overlayItem.zoomLevel') {
return <ZoomOverlayItem {...overlayItemProps} />;
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
return <InstanceNumberOverlayItem {...overlayItemProps} />;
if (!item) {
return null;
}
const { customizationType } = item;
const OverlayItemComponent = OverlayItemComponents[customizationType];
if (OverlayItemComponent) {
return <OverlayItemComponent {...overlayItemProps} />;
} else {
const renderItem = customizationService.transform(item);
@@ -282,103 +211,106 @@ function CustomizableViewportOverlay({
]
);
const getTopLeftContent = useCallback(() => {
const items = topLeftCustomization?.items || [
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
];
return (
<>
{items.map((item, i) => (
<div key={`topLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [topLeftCustomization, _renderOverlayItem]);
const getContent = useCallback(
(customization, defaultItems, keyPrefix) => {
const items = customization?.items ?? defaultItems;
const getTopRightContent = useCallback(() => {
const items = topRightCustomization?.items || [
{
id: 'InstanceNmber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
];
return (
<>
{items.map((item, i) => (
<div key={`topRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [topRightCustomization, _renderOverlayItem]);
const getBottomLeftContent = useCallback(() => {
const items = bottomLeftCustomization?.items || [];
return (
<>
{items.map((item, i) => (
<div key={`bottomLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [bottomLeftCustomization, _renderOverlayItem]);
const getBottomRightContent = useCallback(() => {
const items = bottomRightCustomization?.items || [];
return (
<>
{items.map((item, i) => (
<div key={`bottomRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [bottomRightCustomization, _renderOverlayItem]);
return (
<>
{items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}>
{item?.condition
? item.condition()
? _renderOverlayItem(item)
: null
: _renderOverlayItem(item)}
</div>
))}
</>
);
},
[_renderOverlayItem]
);
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
bottomLeft={getBottomLeftContent()}
bottomRight={getBottomRightContent()}
topLeft={
/**
* Inline default overlay items for a more standard expansion
*/
getContent(
topLeftCustomization,
[
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: () => {
const activeToolName = toolGroupService.getActiveToolForViewport(viewportId);
return activeToolName === 'Zoom';
},
},
],
'topLeftOverlayItem'
)
}
topRight={getContent(
topRightCustomization,
[
{
id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
],
'topRightOverlayItem'
)}
bottomLeft={getContent(bottomLeftCustomization, [null], 'bottomLeftOverlayItem')}
bottomRight={getContent(bottomRightCustomization, [null], 'bottomRightOverlayItem')}
/>
);
}
function _getViewportInstance(viewportData, imageIndex) {
const getViewportInstance = (viewportData, imageIndex) => {
const { viewportType, data } = viewportData;
let imageId = null;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
imageId = viewportData.data.imageIds[imageIndex];
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
const volumes = viewportData.data;
if (volumes && volumes.length == 1) {
const volume = volumes[0];
imageId = volume.imageIds[imageIndex];
}
}
return imageId ? metaData.get('instance', imageId) || {} : {};
}
function _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService) {
switch (viewportType) {
case Enums.ViewportType.STACK:
imageId = data.imageIds[imageIndex];
break;
case Enums.ViewportType.ORTHOGRAPHIC:
if (data?.length === 1) {
imageId = data[0].imageIds[imageIndex];
}
break;
default:
break;
}
return imageId ? metaData.get('instance', imageId) || {} : {};
};
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
if (!instanceNumber && instanceNumber !== 0) {
return null;
}
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportId,
cornerstoneViewportService
);
switch (viewportData.viewportType) {
case Enums.ViewportType.STACK:
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
break;
case Enums.ViewportType.ORTHOGRAPHIC:
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
viewportId,
cornerstoneViewportService
);
break;
}
return instanceNumber;
}
return instanceNumber ?? null;
};
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.data.imageIds;
@@ -442,6 +374,68 @@ function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewp
}
}
/**
* Window Level / Center Overlay item
*/
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi;
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 shrink-0 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1 shrink-0">L:</span>
<span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}
/**
* Zoom Level Overlay item
*/
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
/**
* Instance Number Overlay Item
*/
function InstanceNumberOverlayItem({
instanceNumber,
imageSliceData,
customization,
}: OverlayItemProps) {
const { imageIndex, numberOfSlices } = imageSliceData;
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">I:</span>
<span className="font-light">
{instanceNumber !== undefined && instanceNumber !== null
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}
CustomizableViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
@@ -1,278 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui';
import { ServicesManager } from '@ohif/core';
const EPSILON = 1e-4;
function CornerstoneViewportOverlay({
element,
viewportData,
imageSliceData,
viewportId,
servicesManager,
}) {
const { cornerstoneViewportService, toolbarService } = servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(toolbarService.getActiveTools());
}, []);
useEffect(() => {
let isMounted = true;
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
if (!isMounted) {
return;
}
setActiveTools(toolbarService.getActiveTools());
}
);
return () => {
isMounted = false;
unsubscribe();
};
}, []);
/**
* Updating the VOI when the viewport changes its voi
*/
useEffect(() => {
const updateVOI = eventDetail => {
const { range } = eventDetail.detail;
if (!range) {
return;
}
const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
setVOI({ windowCenter, windowWidth });
};
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
};
}, [viewportId, viewportData, voi, element]);
/**
* Updating the scale when the viewport changes its zoom
*/
useEffect(() => {
const updateScale = eventDetail => {
const { previousCamera, camera } = eventDetail.detail;
if (
previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale
) {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
const imageData = viewport.getImageData();
if (!imageData) {
return;
}
if (camera.scale) {
setScale(camera.scale);
return;
}
const { spacing } = imageData;
// convert parallel scale to scale
const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
setScale(scale);
}
};
element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
};
}, [viewportId, viewportData]);
const getTopLeftContent = useCallback(() => {
const { windowWidth, windowCenter } = voi;
if (activeTools.includes('WindowLevel')) {
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div className="flex flex-row text-base">
<span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1">L:</span>
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}
if (activeTools.includes('Zoom')) {
return (
<div className="flex flex-row text-base">
<span className="mr-1">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
return null;
}, [voi, scale, activeTools]);
const getTopRightContent = useCallback(() => {
const { imageIndex, numberOfSlices } = imageSliceData;
if (!viewportData) {
return;
}
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
if (!instanceNumber) {
return null;
}
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportId,
cornerstoneViewportService
);
}
return (
<div className="flex flex-row text-base">
<span className="mr-1">I:</span>
<span className="font-light">
{instanceNumber !== undefined
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}, [imageSliceData, viewportData, viewportId]);
if (!viewportData) {
return null;
}
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
if (!ohifViewport) {
return null;
}
const backgroundColor = ohifViewport.getViewportOptions().background;
// Todo: probably this can be done in a better way in which we identify bright
// background
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
color={isLight && 'text-[#0944B3]'}
/>
);
}
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.data.imageIds;
const imageId = imageIds[imageIndex];
if (!imageId) {
return;
}
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
const stackSize = imageIds.length;
if (stackSize <= 1) {
return;
}
return parseInt(instanceNumber);
}
// Since volume viewports can be in any view direction, they can render
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
function _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportId,
cornerstoneViewportService
) {
const volumes = viewportData.volumes;
// Todo: support fusion of acquisition plane which has instanceNumber
if (!volumes || volumes.length > 1) {
return;
}
const volume = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!cornerstoneViewport) {
return;
}
const camera = cornerstoneViewport.getCamera();
const { viewPlaneNormal } = camera;
// checking if camera is looking at the acquisition plane (defined by the direction on the volume)
const scanAxisNormal = direction.slice(6, 9);
// check if viewPlaneNormal is parallel to scanAxisNormal
const cross = vec3.cross(vec3.create(), viewPlaneNormal, scanAxisNormal);
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
if (isAcquisitionPlane) {
const imageId = imageIds[imageIndex];
if (!imageId) {
return {};
}
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
return parseInt(instanceNumber);
}
}
CornerstoneViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
viewportId: PropTypes.string,
servicesManager: PropTypes.instanceOf(ServicesManager),
};
export default CornerstoneViewportOverlay;
+109 -94
View File
@@ -21,6 +21,12 @@ import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { CornerstoneServices } from './types';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
voi: toggleVOISliceSync,
};
function commandsModule({
servicesManager,
@@ -30,11 +36,11 @@ function commandsModule({
viewportGridService,
toolGroupService,
cineService,
toolbarService,
uiDialogService,
cornerstoneViewportService,
uiNotificationService,
measurementService,
syncGroupService,
} = servicesManager.services as CornerstoneServices;
const { measurementServiceSource } = this;
@@ -227,30 +233,10 @@ function commandsModule({
arrowTextCallback: ({ callback, data }) => {
callInputDialog(uiDialogService, data, callback);
},
cleanUpCrosshairs: () => {
// if the crosshairs tool is active, deactivate it and set window level active
// since we are going back to main non-mpr HP
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
if (activeViewportToolGroup._toolInstances?.Crosshairs?.mode === Enums.ToolModes.Active) {
actions.toolbarServiceRecordInteraction({
interactionType: 'tool',
commands: [
{
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
}
},
toggleCine: () => {
const { viewports } = viewportGridService.getState();
const { isCineEnabled } = cineService.getState();
cineService.setIsCineEnabled(!isCineEnabled);
toolbarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
},
setWindowLevel({ window, level, toolGroupId }) {
@@ -279,35 +265,40 @@ function commandsModule({
});
viewport.render();
},
setToolEnabled: ({ toolName, toggle }) => {
const { viewports } = viewportGridService.getState();
// Just call the toolbar service record interaction - allows
// executing a toolbar command as a full toolbar command with side affects
// coming from the ToolbarService itself.
toolbarServiceRecordInteraction: props => {
toolbarService.recordInteraction(props);
},
// Enable or disable a toggleable command, without calling the activation
// Used to setup already active tools from hanging protocols
setToolbarToggled: props => {
toolbarService.setToggled(props.toolId, props.isActive ?? true);
},
setToolActive: ({ toolName, toolGroupId = null, toggledState }) => {
if (toolName === 'Crosshairs') {
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
uiNotificationService.show({
title: 'Crosshairs',
message:
'You need to be in a MPR view to use Crosshairs. Click on MPR button in the toolbar to activate it.',
type: 'info',
duration: 3000,
});
throw new Error('Crosshairs tool is not available in this viewport');
}
if (!viewports.size) {
return;
}
const toolGroup = toolGroupService.getToolGroup(null);
if (!toolGroup) {
return;
}
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
// Toggle the tool's state only if the toggle is true
if (toggle) {
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
}
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
const toolName = itemId || value;
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
toolGroupIds.forEach(toolGroupId => {
actions.setToolActive({ toolName, toolGroupId });
});
},
setToolActive: ({ toolName, toolGroupId = null }) => {
const { viewports } = viewportGridService.getState();
if (!viewports.size) {
@@ -320,34 +311,13 @@ function commandsModule({
return;
}
if (!toolGroup.getToolInstance(toolName)) {
uiNotificationService.show({
title: `${toolName} tool`,
message: `The ${toolName} tool is not available in this viewport.`,
type: 'info',
duration: 3000,
});
throw new Error(`ToolGroup ${toolGroup.id} does not have this tool.`);
}
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
if (activeToolName) {
// Todo: this is a hack to prevent the crosshairs to stick around
// after another tool is selected. We should find a better way to do this
if (activeToolName === 'Crosshairs') {
toolGroup.setToolDisabled(activeToolName);
} else {
toolGroup.setToolPassive(activeToolName);
}
}
// If there is a toggle state, then simply set the enabled/disabled state without
// setting the tool active.
if (toggledState != null) {
toggledState ? toolGroup.setToolEnabled(toolName) : toolGroup.setToolDisabled(toolName);
return;
const activeToolOptions = toolGroup.getToolConfiguration(activeToolName);
activeToolOptions?.disableOnPassive
? toolGroup.setToolDisabled(activeToolName)
: toolGroup.setToolPassive(activeToolName);
}
// Set the new toolName to be active
@@ -565,33 +535,59 @@ function commandsModule({
(currentIndex + direction + viewportIds.length) % viewportIds.length;
viewportGridService.setActiveViewportId(viewportIds[nextViewportIndex] as string);
},
/**
* If the syncId is given and a synchronizer with that ID already exists, it will
* toggle it on/off for the provided viewports. If not, it will attempt to create
* a new synchronizer using the given syncId and type for the specified viewports.
* If no viewports are provided, you may notice some default behavior.
* - 'voi' type, we will aim to synchronize all viewports with the same modality
* -'imageSlice' type, we will aim to synchronize all viewports with the same orientation.
*
* @param options
* @param options.viewports - The viewports to synchronize
* @param options.syncId - The synchronization group ID
* @param options.type - The type of synchronization to perform
*/
toggleSynchronizer: ({ type, viewports, syncId }) => {
const synchronizer = syncGroupService.getSynchronizer(syncId);
toggleImageSliceSync: ({ toggledState }) => {
toggleImageSliceSync({
servicesManager,
toggledState,
});
if (synchronizer) {
synchronizer.isDisabled() ? synchronizer.setEnabled(true) : synchronizer.setEnabled(false);
return;
}
const fn = toggleSyncFunctions[type];
if (fn) {
fn({
servicesManager,
viewports,
syncId,
});
}
},
setSourceViewportForReferenceLinesTool: ({ toggledState, viewportId }) => {
setSourceViewportForReferenceLinesTool: ({ viewportId }) => {
if (!viewportId) {
const { activeViewportId } = viewportGridService.getState();
viewportId = activeViewportId;
viewportId = activeViewportId ?? 'default';
}
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
toolGroup.setToolConfiguration(
toolGroup?.setToolConfiguration(
ReferenceLinesTool.toolName,
{
sourceViewportId: viewportId,
},
true // overwrite
);
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
storePresentation: ({ viewportId }) => {
cornerstoneViewportService.storePresentation({ viewportId });
},
attachProtocolViewportDataListener: ({ protocol, stageIndex }) => {
const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const command = protocol.callbacks.onViewportDataInitialized;
@@ -608,6 +604,26 @@ function commandsModule({
}
});
},
resetCrosshairs: ({ viewportId }) => {
const crosshairInstances = [];
const getCrosshairInstances = toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
crosshairInstances.push(toolGroup.getToolInstance('Crosshairs'));
};
if (!viewportId) {
const toolGroupIds = toolGroupService.getToolGroupIds();
toolGroupIds.forEach(getCrosshairInstances);
} else {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
getCrosshairInstances(toolGroup.id);
}
crosshairInstances.forEach(ins => {
ins.resetCrosshairs();
});
},
};
const definitions = {
@@ -615,7 +631,6 @@ function commandsModule({
// context menu
showCornerstoneContextMenu: {
commandFn: actions.showCornerstoneContextMenu,
storeContexts: [],
options: {
menuCustomizationId: 'measurementsContextMenu',
commands: [
@@ -631,8 +646,6 @@ function commandsModule({
},
getNearbyAnnotation: {
commandFn: actions.getNearbyAnnotation,
storeContexts: [],
options: {},
},
deleteMeasurement: {
commandFn: actions.deleteMeasurement,
@@ -643,16 +656,18 @@ function commandsModule({
updateMeasurement: {
commandFn: actions.updateMeasurement,
},
setWindowLevel: {
commandFn: actions.setWindowLevel,
},
toolbarServiceRecordInteraction: {
commandFn: actions.toolbarServiceRecordInteraction,
},
setToolActive: {
commandFn: actions.setToolActive,
},
setToolActiveToolbar: {
commandFn: actions.setToolActiveToolbar,
},
setToolEnabled: {
commandFn: actions.setToolEnabled,
},
rotateViewportCW: {
commandFn: actions.rotateViewport,
options: { rotation: 90 },
@@ -735,15 +750,15 @@ function commandsModule({
storePresentation: {
commandFn: actions.storePresentation,
},
setToolbarToggled: {
commandFn: actions.setToolbarToggled,
},
cleanUpCrosshairs: {
commandFn: actions.cleanUpCrosshairs,
},
attachProtocolViewportDataListener: {
commandFn: actions.attachProtocolViewportDataListener,
},
resetCrosshairs: {
commandFn: actions.resetCrosshairs,
},
toggleSynchronizer: {
commandFn: actions.toggleSynchronizer,
},
};
return {
@@ -1,38 +1,18 @@
import React, { useCallback, useEffect, useState } from 'react';
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { CinePlayer, useCine } from '@ohif/ui';
import { Enums, eventTarget } from '@cornerstonejs/core';
import { useAppConfig } from '@state';
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
const {
toolbarService,
customizationService,
displaySetService,
viewportGridService,
cineService,
} = servicesManager.services;
const [{ isCineEnabled, cines }] = useCine();
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
const [{ isCineEnabled, cines }, api] = useCine();
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
const [appConfig] = useAppConfig();
const isMountedRef = useRef(null);
const { component: CinePlayerComponent = CinePlayer } =
customizationService.get('cinePlayer') ?? {};
const handleCineClose = () => {
toolbarService.recordInteraction({
groupId: 'MoreTools',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleCine',
commandOptions: {},
toolName: 'cine',
context: 'CORNERSTONE',
},
],
});
};
const cineHandler = () => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
return;
@@ -45,11 +25,11 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(enabledVPElement, {
api.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(enabledVPElement);
api.stopClip(enabledVPElement);
}
};
@@ -70,34 +50,36 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
});
if (isPlaying) {
cineService.setIsCineEnabled(isPlaying);
api.setIsCineEnabled(isPlaying);
}
cineService.setCine({ id: viewportId, isPlaying, frameRate });
api.setCine({ id: viewportId, isPlaying, frameRate });
setNewStackFrameRate(frameRate);
}, [cineService, displaySetService, viewportId, viewportGridService, cines]);
}, [displaySetService, viewportId, viewportGridService, cines]);
useEffect(() => {
isMountedRef.current = true;
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
return () => {
cineService.setCine({ id: viewportId, isPlaying: false });
isMountedRef.current = false;
api.stopClip(enabledVPElement);
api.setCine({ id: viewportId, isPlaying: false });
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
};
}, [enabledVPElement, newStackCineHandler]);
useEffect(() => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
return;
}
cineHandler();
return () => {
if (enabledVPElement && cines?.[viewportId]?.isPlaying) {
cineService.stopClip(enabledVPElement);
}
api.stopClip(enabledVPElement);
};
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
}, [cines, viewportId, enabledVPElement, cineHandler]);
const cine = cines[viewportId];
const isPlaying = (cine && cine.isPlaying) || false;
@@ -108,15 +90,22 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
className="absolute left-1/2 bottom-3 -translate-x-1/2"
frameRate={newStackFrameRate}
isPlaying={isPlaying}
onClose={handleCineClose}
onPlayPauseChange={isPlaying =>
cineService.setCine({
onClose={() => {
// also stop the clip
api.setCine({
id: viewportId,
isPlaying: false,
});
api.setIsCineEnabled(false);
}}
onPlayPauseChange={isPlaying => {
api.setCine({
id: viewportId,
isPlaying,
})
}
});
}}
onFrameRateChange={frameRate =>
cineService.setCine({
api.setCine({
id: viewportId,
frameRate,
})
@@ -0,0 +1,251 @@
import { Enums } from '@cornerstonejs/tools';
const getToggledClassName = (isToggled: boolean) => {
return isToggled
? '!text-primary-active'
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
};
export default function getToolbarModule({ commandsManager, servicesManager }) {
const {
toolGroupService,
syncGroupService,
cornerstoneViewportService,
hangingProtocolService,
displaySetService,
viewportGridService,
} = servicesManager.services;
return [
// functions/helpers to be used by the toolbar buttons to decide if they should
// enabled or not
{
name: 'evaluate.cornerstoneTool',
evaluate: ({ viewportId, button }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = getToolNameForButton(button);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
};
}
const isPrimaryActive = toolGroup.getActivePrimaryMouseButtonTool() === toolName;
return {
disabled: false,
className: isPrimaryActive
? '!text-black bg-primary-light'
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light',
// Todo: isActive right now is used for nested buttons where the primary
// button needs to be fully rounded (vs partial rounded) when active
// otherwise it does not have any other use
isActive: isPrimaryActive,
};
},
},
{
name: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
evaluate: ({ viewportId, button, itemId }) => {
const { items } = button.props;
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return {
primary: button.props.primary,
items,
};
}
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
// 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);
return toolName === activeToolName;
});
// if there is an active tool in the items dropdown bound to the primary mouse/touch
// we should show that no matter what
if (activeToolIndex > -1) {
return {
primary: items[activeToolIndex],
items,
};
}
if (!itemId) {
return {
primary: button.props.primary,
items,
};
}
// other wise we can move the clicked tool to the primary button
const clickedItemProps = items.find(item => item.id === itemId || item.itemId === itemId);
return {
primary: clickedItemProps,
items,
};
},
},
{
name: 'evaluate.action',
evaluate: ({ viewportId, button }) => {
return {
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
};
},
},
{
name: 'evaluate.cornerstoneTool.toggle',
evaluate: ({ viewportId, button }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = getToolNameForButton(button);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
};
}
const isOff = [Enums.ToolModes.Disabled, Enums.ToolModes.Passive].includes(
toolGroup.getToolOptions(toolName).mode
);
return {
className: getToggledClassName(!isOff),
};
},
},
{
name: 'evaluate.cornerstone.synchronizer',
evaluate: ({ viewportId, button }) => {
let synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
if (!synchronizers?.length) {
return {
className: getToggledClassName(false),
};
}
const synchronizerType = button?.commands?.[0]?.commandOptions?.type;
synchronizers = syncGroupService.getSynchronizersOfType(synchronizerType);
if (!synchronizers?.length) {
return {
className: getToggledClassName(false),
};
}
// Todo: we need a better way to find the synchronizers based on their
// type, but for now we just check the first one and see if it is
// enabled
const synchronizer = synchronizers[0];
const isEnabled = synchronizer?._enabled;
return {
className: getToggledClassName(isEnabled),
};
},
},
{
name: 'evaluate.viewportProperties.toggle',
evaluate: ({ viewportId, button }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport.isDisabled) {
return;
}
const propId = button.id;
const properties = viewport.getProperties();
const camera = viewport.getCamera();
const prop = camera?.[propId] || properties?.[propId];
if (!prop) {
return {
disabled: false,
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
};
}
const isToggled = prop;
return {
className: getToggledClassName(isToggled),
};
},
},
{
name: 'evaluate.mpr',
evaluate: ({ viewportId, button }) => {
const { protocol } = hangingProtocolService.getActiveProtocol();
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const areReconstructable = displaySets.every(displaySet => {
return displaySet.isReconstructable;
});
if (!areReconstructable) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
};
}
const isMpr = protocol?.id === 'mpr';
return {
disabled: false,
className: getToggledClassName(isMpr),
};
},
},
];
}
function getToolNameForButton(button) {
const { props } = button;
const commands = props?.commands || button.commands;
const commandsArray = Array.isArray(commands) ? commands : [commands];
const firstCommand = commandsArray[0];
if (typeof firstCommand === 'string') {
// likely not a cornerstone tool
return null;
}
if ('commandOptions' in firstCommand) {
return firstCommand.commandOptions.toolName ?? props?.id ?? button.id;
}
// use id as a fallback for toolName
return props?.id ?? button.id;
}
-5
View File
@@ -22,11 +22,6 @@ export const mpr: Types.HangingProtocol.Protocol = {
},
],
// Turns off crosshairs when switching out of MPR mode
onProtocolExit: [
{
commandName: 'cleanUpCrosshairs',
},
],
},
displaySetSelectors: {
activeDisplaySet: {
+2 -1
View File
@@ -13,6 +13,7 @@ import init from './init';
import getCustomizationModule from './getCustomizationModule';
import getCommandsModule from './commandsModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import getToolbarModule from './getToolbarModule';
import ToolGroupService from './services/ToolGroupService';
import SyncGroupService from './services/SyncGroupService';
import SegmentationService from './services/SegmentationService';
@@ -26,7 +27,6 @@ import dicomLoaderService from './utils/dicomLoaderService';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { id } from './id';
import * as csWADOImageLoader from './initWADOImageLoader.js';
import { measurementMappingUtils } from './utils/measurementServiceMappings';
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
@@ -79,6 +79,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
return init.call(this, props);
},
getToolbarModule,
getHangingProtocolModule,
getViewportModule({ servicesManager, commandsManager }) {
const ExtendedOHIFCornerstoneViewport = props => {
+27 -77
View File
@@ -15,7 +15,6 @@ import {
utilities as csUtilities,
Enums as csEnums,
} from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/tools';
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
import initWADOImageLoader from './initWADOImageLoader';
@@ -41,7 +40,6 @@ export default async function init({
servicesManager,
commandsManager,
extensionManager,
configuration,
appConfig,
}: Types.Extensions.ExtensionParams): Promise<void> {
// Note: this should run first before initializing the cornerstone
@@ -94,13 +92,26 @@ export default async function init({
cineService,
cornerstoneViewportService,
hangingProtocolService,
toolGroupService,
toolbarService,
viewportGridService,
stateSyncService,
syncGroupService,
segmentationService,
} = servicesManager.services as CornerstoneServices;
toolbarService.registerEventForToolbarUpdate(cornerstoneViewportService, [
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
]);
toolbarService.registerEventForToolbarUpdate(segmentationService, [
segmentationService.EVENTS.SEGMENTATION_ADDED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
segmentationService.EVENTS.SEGMENTATION_UPDATED,
]);
toolbarService.registerEventForToolbarUpdate(eventTarget, [
cornerstoneTools.Enums.Events.TOOL_ACTIVATED,
]);
window.services = servicesManager.services;
window.extensionManager = extensionManager;
window.commandsManager = commandsManager;
@@ -225,47 +236,6 @@ export default async function init({
commandsManager,
});
/**
* When a viewport gets a new display set, this call will go through all the
* active tools in the toolbar, and call any commands registered in the
* toolbar service with a callback to re-enable on displaying the viewport.
*/
const toolbarEventListener = evt => {
const { element } = evt.detail;
const activeTools = toolbarService.getActiveTools();
activeTools.forEach(tool => {
const toolData = toolbarService.getNestedButton(tool);
const commands = toolData?.listeners?.[evt.type];
commandsManager.run(commands, { element, evt });
});
};
/** Listens for active viewport events and fires the toolbar listeners */
const activeViewportEventListener = evt => {
const { viewportId } = evt;
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
const activeTools = toolbarService.getActiveTools();
activeTools.forEach(tool => {
if (!toolGroup?._toolInstances?.[tool]) {
return;
}
// check if tool is active on the new viewport
const toolEnabled = toolGroup._toolInstances[tool].mode === Enums.ToolModes.Enabled;
if (!toolEnabled) {
return;
}
const button = toolbarService.getNestedButton(tool);
const commands = button?.listeners?.[evt.type];
commandsManager.run(commands, { viewportId, evt });
});
};
/**
* Runs error handler for failed requests.
* @param event
@@ -275,30 +245,6 @@ export default async function init({
handler(detail.error);
};
const resetCrosshairs = evt => {
const { element } = evt.detail;
const { viewportId, renderingEngineId } = cornerstone.getEnabledElement(element);
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
);
if (!toolGroup || !toolGroup._toolInstances?.['Crosshairs']) {
return;
}
const mode = toolGroup._toolInstances['Crosshairs'].mode;
if (mode === Enums.ToolModes.Active) {
toolGroup.setToolActive('Crosshairs');
} else if (mode === Enums.ToolModes.Passive) {
toolGroup.setToolPassive('Crosshairs');
} else if (mode === Enums.ToolModes.Enabled) {
toolGroup.setToolEnabled('Crosshairs');
}
};
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
const { element } = evt.detail;
cornerstoneTools.utilities.stackContextPrefetch.enable(element);
@@ -309,17 +255,21 @@ export default async function init({
function elementEnabledHandler(evt) {
const { element } = evt.detail;
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
element.addEventListener(EVENTS.CAMERA_RESET, evt => {
const { element } = evt.detail;
const { viewportId } = getEnabledElement(element);
commandsManager.runCommand('resetCrosshairs', { viewportId });
});
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
// eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
initViewTiming({ element, eventTarget });
initViewTiming({ element });
}
function elementDisabledHandler(evt) {
const { element } = evt.detail;
element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
// element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
// TODO - consider removing the callback when all elements are gone
// eventTarget.removeEventListener(
@@ -332,10 +282,10 @@ export default async function init({
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
activeViewportEventListener
);
// viewportGridService.subscribe(
// viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
// activeViewportEventListener
// );
}
function CPUModal() {
@@ -962,7 +962,7 @@ class SegmentationService extends PubSubService {
// Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so
// it is easily compressible in worker thread.
await volumeLoader.createAndCacheDerivedVolume(volumeId, {
await volumeLoader.createAndCacheDerivedSegmentationVolume(volumeId, {
volumeId: segmentationId,
targetBuffer: {
type: 'Uint8Array',
@@ -1368,8 +1368,6 @@ class SegmentationService extends PubSubService {
public setConfiguration = (configuration: SegmentationConfig): void => {
const {
brushSize,
brushThresholdGate,
fillAlpha,
fillAlphaInactive,
outlineWidthActive,
@@ -1461,7 +1459,6 @@ class SegmentationService extends PubSubService {
segmentInfo.label = label;
if (suppressEvents === false) {
// this._setSegmentationModified(segmentationId);
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
segmentation,
});
@@ -1,4 +1,5 @@
import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs/tools';
import { getRenderingEngines } from '@cornerstonejs/core';
import { pubSubServiceInterface, Types, ServicesManager } from '@ohif/core';
@@ -25,6 +26,7 @@ const POSITION = 'cameraposition';
const VOI = 'voi';
const ZOOMPAN = 'zoompan';
const STACKIMAGE = 'stackimage';
const IMAGE_SLICE = 'imageslice';
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
@@ -45,9 +47,14 @@ export default class SyncGroupService {
[POSITION]: synchronizers.createCameraPositionSynchronizer,
[VOI]: synchronizers.createVOISynchronizer,
[ZOOMPAN]: synchronizers.createZoomPanSynchronizer,
// todo: remove stack image since it is legacy now and the image_slice
// handles both stack and volume viewports
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
[IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer,
};
synchronizersByType: { [key: string]: Synchronizer[] } = {};
constructor(serviceManager: ServicesManager) {
this.servicesManager = serviceManager;
this.listeners = {};
@@ -57,14 +64,27 @@ export default class SyncGroupService {
}
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
// Initialize if not already done
this.synchronizersByType[type] = this.synchronizersByType[type] || [];
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
if (syncCreator) {
return syncCreator(id, options);
const synchronizer = syncCreator(id, options);
if (synchronizer) {
this.synchronizersByType[type].push(synchronizer);
return synchronizer;
}
} else {
console.warn('Unknown synchronizer type', type, id);
console.warn(`Unknown synchronizer type: ${type}, id: ${id}`);
}
}
public getSyncCreatorForType(type: string): SyncCreator {
return this.synchronizerCreators[type.toLowerCase()];
}
/**
* Creates a synchronizer type.
* @param type is the type of the synchronizer to create
@@ -78,6 +98,15 @@ export default class SyncGroupService {
return SynchronizerManager.getSynchronizer(id);
}
/**
* Retrieves an array of synchronizers of a specific type.
* @param type - The type of synchronizers to retrieve.
* @returns An array of synchronizers of the specified type.
*/
public getSynchronizersOfType(type: string): Synchronizer[] {
return this.synchronizersByType[type];
}
protected _getOrCreateSynchronizer(
type: string,
id: string,
@@ -125,14 +154,17 @@ export default class SyncGroupService {
SynchronizerManager.destroy();
}
public getSynchronizersForViewport(
viewportId: string,
renderingEngineId: string
): Synchronizer[] {
return SynchronizerManager.getAllSynchronizers().filter(
public getSynchronizersForViewport(viewportId: string): Synchronizer[] {
const renderingEngine =
getRenderingEngines().find(re => {
return re.getViewports().find(vp => vp.id === viewportId);
}) || getRenderingEngines()[0];
const synchronizers = SynchronizerManager.getAllSynchronizers();
return synchronizers.filter(
s =>
s.hasSourceViewport(renderingEngineId, viewportId) ||
s.hasTargetViewport(renderingEngineId, viewportId)
s.hasSourceViewport(renderingEngine.id, viewportId) ||
s.hasTargetViewport(renderingEngine.id, viewportId)
);
}
@@ -189,19 +189,6 @@ export default class ToolGroupService {
this.addToolsToToolGroup(toolGroupId, tools);
return toolGroup;
}
/**
private changeConfigurationIfNecessary(toolGroup, volumeUID) {
// handle specific assignment for volumeUID (e.g., fusion)
const toolInstances = toolGroup._toolInstances;
// Object.values(toolInstances).forEach(toolInstance => {
// if (toolInstance.configuration) {
// toolInstance.configuration.volumeUID = volumeUID;
// }
// });
}
*/
/**
* Get the tool's configuration based on the tool name and tool group id
* @param toolGroupId - The id of the tool group that the tool instance belongs to.
@@ -234,7 +234,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
const { viewPlaneNormal, viewUp } = csViewport.getCamera();
const initialImageIndex = csViewport.getCurrentImageIdIndex();
const initialImageIndex = csViewport.getCurrentImageIdIndex() || 0;
const zoom = csViewport.getZoom();
const pan = csViewport.getPan();
@@ -347,10 +347,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { stateSyncService, syncGroupService } = this.servicesManager.services;
const synchronizers = syncGroupService.getSynchronizersForViewport(
viewportId,
this.renderingEngine.id
);
const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
const { positionPresentationStore, synchronizersStore, lutPresentationStore } =
stateSyncService.getState();
@@ -189,7 +189,7 @@ const CornerstoneViewportDownloadForm = ({
// add the viewport to the toolGroup
toolGroup.addViewport(downloadViewportId, renderingEngineId);
Object.keys(toolGroup._toolInstances).forEach(toolName => {
Object.keys(toolGroup.getToolInstances()).forEach(toolName => {
// make all tools Enabled so that they can not be interacted with
// in the download viewport
if (toggle && toolName !== 'Crosshairs') {
@@ -1,20 +1,35 @@
const IMAGE_SLICE_SYNC_NAME = 'IMAGE_SLICE_SYNC';
export default function toggleImageSliceSync({
toggledState,
servicesManager,
viewports: providedViewports,
syncId,
}) {
if (!toggledState) {
return disableSync(IMAGE_SLICE_SYNC_NAME, servicesManager);
}
const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } =
servicesManager.services;
syncId ||= IMAGE_SLICE_SYNC_NAME;
const viewports =
providedViewports || getReconstructableStackViewports(viewportGridService, displaySetService);
// Todo: right now we don't have a proper way to define specific
// viewports to add to synchronizers, and right now it is global or not
// after we do that, we should do fine grained control of the synchronizers
const someViewportHasSync = viewports.some(viewport => {
const syncStates = syncGroupService.getSynchronizersForViewport(
viewport.viewportOptions.viewportId
);
const imageSync = syncStates.find(syncState => syncState.id === syncId);
return !!imageSync;
});
if (someViewportHasSync) {
return disableSync(syncId, servicesManager);
}
// create synchronization group and add the viewports to it.
viewports.forEach(gridViewport => {
const { viewportId } = gridViewport.viewportOptions;
@@ -23,8 +38,8 @@ export default function toggleImageSliceSync({
return;
}
syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, {
type: 'stackimage',
id: IMAGE_SLICE_SYNC_NAME,
type: 'imageSlice',
id: syncId,
source: true,
target: true,
});
@@ -0,0 +1,94 @@
const VOI_SYNC_NAME = 'VOI_SYNC';
const getSyncId = modality => `${VOI_SYNC_NAME}_${modality}`;
export default function toggleVOISliceSync({
servicesManager,
viewports: providedViewports,
syncId,
}) {
const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } =
servicesManager.services;
const viewports =
providedViewports || groupViewportsByModality(viewportGridService, displaySetService);
// Todo: right now we don't have a proper way to define specific
// viewports to add to synchronizers, and right now it is global or not
// after we do that, we should do fine grained control of the synchronizers
// we can apply voi sync within each modality group
for (const [modality, modalityViewports] of Object.entries(viewports)) {
const syncIdToUse = syncId || getSyncId(modality);
const someViewportHasSync = modalityViewports.some(viewport => {
const syncStates = syncGroupService.getSynchronizersForViewport(
viewport.viewportOptions.viewportId
);
const imageSync = syncStates.find(syncState => syncState.id === syncIdToUse);
return !!imageSync;
});
if (someViewportHasSync) {
return disableSync(modalityViewports, syncIdToUse, servicesManager);
}
// create synchronization group and add the modalityViewports to it.
modalityViewports.forEach(gridViewport => {
const { viewportId } = gridViewport.viewportOptions;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, {
type: 'voi',
id: syncIdToUse,
source: true,
target: true,
});
});
}
}
function disableSync(modalityViewports, syncId, servicesManager) {
const { syncGroupService, cornerstoneViewportService } = servicesManager.services;
const viewports = modalityViewports;
viewports.forEach(gridViewport => {
const { viewportId } = gridViewport.viewportOptions;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
syncGroupService.removeViewportFromSyncGroup(
viewport.id,
viewport.getRenderingEngine().id,
syncId
);
});
}
function groupViewportsByModality(viewportGridService, displaySetService) {
let { viewports } = viewportGridService.getState();
viewports = [...viewports.values()];
// group the viewports by modality
return viewports.reduce((acc, viewport) => {
const { displaySetInstanceUIDs } = viewport;
// Todo: add proper fusion support
const displaySetInstanceUID = displaySetInstanceUIDs[0];
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const modality = displaySet.Modality;
if (!acc[modality]) {
acc[modality] = [];
}
acc[modality].push(viewport);
return acc;
}, {});
}