feat(ViewportActionMenu): window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering (#3963)

Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
This commit is contained in:
IbrahimandJoe Boccanfuso authored and GitHub committed 2024-04-02 09:35:30 -04:00
1 parent 07301fc020
commit b7f90e3951
164 files changed
+5887 -932

No files matched your search

@@ -10,7 +10,7 @@ import {
utilities as csUtils,
} from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import { Notification, useViewportDialog } from '@ohif/ui';
import { Notification, useViewportDialog, Types as UITypes, AllInOneMenu } from '@ohif/ui';
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
import { setEnabledElement } from '../state';
@@ -21,6 +21,11 @@ import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/
import CornerstoneServices from '../types/CornerstoneServices';
import CinePlayer from '../components/CinePlayer';
import { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
import { useAppConfig } from '@state';
import { LutPresentation, PositionPresentation } from '../types/Presentation';
const STACK = 'stack';
@@ -116,6 +121,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef();
const [appConfig] = useAppConfig();
const {
measurementService,
@@ -127,12 +133,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
cornerstoneCacheService,
viewportGridService,
stateSyncService,
viewportActionCornersService,
customizationService,
} = servicesManager.services as CornerstoneServices;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
@@ -152,6 +160,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
viewportActionCornersService.clear(viewportId);
},
[viewportId]
);
@@ -361,6 +371,34 @@ const OHIFCornerstoneViewport = React.memo(props => {
};
}, [displaySets, elementRef, viewportId]);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
// Doing an === check here because the default config value when not set is true
if (appConfig.addWindowLevelActionMenu === false) {
return;
}
// TODO: In the future we should consider using the customization service
// to determine if and in which corner various action components should go.
const wlActionMenu = getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
});
viewportActionCornersService.setComponent({
viewportId,
id: 'windowLevelActionMenu',
component: wlActionMenu,
location: viewportActionCornersService.LOCATIONS.topRight,
indexPriority: -100,
});
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
return (
<React.Fragment>
<div className="viewport-wrapper">
@@ -388,7 +426,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
servicesManager={servicesManager}
/>
</div>
<div className="absolute w-full">
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
@@ -400,6 +439,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
/>
)}
</div>
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
<OHIFViewportActionCorners viewportId={viewportId} />
</React.Fragment>
);
}, areEqual);
@@ -80,10 +80,13 @@ function CustomizableViewportOverlay({
'cornerstoneOverlayBottomRight'
);
const instance = useMemo(
() => (viewportData ? getViewportInstance(viewportData, imageIndex) : null),
[viewportData, imageIndex]
);
const instance = useMemo(() => {
if (viewportData != null) {
return _getViewportInstance(viewportData, imageIndex);
} else {
return null;
}
}, [viewportData, imageIndex]);
const instanceNumber = useMemo(
() =>
@@ -220,7 +223,7 @@ function CustomizableViewportOverlay({
{items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}>
{item?.condition
? item.condition()
? item.condition({ instance, formatters: { formatDate: formatDICOMDate } })
? _renderOverlayItem(item)
: null
: _renderOverlayItem(item)}
@@ -242,56 +245,72 @@ function CustomizableViewportOverlay({
topLeftCustomization,
[
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
id: 'StudyDate',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Study date',
condition: ({ instance }) => instance && instance.StudyDate,
contentF: ({ instance, formatters: { formatDate } }) =>
formatDate(instance.StudyDate),
},
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: () => {
const activeToolName = toolGroupService.getActiveToolForViewport(viewportId);
return activeToolName === 'Zoom';
},
id: 'SeriesDescription',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Series description',
attribute: 'SeriesDescription',
condition: ({ instance }) => instance && instance.SeriesDescription,
},
],
'topLeftOverlayItem'
)
}
topRight={getContent(
topRightCustomization,
topRight={getContent(topRightCustomization, [], 'topRightOverlayItem')}
bottomLeft={getContent(
bottomLeftCustomization,
[
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: () => {
const activeToolName = toolGroupService.getActiveToolForViewport(viewportId);
return activeToolName === 'Zoom';
},
},
],
'bottomLeftOverlayItem'
)}
bottomRight={getContent(
bottomRightCustomization,
[
{
id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
],
'topRightOverlayItem'
'bottomRightOverlayItem'
)}
bottomLeft={getContent(bottomLeftCustomization, [null], 'bottomLeftOverlayItem')}
bottomRight={getContent(bottomRightCustomization, [null], 'bottomRightOverlayItem')}
/>
);
}
const getViewportInstance = (viewportData, imageIndex) => {
const { viewportType, data } = viewportData;
function _getViewportInstance(viewportData, imageIndex) {
let imageId = null;
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;
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) || {} : {};
};
}
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
let instanceNumber;
@@ -389,9 +408,9 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
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="ml-1 mr-2 shrink-0">{windowWidth.toFixed(0)}</span>
<span className="mr-1 shrink-0">L:</span>
<span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
<span className="ml-1 shrink-0">{windowCenter.toFixed(0)}</span>
</div>
);
}
@@ -406,7 +425,7 @@ function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
<span>{scale.toFixed(2)}x</span>
</div>
);
}
@@ -427,7 +446,7 @@ function InstanceNumberOverlayItem({
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">I:</span>
<span className="font-light">
<span>
{instanceNumber !== undefined && instanceNumber !== null
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
@@ -108,17 +108,15 @@ function ViewportOrientationMarkers({
console.log('ViewportOrientationMarkers::No viewport');
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 ? csUtils.isEqual(backgroundColor, [1, 1, 1]) : false;
return orientationMarkers.map((m, index) => (
<div
className={classNames(
'overlay-text',
`${m}-mid orientation-marker`,
isLight ? 'text-[#726F7E]' : 'text-[#ccc]'
'text-aqua-pale',
'text-[13px]',
'leading-5'
)}
key={`${m}-mid orientation-marker`}
>
@@ -135,7 +133,7 @@ function ViewportOrientationMarkers({
element,
]);
return <div className="ViewportOrientationMarkers noselect">{markers}</div>;
return <div className="ViewportOrientationMarkers select-none">{markers}</div>;
}
ViewportOrientationMarkers.propTypes = {
+185 -14
View File
@@ -40,6 +40,8 @@ function commandsModule({
cornerstoneViewportService,
uiNotificationService,
measurementService,
colorbarService,
hangingProtocolService,
syncGroupService,
} = servicesManager.services as CornerstoneServices;
@@ -239,18 +241,12 @@ function commandsModule({
cineService.setIsCineEnabled(!isCineEnabled);
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
},
setWindowLevel({ window, level, toolGroupId }) {
setViewportWindowLevel({ viewportId, window, level }) {
// convert to numbers
const windowWidthNum = Number(window);
const windowCenterNum = Number(level);
const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return;
}
// get actor from the viewport
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewport = renderingEngine.getViewport(viewportId);
@@ -265,6 +261,27 @@ function commandsModule({
});
viewport.render();
},
toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => {
const hasColorbar = colorbarService.hasColorbar(viewportId);
if (hasColorbar) {
colorbarService.removeColorbar(viewportId);
return;
}
colorbarService.addColorbar(viewportId, displaySetInstanceUIDs, options);
},
setWindowLevel(props) {
const { toolGroupId } = props;
const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return;
}
actions.setViewportWindowLevel({ ...props, viewportId });
},
setToolEnabled: ({ toolName, toggle }) => {
const { viewports } = viewportGridService.getState();
@@ -514,18 +531,51 @@ function commandsModule({
cstUtils.scroll(viewport, options);
},
setViewportColormap: ({ viewportId, displaySetInstanceUID, colormap, immediate = false }) => {
setViewportColormap: ({
viewportId,
displaySetInstanceUID,
colormap,
opacity = 1,
immediate = false,
}) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const actorEntries = viewport.getActors();
let hpOpacity;
// Retrieve active protocol's viewport match details
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
// Get display set options for the specified viewport ID
const displaySetsInfo = viewportMatchDetails.get(viewportId)?.displaySetsInfo;
const actorEntry = actorEntries.find(actorEntry => {
return actorEntry.uid.includes(displaySetInstanceUID);
});
if (displaySetsInfo) {
// Find the display set that matches the given UID
const matchingDisplaySet = displaySetsInfo.find(
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID
);
// If a matching display set is found, update the opacity with its value
hpOpacity = matchingDisplaySet?.displaySetOptions?.options?.colormap?.opacity;
}
const { actor: volumeActor, uid: volumeId } = actorEntry;
// HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || opacity };
viewport.setProperties({ colormap, volumeActor }, volumeId);
const setViewportProperties = (viewport, uid) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
const { actor: volumeActor, uid: volumeId } = actorEntry;
viewport.setProperties({ colormap, volumeActor }, volumeId);
};
if (viewport instanceof StackViewport) {
setViewportProperties(viewport, viewportId);
}
if (viewport instanceof VolumeViewport) {
if (!displaySetInstanceUID) {
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
}
setViewportProperties(viewport, displaySetInstanceUID);
}
if (immediate) {
viewport.render();
@@ -608,6 +658,107 @@ function commandsModule({
}
});
},
setViewportPreset: ({ viewportId, preset }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
viewport.setProperties({
preset,
});
viewport.render();
},
/**
* Sets the volume quality for a given viewport.
* @param {string} viewportId - The ID of the viewport to set the volume quality.
* @param {number} volumeQuality - The desired quality level of the volume rendering.
*/
setVolumeRenderingQulaity: ({ viewportId, volumeQuality }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const mapper = actor.getMapper();
const image = mapper.getInputData();
const dims = image.getDimensions();
const spacing = image.getSpacing();
const spatialDiagonal = vec3.length(
vec3.fromValues(dims[0] * spacing[0], dims[1] * spacing[1], dims[2] * spacing[2])
);
let sampleDistance = spacing.reduce((a, b) => a + b) / 3.0;
sampleDistance /= volumeQuality > 1 ? 0.5 * volumeQuality ** 2 : 1.0;
const samplesPerRay = spatialDiagonal / sampleDistance + 1;
mapper.setMaximumSamplesPerRay(samplesPerRay);
mapper.setSampleDistance(sampleDistance);
viewport.render();
},
/**
* Shifts opacity points for a given viewport id.
* @param {string} viewportId - The ID of the viewport to set the mapping range.
* @param {number} shift - The shift value to shift the points by.
*/
shiftVolumeOpacityPoints: ({ viewportId, shift }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const ofun = actor.getProperty().getScalarOpacity(0);
const opacityPointValues = []; // Array to hold values
// Gather Existing Values
const size = ofun.getSize();
for (let pointIdx = 0; pointIdx < size; pointIdx++) {
const opacityPointValue = [0, 0, 0, 0];
ofun.getNodeValue(pointIdx, opacityPointValue);
// opacityPointValue now holds [xLocation, opacity, midpoint, sharpness]
opacityPointValues.push(opacityPointValue);
}
// Add offset
opacityPointValues.forEach(opacityPointValue => {
opacityPointValue[0] += shift; // Change the location value
});
// Set new values
ofun.removeAllPoints();
opacityPointValues.forEach(opacityPointValue => {
ofun.addPoint(...opacityPointValue);
});
viewport.render();
},
/**
* Sets the volume lighting settings for a given viewport.
* @param {string} viewportId - The ID of the viewport to set the lighting settings.
* @param {Object} options - The lighting settings to be set.
* @param {boolean} options.shade - The shade setting for the lighting.
* @param {number} options.ambient - The ambient setting for the lighting.
* @param {number} options.diffuse - The diffuse setting for the lighting.
* @param {number} options.specular - The specular setting for the lighting.
**/
setVolumeLighting: ({ viewportId, options }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const property = actor.getProperty();
if (options.shade !== undefined) {
property.setShade(options.shade);
}
if (options.ambient !== undefined) {
property.setAmbient(options.ambient);
}
if (options.diffuse !== undefined) {
property.setDiffuse(options.diffuse);
}
if (options.specular !== undefined) {
property.setSpecular(options.specular);
}
viewport.render();
},
resetCrosshairs: ({ viewportId }) => {
const crosshairInstances = [];
@@ -650,6 +801,11 @@ function commandsModule({
},
getNearbyAnnotation: {
commandFn: actions.getNearbyAnnotation,
storeContexts: [],
options: {},
},
toggleViewportColorbar: {
commandFn: actions.toggleViewportColorbar,
},
deleteMeasurement: {
commandFn: actions.deleteMeasurement,
@@ -660,6 +816,9 @@ function commandsModule({
updateMeasurement: {
commandFn: actions.updateMeasurement,
},
setViewportWindowLevel: {
commandFn: actions.setViewportWindowLevel,
},
setWindowLevel: {
commandFn: actions.setWindowLevel,
},
@@ -754,6 +913,18 @@ function commandsModule({
attachProtocolViewportDataListener: {
commandFn: actions.attachProtocolViewportDataListener,
},
setViewportPreset: {
commandFn: actions.setViewportPreset,
},
setVolumeRenderingQulaity: {
commandFn: actions.setVolumeRenderingQulaity,
},
shiftVolumeOpacityPoints: {
commandFn: actions.shiftVolumeOpacityPoints,
},
setVolumeLighting: {
commandFn: actions.setVolumeLighting,
},
resetCrosshairs: {
commandFn: actions.resetCrosshairs,
},
@@ -0,0 +1,23 @@
import React from 'react';
import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
import { ViewportActionCorners } from '@ohif/ui';
export type OHIFViewportActionCornersProps = {
viewportId: string;
};
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
const [viewportActionCornersState] = useViewportActionCornersContext();
if (!viewportActionCornersState[viewportId]) {
return null;
}
return (
<ViewportActionCorners
cornerComponents={viewportActionCornersState[viewportId]}
></ViewportActionCorners>
);
}
export default OHIFViewportActionCorners;
@@ -0,0 +1,114 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { SwitchButton } from '@ohif/ui';
import { StackViewport, VolumeViewport } from '@cornerstonejs/core';
import { ColorbarProps } from '../../types/Colorbar';
import { utilities } from '@cornerstonejs/core';
export function setViewportColorbar(
viewportId,
displaySets,
commandsManager,
serviceManager,
colorbarOptions
) {
const { cornerstoneViewportService } = serviceManager.services;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
if (isLight) {
colorbarOptions.ticks = {
position: 'left',
style: {
font: '12px Arial',
color: '#000000',
maxNumTicks: 8,
tickSize: 5,
tickWidth: 1,
labelMargin: 3,
},
};
}
const displaySetInstanceUIDs = [];
if (viewport instanceof StackViewport) {
displaySetInstanceUIDs.push(viewportId);
}
if (viewport instanceof VolumeViewport) {
displaySets.forEach(ds => {
displaySetInstanceUIDs.push(ds.displaySetInstanceUID);
});
}
commandsManager.run({
commandName: 'toggleViewportColorbar',
commandOptions: {
viewportId,
options: colorbarOptions,
displaySetInstanceUIDs,
},
context: 'CORNERSTONE',
});
}
export function Colorbar({
viewportId,
displaySets,
commandsManager,
serviceManager,
colorbarProperties,
}: ColorbarProps): ReactElement {
const { colorbarService } = serviceManager.services;
const {
width: colorbarWidth,
colorbarTickPosition,
colorbarContainerPosition,
colormaps,
colorbarInitialColormap,
} = colorbarProperties;
const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId));
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, displaySets, commandsManager, serviceManager, {
viewportId,
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const updateColorbarState = () => {
setShowColorbar(colorbarService.hasColorbar(viewportId));
};
const { unsubscribe } = colorbarService.subscribe(
colorbarService.EVENTS.STATE_CHANGED,
updateColorbarState
);
return () => {
unsubscribe();
};
}, [viewportId]);
return (
<div className="all-in-one-menu-item flex w-full justify-center">
<SwitchButton
label="Display Color bar"
checked={showColorbar}
onChange={() => {
onSetColorbar();
}}
/>
</div>
);
}
@@ -0,0 +1,153 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui';
import { StackViewport } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap';
export function Colormap({
colormaps,
viewportId,
displaySets,
commandsManager,
serviceManager,
}: ColormapProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]);
const [showPreview, setShowPreview] = useState(false);
const [prePreviewColormap, setPrePreviewColormap] = useState(null);
const showPreviewRef = useRef(showPreview);
showPreviewRef.current = showPreview;
const prePreviewColormapRef = useRef(prePreviewColormap);
prePreviewColormapRef.current = prePreviewColormap;
const activeDisplaySetRef = useRef(activeDisplaySet);
activeDisplaySetRef.current = activeDisplaySet;
const onSetColorLUT = useCallback(
props => {
// TODO: Better way to check if it's a fusion
const oneOpacityColormaps = ['Grayscale', 'X Ray'];
const opacity =
displaySets.length > 1 && !oneOpacityColormaps.includes(props.colormap.name) ? 0.5 : 1;
commandsManager.run({
commandName: 'setViewportColormap',
commandOptions: {
...props,
opacity,
immediate: true,
},
context: 'CORNERSTONE',
});
},
[commandsManager]
);
const getViewportColormap = (viewportId, displaySet) => {
const { displaySetInstanceUID } = displaySet;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof StackViewport) {
const { colormap } = viewport.getProperties();
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
return colormap;
}
const actorEntries = viewport.getActors();
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
const { colormap } = viewport.getProperties(actorEntry.uid);
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
return colormap;
};
const buttons = useMemo(() => {
return displaySets.map((displaySet, index) => ({
children: displaySet.Modality,
key: index,
style: {
minWidth: `calc(100% / ${displaySets.length})`,
},
}));
}, [displaySets]);
useEffect(() => {
setActiveDisplaySet(displaySets[displaySets.length - 1]);
}, [displaySets]);
return (
<>
{buttons.length > 1 && (
<div className="all-in-one-menu-item flex w-full justify-center">
<ButtonGroup
onActiveIndexChange={index => {
setActiveDisplaySet(displaySets[index]);
setPrePreviewColormap(null);
}}
activeIndex={
displaySets.findIndex(
ds => ds.displaySetInstanceUID === activeDisplaySetRef.current.displaySetInstanceUID
) || 1
}
className="w-[70%] text-[10px]"
>
{buttons.map(({ children, key, style }) => (
<div
key={key}
style={style}
>
{children}
</div>
))}
</ButtonGroup>
</div>
)}
<div className="all-in-one-menu-item flex w-full justify-center">
<SwitchButton
label="Preview in viewport"
checked={showPreview}
onChange={checked => {
setShowPreview(checked);
}}
/>
</div>
<AllInOneMenu.DividerItem />
<AllInOneMenu.ItemPanel>
{colormaps.map((colormap, index) => (
<AllInOneMenu.Item
key={index}
label={colormap.description}
onClick={() => {
onSetColorLUT({
viewportId,
colormap,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
setPrePreviewColormap(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
setPrePreviewColormap(getViewportColormap(viewportId, activeDisplaySetRef.current));
onSetColorLUT({
viewportId,
colormap,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewColormapRef.current) {
onSetColorLUT({
viewportId,
colormap: prePreviewColormapRef.current,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
}
}}
></AllInOneMenu.Item>
))}
</AllInOneMenu.ItemPanel>
</>
);
}
@@ -0,0 +1,118 @@
import React, { ReactElement, useState, useEffect, useCallback } from 'react';
import { VolumeLightingProps } from '../../types/ViewportPresets';
export function VolumeLighting({
serviceManager,
commandsManager,
viewportId,
}: VolumeLightingProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [ambient, setAmbient] = useState(null);
const [diffuse, setDiffuse] = useState(null);
const [specular, setSpecular] = useState(null);
const onAmbientChange = useCallback(() => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { ambient } });
}, [ambient, commandsManager, viewportId]);
const onDiffuseChange = useCallback(() => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { diffuse } });
}, [diffuse, commandsManager, viewportId]);
const onSpecularChange = useCallback(() => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { specular } });
}, [specular, commandsManager, viewportId]);
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const ambient = actor.getProperty().getAmbient();
const diffuse = actor.getProperty().getDiffuse();
const specular = actor.getProperty().getSpecular();
setAmbient(ambient);
setDiffuse(diffuse);
setSpecular(specular);
}, [viewportId, cornerstoneViewportService]);
return (
<>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="ambient"
>
Ambient
</label>
{ambient !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={ambient}
onChange={e => {
setAmbient(e.target.value);
onAmbientChange();
}}
id="ambient"
max={1}
min={0}
type="range"
step={0.1}
style={{ background: calculateBackground(ambient) }}
/>
)}
</div>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="diffuse"
>
Diffuse
</label>
{diffuse !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={diffuse}
onChange={e => {
setDiffuse(e.target.value);
onDiffuseChange();
}}
id="diffuse"
max={1}
min={0}
type="range"
step={0.1}
style={{ background: calculateBackground(diffuse) }}
/>
)}
</div>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="specular"
>
Specular
</label>
{specular !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={specular}
onChange={e => {
setSpecular(e.target.value);
onSpecularChange();
}}
id="specular"
max={1}
min={0}
type="range"
step={0.1}
style={{ background: calculateBackground(specular) }}
/>
)}
</div>
</>
);
}
@@ -0,0 +1,46 @@
import React, { ReactElement } from 'react';
import { AllInOneMenu } from '@ohif/ui';
import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets';
import { VolumeRenderingQuality } from './VolumeRenderingQuality';
import { VolumeShift } from './VolumeShift';
import { VolumeLighting } from './VolumeLighting';
import { VolumeShade } from './VolumeShade';
export function VolumeRenderingOptions({
viewportId,
commandsManager,
volumeRenderingQualityRange,
serviceManager,
}: VolumeRenderingOptionsProps): ReactElement {
return (
<AllInOneMenu.ItemPanel>
<VolumeRenderingQuality
viewportId={viewportId}
commandsManager={commandsManager}
serviceManager={serviceManager}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
<VolumeShift
viewportId={viewportId}
commandsManager={commandsManager}
serviceManager={serviceManager}
/>
<div className="all-in-one-menu-item flex w-full justify-start">
<div className="text-aqua-pale text-[13px]">LIGHTING</div>
</div>
<AllInOneMenu.DividerItem />
<div className="all-in-one-menu-item flex w-full justify-center">
<VolumeShade
commandsManager={commandsManager}
serviceManager={serviceManager}
viewportId={viewportId}
/>
</div>
<VolumeLighting
viewportId={viewportId}
commandsManager={commandsManager}
serviceManager={serviceManager}
/>
</AllInOneMenu.ItemPanel>
);
}
@@ -0,0 +1,38 @@
import { AllInOneMenu, Icon } from '@ohif/ui';
import React, { ReactElement } from 'react';
import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets';
import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent';
export function VolumeRenderingPresets({
viewportId,
serviceManager,
commandsManager,
volumeRenderingPresets,
}: VolumeRenderingPresetsProps): ReactElement {
const { uiModalService } = serviceManager.services;
const onClickPresets = () => {
uiModalService.show({
content: VolumeRenderingPresetsContent,
title: 'Rendering Presets',
movable: true,
contentProps: {
onClose: uiModalService.hide,
presets: volumeRenderingPresets,
viewportId,
commandsManager,
},
containerDimensions: 'h-[543px] w-[460px]',
contentDimensions: 'h-[493px] w-[460px] pl-[12px]',
});
};
return (
<AllInOneMenu.Item
label="Rendering Presets"
icon={<Icon name="VolumeRendering" />}
rightIcon={<Icon name="action-new-dialog" />}
onClick={onClickPresets}
/>
);
}
@@ -0,0 +1,95 @@
import { Icon } from '@ohif/ui';
import { ButtonEnums } from '@ohif/ui';
import React, { ReactElement, useState, useCallback } from 'react';
import { Button, InputFilterText } from '@ohif/ui';
import { ViewportPreset, VolumeRenderingPresetsContentProps } from '../../types/ViewportPresets';
export function VolumeRenderingPresetsContent({
presets,
viewportId,
commandsManager,
onClose,
}: VolumeRenderingPresetsContentProps): ReactElement {
const [filteredPresets, setFilteredPresets] = useState(presets);
const [searchValue, setSearchValue] = useState('');
const [selectedPreset, setSelectedPreset] = useState<ViewportPreset | null>(null);
const handleSearchChange = useCallback(
(value: string) => {
setSearchValue(value);
const filtered = value
? presets.filter(preset => preset.name.toLowerCase().includes(value.toLowerCase()))
: presets;
setFilteredPresets(filtered);
},
[presets]
);
const handleApply = useCallback(
props => {
commandsManager.runCommand('setViewportPreset', {
...props,
});
},
[commandsManager]
);
const formatLabel = (label: string, maxChars: number) => {
return label.length > maxChars ? `${label.slice(0, maxChars)}...` : label;
};
return (
<div className="flex min-h-full w-full flex-col justify-between">
<div className="border-secondary-light h-[433px] w-full overflow-hidden rounded border bg-black px-2.5">
<div className="flex h-[46px] w-full items-center justify-start">
<div className="h-[26px] w-[200px]">
<InputFilterText
value={searchValue}
onDebounceChange={handleSearchChange}
placeholder={'Search all'}
/>
</div>
</div>
<div className="ohif-scrollbar overflow h-[385px] w-full overflow-y-auto">
<div className="grid grid-cols-4 gap-3 pt-2 pr-3">
{filteredPresets.map((preset, index) => (
<div
key={index}
className="flex cursor-pointer flex-col items-start"
onClick={() => {
setSelectedPreset(preset);
handleApply({ preset: preset.name, viewportId });
}}
>
<Icon
name={preset.name}
className={
selectedPreset?.name === preset.name
? 'border-primary-light h-[75px] w-[95px] max-w-none rounded border-2'
: 'hover:border-primary-light h-[75px] w-[95px] max-w-none rounded border-2 border-black'
}
/>
<label className="text-aqua-pale mt-2 text-left text-xs">
{formatLabel(preset.name, 11)}
</label>
</div>
))}
</div>
</div>
</div>
<footer className="flex h-[60px] w-full items-center justify-end">
<div className="flex">
<Button
name="Cancel"
size={ButtonEnums.size.medium}
type={ButtonEnums.type.secondary}
onClick={onClose}
>
{' '}
Cancel{' '}
</Button>
</div>
</footer>
</div>
);
}
@@ -0,0 +1,69 @@
import React, { ReactElement, useCallback, useState, useEffect } from 'react';
import { VolumeRenderingQualityProps } from '../../types/ViewportPresets';
export function VolumeRenderingQuality({
volumeRenderingQualityRange,
commandsManager,
serviceManager,
viewportId,
}: VolumeRenderingQualityProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const { min, max, step } = volumeRenderingQualityRange;
const [quality, setQuality] = useState(null);
const onChange = useCallback(
(value: number) => {
commandsManager.runCommand('setVolumeRenderingQulaity', {
viewportId,
volumeQuality: value,
});
setQuality(value);
},
[commandsManager, viewportId]
);
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const mapper = actor.getMapper();
const image = mapper.getInputData();
const spacing = image.getSpacing();
const sampleDistance = mapper.getSampleDistance();
const averageSpacing = spacing.reduce((a, b) => a + b) / 3.0;
if (sampleDistance === averageSpacing) {
setQuality(1);
} else {
setQuality(Math.sqrt(averageSpacing / (sampleDistance * 0.5)));
}
}, [cornerstoneViewportService, viewportId]);
return (
<>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="volume"
>
Quality
</label>
{quality !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={quality}
id="volume"
max={max}
min={min}
type="range"
step={step}
onChange={e => onChange(parseInt(e.target.value, 10))}
style={{ background: calculateBackground((quality - min) / (max - min)) }}
/>
)}
</div>
</>
);
}
@@ -0,0 +1,39 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { SwitchButton } from '@ohif/ui';
import { VolumeShadeProps } from '../../types/ViewportPresets';
export function VolumeShade({
commandsManager,
viewportId,
serviceManager,
}: VolumeShadeProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [shade, setShade] = useState(true);
const [key, setKey] = useState(0);
const onShadeChange = useCallback(
(checked: boolean) => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { shade: checked } });
},
[commandsManager, viewportId]
);
useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const shade = actor.getProperty().getShade();
setShade(shade);
setKey(key + 1);
}, [viewportId, cornerstoneViewportService]);
return (
<SwitchButton
key={key}
label="Shade"
checked={shade}
onChange={() => {
setShade(!shade);
onShadeChange(!shade);
}}
/>
);
}
@@ -0,0 +1,91 @@
import React, { ReactElement, useCallback, useEffect, useState, useRef } from 'react';
import { VolumeShiftProps } from '../../types/ViewportPresets';
export function VolumeShift({
viewportId,
commandsManager,
serviceManager,
}: VolumeShiftProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [minShift, setMinShift] = useState<number | null>(null);
const [maxShift, setMaxShift] = useState<number | null>(null);
const [shift, setShift] = useState<number | null>(
cornerstoneViewportService.getCornerstoneViewport(viewportId)?.shiftedBy || 0
);
const [step, setStep] = useState<number | null>(null);
const [isBlocking, setIsBlocking] = useState(false);
const prevShiftRef = useRef<number>(shift);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const ofun = actor.getProperty().getScalarOpacity(0);
useEffect(() => {
if (isBlocking) {
return;
}
const range = ofun.getRange();
const transferFunctionWidth = range[1] - range[0];
const minShift = -transferFunctionWidth;
const maxShift = transferFunctionWidth;
setMinShift(minShift);
setMaxShift(maxShift);
setStep(Math.pow(10, Math.floor(Math.log10(transferFunctionWidth / 500))));
}, [cornerstoneViewportService, viewportId, actor, ofun, isBlocking]);
const onChangeRange = useCallback(
newShift => {
const shiftDifference = newShift - prevShiftRef.current;
prevShiftRef.current = newShift;
viewport.shiftedBy = newShift;
commandsManager.runCommand('shiftVolumeOpacityPoints', {
viewportId,
shift: shiftDifference,
});
},
[commandsManager, viewportId, viewport]
);
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
return (
<>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="shift"
>
Shift
</label>
{step !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={shift}
onChange={e => {
const shiftValue = parseInt(e.target.value, 10);
setShift(shiftValue);
onChangeRange(shiftValue);
}}
id="shift"
onMouseDown={() => setIsBlocking(true)}
onMouseUp={() => setIsBlocking(false)}
max={maxShift}
min={minShift}
type="range"
step={step}
style={{
background: calculateBackground((shift - minShift) / (maxShift - minShift)),
}}
/>
)}
</div>
</>
);
}
@@ -0,0 +1,48 @@
import React, { ReactElement, useCallback } from 'react';
import { AllInOneMenu } from '@ohif/ui';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { CommandsManager } from '@ohif/core';
import { useTranslation } from 'react-i18next';
export type WindowLevelProps = {
viewportId: string;
presets: Record<string, Array<WindowLevelPreset>>;
commandsManager: CommandsManager;
};
export function WindowLevel({
viewportId,
commandsManager,
presets,
}: WindowLevelProps): ReactElement {
const { t } = useTranslation('WindowLevelActionMenu');
const onSetWindowLevel = useCallback(
props => {
commandsManager.run({
commandName: 'setViewportWindowLevel',
commandOptions: {
...props,
},
context: 'CORNERSTONE',
});
},
[commandsManager]
);
return (
<AllInOneMenu.ItemPanel>
<AllInOneMenu.HeaderItem>
{t('Modality Presets', { modality: Object.keys(presets)[0] })}
</AllInOneMenu.HeaderItem>
{Object.values(presets)[0].map((preset, index) => (
<AllInOneMenu.Item
key={index}
label={preset.description}
secondaryLabel={`${preset.window} / ${preset.level}`}
onClick={() => onSetWindowLevel({ ...preset, viewportId })}
></AllInOneMenu.Item>
))}
</AllInOneMenu.ItemPanel>
);
}
@@ -0,0 +1,205 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { AllInOneMenu, useViewportGrid } from '@ohif/ui';
import { CommandsManager, ServicesManager } from '@ohif/core';
import { Colormap } from './Colormap';
import { Colorbar } from './Colorbar';
import { setViewportColorbar } from './Colorbar';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { ColorbarProperties } from '../../types/Colorbar';
import { VolumeRenderingQualityRange } from '../../types/ViewportPresets';
import { WindowLevel } from './WindowLevel';
import { VolumeRenderingPresets } from './VolumeRenderingPresets';
import { VolumeRenderingOptions } from './VolumeRenderingOptions';
import { ViewportPreset } from '../../types/ViewportPresets';
import { VolumeViewport3D } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core';
export type WindowLevelActionMenuProps = {
viewportId: string;
element: HTMLElement;
presets: Record<string, Array<WindowLevelPreset>>;
verticalDirection: AllInOneMenu.VerticalDirection;
horizontalDirection: AllInOneMenu.HorizontalDirection;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
colorbarProperties: ColorbarProperties;
displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
};
export function WindowLevelActionMenu({
viewportId,
element,
presets,
verticalDirection,
horizontalDirection,
commandsManager,
serviceManager,
colorbarProperties,
displaySets,
volumeRenderingPresets,
volumeRenderingQualityRange,
}: WindowLevelActionMenuProps): ReactElement {
const {
colormaps,
colorbarContainerPosition,
colorbarInitialColormap,
colorbarTickPosition,
width: colorbarWidth,
} = colorbarProperties;
const { colorbarService, cornerstoneViewportService } = serviceManager.services;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
const { t } = useTranslation('WindowLevelActionMenu');
const [viewportGrid] = useViewportGrid();
const { activeViewportId } = viewportGrid;
const [vpHeight, setVpHeight] = useState(element?.clientHeight);
const [menuKey, setMenuKey] = useState(0);
const [is3DVolume, setIs3DVolume] = useState(false);
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, displaySets, commandsManager, serviceManager, {
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const newVpHeight = element?.clientHeight;
if (vpHeight !== newVpHeight) {
setVpHeight(newVpHeight);
}
}, [element, vpHeight]);
useEffect(() => {
if (!colorbarService.hasColorbar(viewportId)) {
return;
}
window.setTimeout(() => {
colorbarService.removeColorbar(viewportId);
onSetColorbar();
}, 0);
}, [viewportId]);
useEffect(() => {
if (colorbarService.hasColorbar(viewportId)) {
colorbarService.removeColorbar(viewportId);
}
}, [displaySets]);
useEffect(() => {
setMenuKey(menuKey + 1);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof VolumeViewport3D) {
setIs3DVolume(true);
} else {
setIs3DVolume(false);
}
}, [
displaySets,
viewportId,
presets,
volumeRenderingQualityRange,
volumeRenderingPresets,
colorbarProperties,
activeViewportId,
viewportGrid,
]);
return (
<AllInOneMenu.IconMenu
icon="viewport-window-level"
verticalDirection={verticalDirection}
horizontalDirection={horizontalDirection}
iconClassName={classNames(
// Visible on hover and for the active viewport
activeViewportId === viewportId ? 'visible' : 'invisible group-hover:visible',
'flex shrink-0 cursor-pointer rounded active:text-white',
isLight
? 'text-aqua-pale hover:bg-secondary-dark'
: 'text-primary-light hover:bg-secondary-light/60'
)}
menuStyle={{ maxHeight: vpHeight - 32, minWidth: 218 }}
onVisibilityChange={() => {
setVpHeight(element.clientHeight);
}}
menuKey={menuKey}
>
<AllInOneMenu.ItemPanel>
{!is3DVolume && (
<Colorbar
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
commandsManager={commandsManager}
serviceManager={serviceManager}
colorbarProperties={colorbarProperties}
/>
)}
{colormaps && !is3DVolume && (
<AllInOneMenu.SubMenu
key="colorLUTPresets"
itemLabel="Color LUT"
itemIcon="icon-color-lut"
>
<Colormap
colormaps={colormaps}
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
commandsManager={commandsManager}
serviceManager={serviceManager}
/>
</AllInOneMenu.SubMenu>
)}
{presets && !is3DVolume && (
<AllInOneMenu.SubMenu
key="windowLevelPresets"
itemLabel={t('Modality Window Presets', { modality: Object.keys(presets)[0] })}
itemIcon="viewport-window-level"
>
<WindowLevel
viewportId={viewportId}
commandsManager={commandsManager}
presets={presets}
/>
</AllInOneMenu.SubMenu>
)}
{volumeRenderingPresets && is3DVolume && (
<VolumeRenderingPresets
serviceManager={serviceManager}
viewportId={viewportId}
commandsManager={commandsManager}
volumeRenderingPresets={volumeRenderingPresets}
/>
)}
{volumeRenderingQualityRange && is3DVolume && (
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
<VolumeRenderingOptions
viewportId={viewportId}
commandsManager={commandsManager}
volumeRenderingQualityRange={volumeRenderingQualityRange}
serviceManager={serviceManager}
/>
</AllInOneMenu.SubMenu>
)}
</AllInOneMenu.ItemPanel>
</AllInOneMenu.IconMenu>
);
}
@@ -0,0 +1,23 @@
// The following are the default window level presets and can be further
// configured via the customization service.
const defaultWindowLevelPresets = {
CT: [
{ description: 'Soft tissue', window: '400', level: '40' },
{ description: 'Lung', window: '1500', level: '-600' },
{ description: 'Liver', window: '150', level: '90' },
{ description: 'Bone', window: '2500', level: '480' },
{ description: 'Brain', window: '80', level: '40' },
],
PT: [
{ description: 'Default', window: '5', level: '2.5' },
{ description: 'SUV', window: '0', level: '3' },
{ description: 'SUV', window: '0', level: '5' },
{ description: 'SUV', window: '0', level: '7' },
{ description: 'SUV', window: '0', level: '8' },
{ description: 'SUV', window: '0', level: '10' },
{ description: 'SUV', window: '0', level: '15' },
],
};
export default defaultWindowLevelPresets;
@@ -0,0 +1,44 @@
import React, { ReactNode } from 'react';
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
export function getWindowLevelActionMenu({
viewportId,
element,
displaySets,
servicesManager,
commandsManager,
verticalDirection,
horizontalDirection,
}): ReactNode {
const { customizationService } = servicesManager.services;
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
const colorbarProperties = customizationService.get('cornerstone.colorbar');
const { volumeRenderingPresets, volumeRenderingQualityRange } = customizationService.get(
'cornerstone.3dVolumeRendering'
);
const displaySetPresets = displaySets
.filter(displaySet => presets[displaySet.Modality])
.map(displaySet => {
return { [displaySet.Modality]: presets[displaySet.Modality] };
});
const hasMenu = displaySetPresets.length > 0;
return hasMenu ? (
<WindowLevelActionMenu
viewportId={viewportId}
element={element}
presets={displaySetPresets[0]}
verticalDirection={verticalDirection}
horizontalDirection={horizontalDirection}
commandsManager={commandsManager}
serviceManager={servicesManager}
colorbarProperties={colorbarProperties}
displaySets={displaySets}
volumeRenderingPresets={volumeRenderingPresets}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
) : null;
}
@@ -0,0 +1,159 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
} from 'react';
import PropTypes from 'prop-types';
import { Types, ViewportActionCornersLocations } from '@ohif/ui';
import ViewportActionCornersService, {
ActionComponentInfo,
} from '../services/ViewportActionCornersService/ViewportActionCornersService';
interface StateComponentInfo extends Types.ViewportActionCornersComponentInfo {
indexPriority: number;
}
type State = Record<string, Record<ViewportActionCornersLocations, Array<StateComponentInfo>>>;
const DEFAULT_STATE: State = {
// default here is the viewportId of the default viewport
default: {
[ViewportActionCornersLocations.topLeft]: [],
[ViewportActionCornersLocations.topRight]: [],
[ViewportActionCornersLocations.bottomLeft]: [],
[ViewportActionCornersLocations.bottomRight]: [],
},
// [anotherViewportId]: { ..... }
};
export const ViewportActionCornersContext = createContext(DEFAULT_STATE);
export function ViewportActionCornersProvider({ children, service }) {
const viewportActionCornersReducer = (state, action) => {
switch (action.type) {
case 'SET_ACTION_COMPONENT': {
const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
// Get the components at the specified location of the specified viewport.
let locationComponents = state?.[viewportId]?.[location]
? [...state[viewportId][location]]
: [];
// If the component (id) already exists at the location specified in the payload,
// then it must be replaced with the component in the payload so first
// remove it from that location.
const deletionIndex = locationComponents.findIndex(component => component.id === id);
if (deletionIndex !== -1) {
locationComponents = [
...locationComponents.slice(0, deletionIndex),
...locationComponents.slice(deletionIndex + 1),
];
}
// Insert the component from the payload but
// do not insert an undefined or null component.
if (component) {
const insertionIndex = locationComponents.findIndex(
component => indexPriority <= component.indexPriority
);
locationComponents = [
...locationComponents.slice(0, insertionIndex),
{
id,
component,
indexPriority,
},
...locationComponents.slice(insertionIndex + 1),
];
}
return {
...state,
...{
[viewportId]: {
...state[viewportId],
[location]: locationComponents,
},
},
};
}
case 'CLEAR_ACTION_COMPONENTS': {
const viewportId = action.payload;
const nextState = { ...state };
delete nextState[viewportId];
return nextState;
}
default:
return { ...state };
}
};
const [viewportActionCornersState, dispatch] = useReducer(
viewportActionCornersReducer,
DEFAULT_STATE
);
const getState = useCallback(() => {
return viewportActionCornersState;
}, [viewportActionCornersState]);
const setComponent = useCallback(
(actionComponentInfo: ActionComponentInfo) => {
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo });
},
[dispatch]
);
const setComponents = useCallback(
(actionComponentInfos: Array<ActionComponentInfo>) => {
actionComponentInfos.forEach(actionComponentInfo =>
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo })
);
},
[dispatch]
);
const clear = useCallback(
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
[dispatch]
);
useEffect(() => {
if (service) {
service.setServiceImplementation({
getState,
setComponent,
setComponents,
clear,
});
}
}, [getState, service, setComponent, setComponents]);
// run many of the calls through the service itself since we want to publish events
const api = {
getState,
setComponent: props => service.setComponent(props),
setComponents: props => service.setComponents(props),
clear: props => service.clear(props),
};
const contextValue = useMemo(
() => [viewportActionCornersState, api],
[viewportActionCornersState, api]
);
return (
<ViewportActionCornersContext.Provider value={contextValue}>
{children}
</ViewportActionCornersContext.Provider>
);
}
ViewportActionCornersProvider.propTypes = {
children: PropTypes.node,
service: PropTypes.instanceOf(ViewportActionCornersService).isRequired,
};
export const useViewportActionCornersContext = () => useContext(ViewportActionCornersContext);
@@ -1,6 +1,12 @@
import { Enums } from '@cornerstonejs/tools';
import { toolNames } from './initCornerstoneTools';
import DicomUpload from './components/DicomUpload/DicomUpload';
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
import { colormaps } from './utils/colormaps';
import { CONSTANTS } from '@cornerstonejs/core';
const DefaultColormap = 'Grayscale';
const { VIEWPORT_PRESETS } = CONSTANTS;
const tools = {
active: [
@@ -37,6 +43,27 @@ function getCustomizationModule() {
id: 'cornerstone.overlayViewportTools',
tools,
},
{
id: 'cornerstone.windowLevelPresets',
presets: defaultWindowLevelPresets,
},
{
id: 'cornerstone.colorbar',
width: '16px',
colorbarTickPosition: 'left',
colormaps,
colorbarContainerPosition: 'right',
colorbarInitialColormap: DefaultColormap,
},
{
id: 'cornerstone.3dVolumeRendering',
volumeRenderingPresets: VIEWPORT_PRESETS,
volumeRenderingQualityRange: {
min: 1,
max: 4,
step: 1,
},
},
],
},
];
+9
View File
@@ -52,6 +52,9 @@ export const fourUp = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -94,6 +97,9 @@ export const fourUp = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -117,6 +123,9 @@ export const fourUp = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
+9
View File
@@ -97,6 +97,9 @@ export const main3D = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -120,6 +123,9 @@ export const main3D = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -143,6 +149,9 @@ export const main3D = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
+9
View File
@@ -87,6 +87,9 @@ export const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -111,6 +114,9 @@ export const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -135,6 +141,9 @@ export const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -59,6 +59,9 @@ export const mprAnd3DVolumeViewport = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -100,6 +103,9 @@ export const mprAnd3DVolumeViewport = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -123,6 +129,9 @@ export const mprAnd3DVolumeViewport = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -97,6 +97,9 @@ export const primary3D = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -120,6 +123,9 @@ export const primary3D = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -143,6 +149,9 @@ export const primary3D = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -72,6 +72,9 @@ export const primaryAxial = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -95,6 +98,9 @@ export const primaryAxial = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@@ -118,6 +124,9 @@ export const primaryAxial = {
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
+10 -1
View File
@@ -19,6 +19,7 @@ import SyncGroupService from './services/SyncGroupService';
import SegmentationService from './services/SegmentationService';
import CornerstoneCacheService from './services/CornerstoneCacheService';
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
import ColorbarService from './services/ColorbarService';
import * as CornerstoneExtensionTypes from './types';
import { toolNames } from './initCornerstoneTools';
@@ -30,6 +31,8 @@ import { id } from './id';
import { measurementMappingUtils } from './utils/measurementServiceMappings';
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
@@ -69,13 +72,19 @@ const cornerstoneExtension: Types.Extensions.Extension = {
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
*/
preRegistration: function (props: Types.Extensions.ExtensionParams): Promise<void> {
const { servicesManager } = props;
const { servicesManager, serviceProvidersManager } = props;
servicesManager.registerService(CornerstoneViewportService.REGISTRATION);
servicesManager.registerService(ToolGroupService.REGISTRATION);
servicesManager.registerService(SyncGroupService.REGISTRATION);
servicesManager.registerService(SegmentationService.REGISTRATION);
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
servicesManager.registerService(ViewportActionCornersService.REGISTRATION);
servicesManager.registerService(ColorbarService.REGISTRATION);
serviceProvidersManager.registerProvider(
ViewportActionCornersService.REGISTRATION.name,
ViewportActionCornersProvider
);
return init.call(this, props);
},
+5 -4
View File
@@ -29,6 +29,10 @@ import initContextMenu from './initContextMenu';
import initDoubleClick from './initDoubleClick';
import { CornerstoneServices } from './types';
import initViewTiming from './utils/initViewTiming';
import { utilities } from '@cornerstonejs/core';
import { colormaps } from './utils/colormaps';
const { registerColormap } = utilities.colormap;
// TODO: Cypress tests are currently grabbing this from the window?
window.cornerstone = cornerstone;
@@ -282,10 +286,7 @@ export default async function init({
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
// viewportGridService.subscribe(
// viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
// activeViewportEventListener
// );
colormaps.forEach(registerColormap);
}
function CPUModal() {
@@ -0,0 +1,267 @@
import { PubSubService } from '@ohif/core';
import { RENDERING_ENGINE_ID } from '../ViewportService/constants';
import { StackViewport, VolumeViewport, getRenderingEngine } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
import { ColorbarOptions, ChangeTypes } from '../../types/Colorbar';
const { ViewportColorbar } = utilities.voi.colorbar;
export default class ColorbarService extends PubSubService {
static EVENTS = {
STATE_CHANGED: 'event::ColorbarService:stateChanged',
};
static defaultStyles = {
position: 'absolute',
boxSizing: 'border-box',
border: 'solid 1px #555',
cursor: 'initial',
};
static positionStyles = {
left: { left: '5%' },
right: { right: '5%' },
top: { top: '5%' },
bottom: { bottom: '5%' },
};
static defaultTickStyles = {
position: 'left',
style: {
font: '12px Arial',
color: '#fff',
maxNumTicks: 8,
tickSize: 5,
tickWidth: 1,
labelMargin: 3,
},
};
public static REGISTRATION = {
name: 'colorbarService',
create: () => {
return new ColorbarService();
},
};
colorbars = {};
constructor() {
super(ColorbarService.EVENTS);
}
/**
* Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`.
* This method sets up the colorbar, associates it with the viewport, and applies initial configurations based on the provided options.
*
* @param viewportId The identifier for the viewport where the colorbar will be added.
* @param displaySetInstanceUIDs An array of display set instance UIDs to associate with the colorbar.
* @param options Configuration options for the colorbar, including position, colormaps, active colormap name, ticks, and width.
*/
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
const viewport = renderingEngine.getViewport(viewportId);
const { element } = viewport;
const actorEntries = viewport.getActors();
const { position, width: thickness, activeColormapName, colormaps } = options;
const numContainers = displaySetInstanceUIDs.length;
const containers = this.createContainers(
numContainers,
element,
position,
thickness,
viewportId
);
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
if (!actorEntry) {
return;
}
const { uid: volumeId } = actorEntry;
const properties = viewport?.getProperties(volumeId);
const colormap = properties?.colormap;
// if there's an initial colormap set, and no colormap on the viewport, set it
if (activeColormapName && !colormap) {
this.setViewportColormap(
viewportId,
displaySetInstanceUID,
colormaps[activeColormapName],
true
);
}
const colorbarContainer = containers[index];
const colorbar = new ViewportColorbar({
id: `ctColorbar-${viewportId}-${index}`,
element,
colormaps: options.colormaps || {},
// if there's an existing colormap set, we use it, otherwise we use the activeColormapName, otherwise, grayscale
activeColormapName: colormap?.name || options?.activeColormapName || 'Grayscale',
container: colorbarContainer,
ticks: {
...ColorbarService.defaultTickStyles,
...options.ticks,
},
volumeId: viewport instanceof VolumeViewport ? volumeId : undefined,
});
if (this.colorbars[viewportId]) {
this.colorbars[viewportId].push({ colorbar, container: colorbarContainer });
} else {
this.colorbars[viewportId] = [{ colorbar, container: colorbarContainer }];
}
});
this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, {
viewportId,
changeType: ChangeTypes.Added,
});
}
/**
* Removes the colorbar associated with a given viewport ID. This involves cleaning up any created DOM elements and internal references.
*
* @param viewportId The identifier for the viewport from which the colorbar will be removed.
*/
public removeColorbar(viewportId) {
const colorbarInfo = this.colorbars[viewportId];
if (!colorbarInfo) {
return;
}
colorbarInfo.forEach(({ colorbar, container }) => {
container.parentNode.removeChild(container);
});
delete this.colorbars[viewportId];
this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, {
viewportId,
changeType: ChangeTypes.Removed,
});
}
/**
* Checks whether a colorbar is associated with a given viewport ID.
*
* @param viewportId The identifier for the viewport to check.
* @returns `true` if a colorbar exists for the specified viewport, otherwise `false`.
*/
public hasColorbar(viewportId) {
return this.colorbars[viewportId] ? true : false;
}
/**
* Retrieves the current state of colorbars, including all active colorbars and their configurations.
*
* @returns An object representing the current state of all colorbars managed by this service.
*/
public getState() {
return this.colorbars;
}
/**
* Retrieves colorbar information for a specific viewport ID.
*
* @param viewportId The identifier for the viewport to retrieve colorbar information for.
* @returns The colorbar information associated with the specified viewport, if available.
*/
public getViewportColorbar(viewportId) {
return this.colorbars[viewportId];
}
/**
* Handles the cleanup and removal of all colorbars from the viewports. This is typically called
* when exiting the mode or context in which the colorbars are used, ensuring that no DOM
* elements or references are left behind.
*/
public onModeExit() {
const viewportIds = Object.keys(this.colorbars);
viewportIds.forEach(viewportId => {
this.removeColorbar(viewportId);
});
}
/**
* Sets the colormap for a viewport. This function is used internally to update the colormap the viewport
*
* @param viewportId The identifier of the viewport to update.
* @param displaySetInstanceUID The display set instance UID associated with the viewport.
* @param colormap The colormap object to set on the viewport.
* @param immediate A boolean indicating whether the viewport should be re-rendered immediately after setting the colormap.
*/
private setViewportColormap(viewportId, displaySetInstanceUID, colormap, immediate = false) {
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
const viewport = renderingEngine.getViewport(viewportId);
const actorEntries = viewport.getActors();
const setViewportProperties = (viewport, uid) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
const { actor: volumeActor, uid: volumeId } = actorEntry;
viewport.setProperties({ colormap, volumeActor }, volumeId);
};
if (viewport instanceof StackViewport) {
setViewportProperties(viewport, viewportId);
}
if (viewport instanceof VolumeViewport) {
setViewportProperties(viewport, displaySetInstanceUID);
}
if (immediate) {
viewport.render();
}
}
/**
* Creates the container elements for colorbars based on the specified parameters. This function dynamically
* generates and styles DOM elements to host the colorbars, positioning them according to the specified options.
*
* @param numContainers The number of containers to create, typically corresponding to the number of colorbars.
* @param element The DOM element within which the colorbar containers will be placed.
* @param position The position of the colorbar containers (e.g., 'top', 'bottom', 'left', 'right').
* @param thickness The thickness of the colorbar containers, affecting their width or height depending on their position.
* @param viewportId The identifier of the viewport for which the containers are being created.
* @returns An array of the created container DOM elements.
*/
private createContainers(numContainers, element, position, thickness, viewportId) {
const containers = [];
const dimensions = {
1: 50,
2: 33,
};
const dimension = dimensions[numContainers] || 50 / numContainers;
Array.from({ length: numContainers }).forEach((_, i) => {
const colorbarContainer = document.createElement('div');
colorbarContainer.id = `ctColorbarContainer-${viewportId}-${i + 1}`;
Object.assign(colorbarContainer.style, ColorbarService.defaultStyles);
if (['top', 'bottom'].includes(position)) {
Object.assign(colorbarContainer.style, {
width: `${dimension}%`,
height: thickness || '2.5%',
left: `${(i + 1) * dimension}%`,
transform: 'translateX(-50%)',
...ColorbarService.positionStyles[position],
});
} else if (['left', 'right'].includes(position)) {
Object.assign(colorbarContainer.style, {
height: `${dimension}%`,
width: thickness || '2.5%',
top: `${(i + 1) * dimension}%`,
transform: 'translateY(-50%)',
...ColorbarService.positionStyles[position],
});
}
element.appendChild(colorbarContainer);
containers.push(colorbarContainer);
});
return containers;
}
}
@@ -0,0 +1,2 @@
import ColorbarService from './ColorbarService';
export default ColorbarService;
@@ -0,0 +1,71 @@
import { PubSubService } from '@ohif/core';
import { ViewportActionCornersLocations } from '@ohif/ui';
import { ReactNode } from 'react';
export type ActionComponentInfo = {
viewportId: string;
id: string;
component: ReactNode;
location: ViewportActionCornersLocations;
indexPriority: number;
};
class ViewportActionCornersService extends PubSubService {
public static readonly EVENTS = {};
public static readonly LOCATIONS = ViewportActionCornersLocations;
public static REGISTRATION = {
name: 'viewportActionCornersService',
altName: 'ViewportActionCornersService',
create: ({ configuration = {} }) => {
return new ViewportActionCornersService();
},
};
serviceImplementation = {};
public LOCATIONS = ViewportActionCornersService.LOCATIONS;
constructor() {
super(ViewportActionCornersService.EVENTS);
this.serviceImplementation = {};
}
public setServiceImplementation({
getState: getStateImplementation,
setComponent: setComponentImplementation,
setComponents: setComponentsImplementation,
clear: clearImplementation,
}): void {
if (getStateImplementation) {
this.serviceImplementation._getState = getStateImplementation;
}
if (setComponentImplementation) {
this.serviceImplementation._setComponent = setComponentImplementation;
}
if (setComponentsImplementation) {
this.serviceImplementation._setComponents = setComponentsImplementation;
}
if (clearImplementation) {
this.serviceImplementation._clear = clearImplementation;
}
}
public getState() {
return this.serviceImplementation._getState();
}
public setComponent(component: ActionComponentInfo) {
this.serviceImplementation._setComponent(component);
}
public setComponents(components: Array<ActionComponentInfo>) {
this.serviceImplementation._setComponents(components);
}
public clear(viewportId: string) {
this.serviceImplementation._clear(viewportId);
}
}
export default ViewportActionCornersService;
@@ -0,0 +1,32 @@
import { ColorMapPreset } from './Colormap';
import { CommandsManager, ServicesManager } from '@ohif/core';
export type ColorbarOptions = {
position: string;
colormaps: Array<ColorMapPreset>;
activeColormapName: string;
ticks: object;
width: string;
};
export type ColorbarProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
displaySets: Array<any>;
colorbarProperties: ColorbarProperties;
};
export type ColorbarProperties = {
width: string;
colorbarTickPosition: string;
colorbarContainerPosition: string;
colormaps: Array<ColorMapPreset>;
colorbarInitialColormap: string;
};
export enum ChangeTypes {
Removed = 'removed',
Added = 'added',
Modified = 'modified',
}
@@ -0,0 +1,16 @@
import { CommandsManager, ServicesManager } from '@ohif/core';
export type ColorMapPreset = {
ColorSpace;
description: string;
RGBPoints;
Name;
};
export type ColormapProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
colormaps: Array<ColorMapPreset>;
displaySets: Array<any>;
};
@@ -4,6 +4,7 @@ import SyncGroupService from '../services/SyncGroupService';
import SegmentationService from '../services/SegmentationService';
import CornerstoneCacheService from '../services/CornerstoneCacheService';
import CornerstoneViewportService from '../services/ViewportService/CornerstoneViewportService';
import ViewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService';
interface CornerstoneServices extends Types.Services {
cornerstoneViewportService: CornerstoneViewportService;
@@ -11,6 +12,7 @@ interface CornerstoneServices extends Types.Services {
syncGroupService: SyncGroupService;
segmentationService: SegmentationService;
cornerstoneCacheService: CornerstoneCacheService;
viewportActionCornersService: ViewportActionCornersService;
}
export default CornerstoneServices;
@@ -0,0 +1,66 @@
import { ServicesManager, CommandsManager } from '@ohif/core';
export type ViewportPreset = {
name: string;
gradientOpacity: string;
specularPower: string;
scalarOpacity: string;
specular: string;
shade: string;
ambient: string;
colorTransfer: string;
diffuse: string;
interpolation: string;
};
export type VolumeRenderingPresetsProps = {
viewportId: string;
serviceManager: ServicesManager;
commandsManager: CommandsManager;
volumeRenderingPresets: ViewportPreset[];
};
export type VolumeRenderingPresetsContentProps = {
presets: ViewportPreset[];
onClose: () => void;
viewportId: string;
commandsManager: CommandsManager;
};
export type VolumeRenderingOptionsProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
};
export type VolumeRenderingQualityRange = {
min: number;
max: number;
step: number;
};
export type VolumeRenderingQualityProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
};
export type VolumeShiftProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
};
export type VolumeShadeProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
};
export type VolumeLightingProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
};
@@ -0,0 +1,5 @@
export type WindowLevelPreset = {
description: string;
window: string;
level: string;
};
File diff suppressed because it is too large. Load diff