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:
Ibrahim 2024-04-02 09:35:30 -04:00 committed by GitHub
parent 07301fc020
commit b7f90e3951
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
164 changed files with 5887 additions and 932 deletions

View File

@ -120,6 +120,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
test: /\.wasm/,
type: 'asset/resource',
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'assets/images/[name].[ext]',
},
},
],
},
], //.concat(vtkRules),
},
resolve: {

View File

@ -1,13 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import OHIF, { utils } from '@ohif/core';
import { ViewportActionBar, useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
const { formatDate } = utils;
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
function OHIFCornerstoneRTViewport(props) {
@ -15,7 +13,6 @@ function OHIFCornerstoneRTViewport(props) {
children,
displaySets,
viewportOptions,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
@ -27,6 +24,7 @@ function OHIFCornerstoneRTViewport(props) {
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@ -126,7 +124,6 @@ function OHIFCornerstoneRTViewport(props) {
const onSegmentChange = useCallback(
direction => {
direction = direction === 'left' ? -1 : 1;
const segmentationId = rtDisplaySet.displaySetInstanceUID;
const segmentation = segmentationService.getSegmentation(segmentationId);
@ -282,20 +279,7 @@ function OHIFCornerstoneRTViewport(props) {
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
SeriesNumber,
} = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => {
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
@ -309,41 +293,47 @@ function OHIFCornerstoneRTViewport(props) {
});
setIsHydrated(isHydrated);
};
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onSegmentChange}
getStatusComponent={() => {
return _getStatusComponent({
isHydrated,
onStatusClick,
});
}}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: `RT Viewport ${SeriesDescription}`,
patientInformation: {
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
spacing:
SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{rtIsLoading && (
<LoadingIndicatorTotalPercent

View File

@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
switch (isHydrated) {
case true:
StatusIcon = () => <Icon name="status-alert" />;
@ -26,23 +23,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
}
const StatusArea = () => (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">RTSTRUCT</span>
</div>
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">RTSTRUCT</span>
</div>
)}
</div>
);
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
</div>
)}
</div>
);
};
return (
<>

View File

@ -1,13 +1,11 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils } from '@ohif/core';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent';
const { formatDate } = utils;
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
function OHIFCornerstoneSEGViewport(props) {
@ -15,7 +13,6 @@ function OHIFCornerstoneSEGViewport(props) {
children,
displaySets,
viewportOptions,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
@ -30,6 +27,7 @@ function OHIFCornerstoneSEGViewport(props) {
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
@ -122,7 +120,6 @@ function OHIFCornerstoneSEGViewport(props) {
const onSegmentChange = useCallback(
direction => {
direction = direction === 'left' ? -1 : 1;
const segmentationId = segDisplaySet.displaySetInstanceUID;
const segmentation = segmentationService.getSegmentation(segmentationId);
@ -247,6 +244,69 @@ function OHIFCornerstoneSEGViewport(props) {
};
}, [segDisplaySet]);
const hydrateSEGDisplaySet = useCallback(
({ segDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [segDisplaySet],
viewportId,
});
},
[commandsManager]
);
const onStatusClick = useCallback(async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the SEG. This is
// required if the user has changed the viewport (other viewport than SEG viewport)
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@ -282,61 +342,8 @@ function OHIFCornerstoneSEGViewport(props) {
SpacingBetweenSlices,
} = referencedDisplaySetRef.current.metadata;
const hydrateSEGDisplaySet = ({ segDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [segDisplaySet],
viewportId,
});
};
const onStatusClick = async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the SEG. This is
// required if the user has changed the viewport (other viewport than SEG viewport)
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
};
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onSegmentChange}
getStatusComponent={() => {
return _getStatusComponent({
isHydrated,
onStatusClick,
});
}}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
seriesDescription: `SEG Viewport ${SeriesDescription}`,
patientInformation: {
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? utils.roundNumber(SliceThickness, 2) : '',
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
spacing:
SpacingBetweenSlices !== undefined ? utils.roundNumber(SpacingBetweenSlices, 2) : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{segIsLoading && (
<LoadingIndicatorTotalPercent

View File

@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
switch (isHydrated) {
case true:
StatusIcon = () => <Icon name="status-alert" />;
@ -26,23 +23,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
}
const StatusArea = () => (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SEG</span>
</div>
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SEG</span>
</div>
)}
</div>
);
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
</div>
)}
</div>
);
};
return (
<>

View File

@ -1,35 +1,30 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
import { ServicesManager, ExtensionManager } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import { useAppConfig } from '@state';
const { formatDate } = utils;
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRViewport(props) {
const {
children,
dataSource,
displaySets,
viewportLabel,
viewportOptions,
servicesManager,
extensionManager,
} = props;
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
const [appConfig] = useAppConfig();
const { displaySetService, cornerstoneViewportService, measurementService } =
servicesManager.services;
const {
displaySetService,
cornerstoneViewportService,
measurementService,
viewportActionCornersService,
} = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@ -48,6 +43,8 @@ function OHIFCornerstoneSRViewport(props) {
const [element, setElement] = useState(null);
const { viewports, activeViewportId } = viewportGrid;
const { t } = useTranslation('Common');
// Optional hook into tracking extension, if present.
let trackedMeasurements;
let sendTrackedMeasurementsEvent;
@ -213,18 +210,11 @@ function OHIFCornerstoneSRViewport(props) {
direction => {
let newMeasurementSelected = measurementSelected;
if (direction === 'right') {
newMeasurementSelected++;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
}
} else {
newMeasurementSelected--;
if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
newMeasurementSelected += direction;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
} else if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
setTrackingIdentifiers(newMeasurementSelected);
@ -298,6 +288,46 @@ function OHIFCornerstoneSRViewport(props) {
updateViewport(measurementSelected);
}, [dataSource, srDisplaySet]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
srDisplaySet,
viewportId,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
index: 0,
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onMeasurementChange}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
isLocked,
onMeasurementChange,
sendTrackedMeasurementsEvent,
srDisplaySet,
t,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@ -317,57 +347,8 @@ function OHIFCornerstoneSRViewport(props) {
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
SeriesNumber,
} = referencedDisplaySetMetadata;
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onMeasurementChange}
getStatusComponent={() =>
_getStatusComponent({
srDisplaySet,
viewportId,
isTracked: false,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
})
}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: SeriesDescription || '',
patientInformation: {
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${parseFloat(SliceThickness).toFixed(2)}mm` : '',
spacing:
SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{getCornerstoneViewport()}
{childrenWithProps}
@ -384,7 +365,6 @@ OHIFCornerstoneSRViewport.propTypes = {
viewportLabel: PropTypes.string,
customProps: PropTypes.object,
viewportOptions: PropTypes.object,
viewportLabel: PropTypes.string,
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
@ -429,6 +409,7 @@ function _getStatusComponent({
isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
}) {
const handleMouseUp = () => {
sendTrackedMeasurementsEvent('HYDRATE_SR', {
@ -437,7 +418,6 @@ function _getStatusComponent({
});
};
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
// 1 - Incompatible

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,
},
},
],
},

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,
},
},
],
},

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,
},
},
],
},

View File

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

View File

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

View File

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

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

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() {

View File

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

View File

@ -0,0 +1,2 @@
import ColorbarService from './ColorbarService';
export default ColorbarService;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
export type WindowLevelPreset = {
description: string;
window: string;
level: string;
};

File diff suppressed because it is too large Load Diff

View File

@ -105,6 +105,8 @@ function ViewerHeader({ hotkeysManager, extensionManager, servicesManager }) {
isReturnEnabled={!!appConfig.showStudyList}
onClickReturnButton={onClickReturnButton}
WhiteLabeling={appConfig.whiteLabeling}
showPatientInfo={appConfig.showPatientInfo}
servicesManager={servicesManager}
>
<ErrorBoundary context="Primary Toolbar">
<div className="relative flex justify-center">

View File

@ -401,15 +401,6 @@ function _mapDisplaySets(
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
const componentType = _getComponentType(ds);
const numPanes = viewportGridService.getNumViewportPanes();
const viewportIdentificator = [];
if (numPanes !== 1) {
viewports.forEach(viewportData => {
if (viewportData?.displaySetInstanceUIDs?.includes(ds.displaySetInstanceUID)) {
viewportIdentificator.push(viewportData.viewportLabel);
}
});
}
const array =
componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets;
@ -435,7 +426,6 @@ function _mapDisplaySets(
},
isTracked: trackedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID),
isHydratedForDerivedDisplaySet: ds.isHydrated,
viewportIdentificator,
};
if (componentType === 'thumbnailNoImage') {

View File

@ -1,27 +1,29 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import OHIF, { utils } from '@ohif/core';
import { ViewportActionBar, Tooltip, Icon } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
import { Tooltip, Icon, ViewportActionArrows, useViewportGrid } from '@ohif/ui';
import { annotation } from '@cornerstonejs/tools';
import { useTrackedMeasurements } from './../getContextModule';
import { BaseVolumeViewport, Enums } from '@cornerstonejs/core';
const { formatDate } = utils;
import { useTranslation } from 'react-i18next';
function TrackedCornerstoneViewport(props) {
const { displaySets, viewportId, viewportLabel, servicesManager, extensionManager } = props;
const { displaySets, viewportId, servicesManager, extensionManager } = props;
const { t } = useTranslation('Common');
const { measurementService, cornerstoneViewportService, viewportGridService } =
servicesManager.services;
const {
measurementService,
cornerstoneViewportService,
viewportGridService,
viewportActionCornersService,
} = servicesManager.services;
// Todo: handling more than one displaySet on the same viewport
const displaySet = displaySets[0];
const { t } = useTranslation('Common');
const [viewportGrid] = useViewportGrid();
const { activeViewportId } = viewportGrid;
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
@ -31,18 +33,7 @@ function TrackedCornerstoneViewport(props) {
const { trackedSeries } = trackedMeasurements.context;
const { SeriesDate, SeriesDescription, SeriesInstanceUID, SeriesNumber } = displaySet;
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
SpacingBetweenSlices,
StudyDate,
ManufacturerModelName,
} = displaySet.images[0];
const { SeriesInstanceUID } = displaySet;
const updateIsTracked = useCallback(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
@ -170,22 +161,51 @@ function TrackedCornerstoneViewport(props) {
};
}, [measurementService, sendTrackedMeasurementsEvent, viewportId, viewportGridService]);
function switchMeasurement(direction) {
const newTrackedMeasurementUID = _getNextMeasurementUID(
direction,
servicesManager,
trackedMeasurementUID,
trackedMeasurements
const switchMeasurement = useCallback(
direction => {
const newTrackedMeasurementUID = _getNextMeasurementUID(
direction,
servicesManager,
trackedMeasurementUID,
trackedMeasurements
);
if (!newTrackedMeasurementUID) {
return;
}
setTrackedMeasurementUID(newTrackedMeasurementUID);
measurementService.jumpToMeasurement(viewportId, newTrackedMeasurementUID);
},
[measurementService, servicesManager, trackedMeasurementUID, trackedMeasurements, viewportId]
);
useEffect(() => {
const statusComponent = _getStatusComponent(isTracked, t);
const arrowsComponent = _getArrowsComponent(
isTracked,
switchMeasurement,
viewportId === activeViewportId
);
if (!newTrackedMeasurementUID) {
return;
}
setTrackedMeasurementUID(newTrackedMeasurementUID);
measurementService.jumpToMeasurement(viewportId, newTrackedMeasurementUID);
}
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: statusComponent,
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: arrowsComponent,
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [activeViewportId, isTracked, switchMeasurement, viewportActionCornersService, viewportId]);
const getCornerstoneViewport = () => {
const { component: Component } = extensionManager.getModuleEntry(
@ -203,35 +223,6 @@ function TrackedCornerstoneViewport(props) {
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
useAltStyling={isTracked}
onArrowsClick={direction => switchMeasurement(direction)}
getStatusComponent={() => _getStatusComponent(isTracked)}
studyData={{
label: viewportLabel,
studyDate: formatDate(SeriesDate) || formatDate(StudyDate) || t('NoStudyDate'),
currentSeries: SeriesNumber, // TODO - switch entire currentSeries to be UID based or actual position based
seriesDescription: SeriesDescription,
patientInformation: {
patientName: PatientName ? OHIF.utils.formatPN(PatientName) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${parseFloat(SliceThickness).toFixed(2)}` : '',
thicknessUnits: t('mm'),
spacing:
SpacingBetweenSlices !== undefined
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}${t('mm')}`
: '',
scanner: ManufacturerModelName || '',
},
}}
/>
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
<div className="relative flex h-full w-full flex-row overflow-hidden">
{getCornerstoneViewport()}
</div>
@ -291,18 +282,11 @@ function _getNextMeasurementUID(
// Not tracking a measurement, or previous measurement now deleted, revert to 0.
measurementIndex = 0;
} else {
if (direction === 'left') {
measurementIndex--;
if (measurementIndex < 0) {
measurementIndex = measurementCount - 1;
}
} else if (direction === 'right') {
measurementIndex++;
if (measurementIndex === measurementCount) {
measurementIndex = 0;
}
measurementIndex += direction;
if (measurementIndex < 0) {
measurementIndex = measurementCount - 1;
} else if (measurementIndex === measurementCount) {
measurementIndex = 0;
}
}
@ -311,9 +295,23 @@ function _getNextMeasurementUID(
return newTrackedMeasurementId;
}
function _getStatusComponent(isTracked) {
const { t } = useTranslation('TrackedCornerstoneViewport');
const trackedIcon = isTracked ? 'status-tracked' : 'status-untracked';
const _getArrowsComponent = (isTracked, switchMeasurement, isActiveViewport) => {
if (!isTracked) {
return null;
}
return (
<ViewportActionArrows
onArrowsClick={direction => switchMeasurement(direction)}
className={isActiveViewport ? 'visible' : 'invisible group-hover:visible'}
></ViewportActionArrows>
);
};
function _getStatusComponent(isTracked, t) {
if (!isTracked) {
return null;
}
return (
<div className="relative">
@ -344,7 +342,7 @@ function _getStatusComponent(isTracked) {
}
>
<Icon
name={trackedIcon}
name={'viewport-status-tracked'}
className="text-aqua-pale"
/>
</Tooltip>

View File

@ -508,13 +508,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
displaySetInstanceUID: ptDisplaySet.displaySetInstanceUID,
colormap: {
name: colormap,
// TODO: This opacity mapping matches that in hpViewports, but
// ideally making this editable in a side panel would be useful
opacity: [
{ value: 0, opacity: 0 },
{ value: 0.1, opacity: 0.9 },
{ value: 1, opacity: 0.95 },
],
},
});

View File

@ -1,10 +1,6 @@
import { addTool, RectangleROIStartEndThresholdTool } from '@cornerstonejs/tools';
import { utilities } from '@cornerstonejs/core';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import colormaps from './utils/colormaps';
const { registerColormap } = utilities.colormap;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
@ -38,5 +34,4 @@ export default function init({ servicesManager }) {
RectangleROIStartEndThreshold.toAnnotation,
RectangleROIStartEndThreshold.toMeasurement
);
colormaps.forEach(registerColormap);
}

View File

@ -20,6 +20,9 @@ const ctAXIAL = {
id: 'ctWLSync',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@ -48,6 +51,9 @@ const ctSAGITTAL = {
id: 'ctWLSync',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@ -75,6 +81,9 @@ const ctCORONAL = {
id: 'ctWLSync',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
@ -108,6 +117,9 @@ const ptAXIAL = {
id: 'ptWLSync',
source: true,
target: true,
options: {
syncColormap: true,
},
},
{
type: 'voi',
@ -152,6 +164,9 @@ const ptSAGITTAL = {
id: 'ptWLSync',
source: true,
target: true,
options: {
syncColormap: true,
},
},
{
type: 'voi',
@ -196,6 +211,9 @@ const ptCORONAL = {
id: 'ptWLSync',
source: true,
target: true,
options: {
syncColormap: true,
},
},
{
type: 'voi',
@ -249,6 +267,9 @@ const fusionAXIAL = {
id: 'fusionWLSync',
source: true,
target: true,
options: {
syncColormap: true,
},
},
{
type: 'voi',
@ -312,6 +333,9 @@ const fusionSAGITTAL = {
id: 'fusionWLSync',
source: true,
target: true,
options: {
syncColormap: true,
},
},
{
type: 'voi',
@ -375,6 +399,9 @@ const fusionCORONAL = {
id: 'fusionWLSync',
source: true,
target: true,
options: {
syncColormap: true,
},
},
{
type: 'voi',
@ -423,6 +450,9 @@ const mipSAGITTAL = {
id: 'ptWLSync',
source: true,
target: true,
options: {
syncColormap: true,
},
},
{
type: 'voi',

View File

@ -114,7 +114,7 @@ const toolbarButtons: Button[] = [
evaluate: 'evaluate.cornerstoneTool',
},
},
// Window Level + Presets...
// Window Level
{
id: 'WindowLevel',
uiType: 'ohif.splitButton',

View File

@ -7,29 +7,6 @@ import type { Button } from '@ohif/core/types';
const { windowLevelPresets } = defaults;
const { createButton } = ToolbarService;
/**
*
* @param {*} preset - preset number (from above import)
* @param {*} title
* @param {*} subtitle
*/
function _createWwwcPreset(preset, title, subtitle) {
return {
id: preset.toString(),
title,
subtitle,
commands: [
{
commandName: 'setWindowLevel',
commandOptions: {
...windowLevelPresets[preset],
},
context: 'CORNERSTONE',
},
],
};
}
export const setToolActiveToolbar = {
commandName: 'setToolActiveToolbar',
commandOptions: {
@ -111,33 +88,15 @@ const toolbarButtons: Button[] = [
evaluate: 'evaluate.cornerstoneTool',
},
},
// Window Level + Presets...
// Window Level
{
id: 'WindowLevel',
uiType: 'ohif.splitButton',
uiType: 'ohif.radioGroup',
props: {
groupId: 'WindowLevel',
primary: createButton({
id: 'WindowLevel',
icon: 'tool-window-level',
label: 'Window Level',
tooltip: 'Window Level',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
secondary: {
icon: 'chevron-down',
label: 'W/L Manual',
tooltip: 'W/L Presets',
},
renderer: WindowLevelMenuItem,
items: [
_createWwwcPreset(1, 'Soft tissue', '400 / 40'),
_createWwwcPreset(2, 'Lung', '1500 / -600'),
_createWwwcPreset(3, 'Liver', '150 / 90'),
_createWwwcPreset(4, 'Bone', '2500 / 480'),
_createWwwcPreset(5, 'Brain', '80 / 40'),
],
icon: 'tool-window-level',
label: 'Window Level',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
// Pan...

View File

@ -49,29 +49,12 @@ const toolbarButtons: Button[] = [
},
{
id: 'WindowLevel',
uiType: 'ohif.splitButton',
uiType: 'ohif.radioGroup',
props: {
groupId: 'WindowLevel',
primary: createButton({
id: 'WindowLevel',
icon: 'tool-window-level',
label: 'Window Level',
tooltip: 'Window Level',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
secondary: {
icon: 'chevron-down',
tooltip: 'W/L Presets',
},
renderer: WindowLevelMenuItem,
items: [
_createWwwcPreset(1, 'Soft tissue', '400 / 40'),
_createWwwcPreset(2, 'Lung', '1500 / -600'),
_createWwwcPreset(3, 'Liver', '150 / 90'),
_createWwwcPreset(4, 'Bone', '2500 / 480'),
_createWwwcPreset(5, 'Brain', '80 / 40'),
],
icon: 'tool-window-level',
label: 'Window Level',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{

View File

@ -90,7 +90,6 @@ function modeFactory({ modeConfiguration }) {
'Pan',
'SyncToggle',
'RectangleROIStartEndThreshold',
'fusionPTColormap',
]);
// For the hanging protocol we need to decide on the window level

View File

@ -103,29 +103,12 @@ const toolbarButtons = [
// Window Level + Presets
{
id: 'WindowLevel',
uiType: 'ohif.splitButton',
uiType: 'ohif.radioGroup',
props: {
groupId: 'WindowLevel',
primary: ToolbarService.createButton({
id: 'WindowLevel',
icon: 'tool-window-level',
label: 'Window Level',
tooltip: 'Window Level',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
secondary: {
icon: 'chevron-down',
tooltip: 'W/L Presets',
},
renderer: WindowLevelMenuItem,
items: [
_createWwwcPreset(1, 'Soft tissue', '400 / 40'),
_createWwwcPreset(2, 'Lung', '1500 / -600'),
_createWwwcPreset(3, 'Liver', '150 / 90'),
_createWwwcPreset(4, 'Bone', '2500 / 480'),
_createWwwcPreset(5, 'Brain', '80 / 40'),
],
icon: 'tool-window-level',
label: 'Window Level',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
// Crosshairs Button
@ -161,38 +144,6 @@ const toolbarButtons = [
evaluate: 'evaluate.cornerstoneTool',
},
},
// Fusion PT Colormap Button
{
id: 'fusionPTColormap',
uiType: 'ohif.splitButton',
props: {
groupId: 'fusionPTColormap',
primary: ToolbarService.createButton({
id: 'fusionPTColormap',
icon: 'tool-fusion-color',
label: 'Fusion PT Colormap',
tooltip: 'Fusion PT Colormap',
commands: [],
evaluate: 'evaluate.action',
}),
secondary: {
icon: 'chevron-down',
tooltip: 'PET Image Colormap',
},
items: [
_createColormap('HSV', 'hsv'),
_createColormap('Hot Iron', 'hot_iron'),
_createColormap('S PET', 's_pet'),
_createColormap('Red Hot', 'red_hot'),
_createColormap('Perfusion', 'perfusion'),
_createColormap('Rainbow', 'rainbow_2'),
_createColormap('SUV', 'suv'),
_createColormap('GE 256', 'ge_256'),
_createColormap('GE', 'ge'),
_createColormap('Siemens', 'siemens'),
],
},
},
];
export default toolbarButtons;

View File

@ -15,7 +15,7 @@ describe('OHIF Multi Study', () => {
cy.get('[data-cy="viewport-pane"]').as('viewportPane');
cy.get('@viewportPane').its('length').should('be.eq', 4);
cy.get('[data-cy="studyDate"]').as('studyDate');
cy.get('[data-cy="viewport-overlay-top-left"] [title="Study date"]').as('studyDate');
cy.get('@studyDate').should(studyDate => {
expect(studyDate.length).to.be.eq(4);

View File

@ -36,6 +36,6 @@ describe('OHIF HP', () => {
cy.initCommonElementsAliases();
// The specified series/sop UID's are index 101, so ensure that image is displayed
cy.get('@viewportInfoTopRight').should('contains.text', 'I:6');
cy.get('@viewportInfoBottomRight').should('contains.text', 'I:6');
});
});

View File

@ -29,10 +29,10 @@ describe('OHIF Cornerstone Hotkeys', () => {
it('checks if hotkeys "ArrowUp" and "ArrowDown" can navigate in the stack', () => {
// Hotkey ArrowDown
cy.get('body').type('{downarrow}');
cy.get('@viewportInfoTopRight').should('contains.text', 'I:2 (2/26)');
cy.get('@viewportInfoBottomRight').should('contains.text', 'I:2 (2/26)');
// Hotkey ArrowUp
cy.get('body').type('{uparrow}');
cy.get('@viewportInfoTopRight').should('contains.text', 'I:1 (1/26)');
cy.get('@viewportInfoBottomRight').should('contains.text', 'I:1 (1/26)');
});
it('checks if hotkeys "V" and "H" can flip the image', () => {

View File

@ -83,7 +83,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.trigger('mouseup', { buttons: 1 });
// The exact text is slightly dependent on the viewport resolution, so leave a range
cy.get('@viewportInfoTopLeft').should($txt => {
cy.get('@viewportInfoBottomLeft').should($txt => {
const text = $txt.text();
expect(text).to.include('W:118').include('L:479');
});
@ -143,7 +143,7 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.resetViewport();
const expectedText = 'W:958L:479';
cy.get('@viewportInfoTopLeft').should('have.text', expectedText);
cy.get('@viewportInfoBottomLeft').should('have.text', expectedText);
});
/*it('checks if CINE tool will prompt a modal with working controls', () => {

View File

@ -8,7 +8,7 @@ describe('OHIF General Viewer', function () {
it('scrolls series stack using scrollbar', function () {
cy.scrollToIndex(13);
cy.get('@viewportInfoTopRight').should('contains.text', '14');
cy.get('@viewportInfoBottomRight').should('contains.text', '14');
});
it('performs right click to zoom', function () {
@ -32,7 +32,7 @@ describe('OHIF General Viewer', function () {
.trigger('mouseup');
// make sure the new zoom level is less than the initial
cy.get('@viewportInfoTopLeft').then($viewportInfo => {
cy.get('@viewportInfoBottomLeft').then($viewportInfo => {
const zoomLevelFinal = $viewportInfo.text().substring(6, 9);
expect(zoomLevelFinal < zoomLevelInitial).to.eq(true);
});

View File

@ -51,13 +51,13 @@ describe('OHIF Measurement Panel', function () {
// Reset to default tool so that the new add length works
cy.addLengthMeasurement([100, 100], [200, 200]); //Adding measurement in the viewport
cy.get('@viewportInfoTopRight').should('contains.text', '(14/');
cy.get('@viewportInfoBottomRight').should('contains.text', '(14/');
// Click on first measurement item
cy.get('[data-cy="measurement-item"]').eq(0).click();
cy.get('@viewportInfoTopRight').should('contains.text', '(1/');
cy.get('@viewportInfoTopRight').should('not.contains.text', '(14/');
cy.get('@viewportInfoBottomRight').should('contains.text', '(1/');
cy.get('@viewportInfoBottomRight').should('not.contains.text', '(14/');
});
/*

View File

@ -31,31 +31,8 @@ describe('OHIF MPR', () => {
cy.wait(250);
cy.get('[data-cy="MPR"]').click();
cy.get('[data-cy="thumbnail-viewport-labels"]').should('have.length', 3);
cy.get('.cornerstone-canvas').should('have.length', 3);
cy.get('[data-cy="thumbnail-viewport-labels"]')
.eq(2)
.find('div')
.should('have.length', 3)
.each(($div, index) => {
const text = $div.text();
switch (index) {
case 0:
expect(text).to.equal('A');
break;
case 1:
expect(text).to.equal('B');
break;
case 2:
expect(text).to.equal('C');
break;
default:
throw new Error(`Unexpected div found with text: ${text}`);
}
});
// check cornerstone to see if each has images
// we can later do visual testing to match the images with a baseline
cy.window()
@ -81,9 +58,6 @@ describe('OHIF MPR', () => {
cy.get('[data-cy="MPR"]').click();
cy.get('.cornerstone-canvas').should('have.length', 1);
// should not have any div under it
cy.get('[data-cy="thumbnail-viewport-labels"]').eq(2).find('div').should('have.length', 0);
});
it('should correctly render Crosshairs for MPR', () => {

View File

@ -5,7 +5,13 @@ import i18n from '@ohif/i18n';
import { I18nextProvider } from 'react-i18next';
import { BrowserRouter } from 'react-router-dom';
import Compose from './routes/Mode/Compose';
import { ServicesManager, ExtensionManager, CommandsManager, HotkeysManager } from '@ohif/core';
import {
ServicesManager,
ExtensionManager,
CommandsManager,
HotkeysManager,
ServiceProvidersManager,
} from '@ohif/core';
import {
DialogProvider,
Modal,
@ -28,6 +34,7 @@ import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes';
let commandsManager: CommandsManager,
extensionManager: ExtensionManager,
servicesManager: ServicesManager,
serviceProvidersManager: ServiceProvidersManager,
hotkeysManager: HotkeysManager;
function App({ config, defaultExtensions, defaultModes }) {
@ -48,6 +55,7 @@ function App({ config, defaultExtensions, defaultModes }) {
commandsManager = init.commandsManager;
extensionManager = init.extensionManager;
servicesManager = init.servicesManager;
serviceProvidersManager = init.serviceProvidersManager;
hotkeysManager = init.hotkeysManager;
// Set appConfig
@ -78,6 +86,15 @@ function App({ config, defaultExtensions, defaultModes }) {
[DialogProvider, { service: uiDialogService }],
[ModalProvider, { service: uiModalService, modal: Modal }],
];
// Loop through and register each of the service providers registered with the ServiceProvidersManager.
const providersFromManager = Object.entries(serviceProvidersManager.providers);
if (providersFromManager.length > 0) {
providersFromManager.forEach(([serviceName, provider]) => {
providers.push([provider, { service: servicesManager.services[serviceName] }]);
});
}
const CombinedProviders = ({ children }) => Compose({ components: providers, children });
let authRoutes = null;

View File

@ -2,6 +2,7 @@ import {
CommandsManager,
ExtensionManager,
ServicesManager,
ServiceProvidersManager,
HotkeysManager,
UINotificationService,
UIModalService,
@ -34,6 +35,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
const commandsManager = new CommandsManager(commandsManagerConfig);
const servicesManager = new ServicesManager(commandsManager);
const serviceProvidersManager = new ServiceProvidersManager();
const hotkeysManager = new HotkeysManager(commandsManager, servicesManager);
const appConfig = {
@ -45,6 +47,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
serviceProvidersManager,
hotkeysManager,
appConfig,
});
@ -130,6 +133,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
commandsManager,
extensionManager,
servicesManager,
serviceProvidersManager,
hotkeysManager,
};
}

View File

@ -32,8 +32,12 @@ import {
ButtonEnums,
} from '@ohif/ui';
import { Types } from '@ohif/ui';
import i18n from '@ohif/i18n';
const PatientInfoVisibility = Types.PatientInfoVisibility;
const { sortBySeriesDate } = utils;
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
@ -527,6 +531,7 @@ function WorkList({
menuOptions={menuOptions}
isReturnEnabled={false}
WhiteLabeling={appConfig.whiteLabeling}
showPatientInfo={PatientInfoVisibility.DISABLED}
/>
<InvestigationalUseDialog dialogConfiguration={appConfig?.investigationalUseDialog} />
<div className="ohif-scrollbar flex grow flex-col overflow-y-auto">

View File

@ -1,7 +1,7 @@
import MODULE_TYPES from './MODULE_TYPES';
import log from '../log';
import { AppConfig } from '../types/AppConfig';
import { PubSubService, ServicesManager } from '../services';
import { PubSubService, ServiceProvidersManager, ServicesManager } from '../services';
import { HotkeysManager, CommandsManager } from '../classes';
import { DataSourceDefinition } from '../types';
@ -10,6 +10,7 @@ import { DataSourceDefinition } from '../types';
*/
export interface ExtensionConstructor {
servicesManager: ServicesManager;
serviceProvidersManager: ServiceProvidersManager;
commandsManager: CommandsManager;
hotkeysManager: HotkeysManager;
appConfig: AppConfig;
@ -28,6 +29,7 @@ export type ExtensionConfiguration = Record<string, unknown>;
export interface ExtensionParams extends ExtensionConstructor {
extensionManager: ExtensionManager;
servicesManager: ServicesManager;
serviceProvidersManager: ServiceProvidersManager;
configuration?: ExtensionConfiguration;
}
@ -71,6 +73,7 @@ export default class ExtensionManager extends PubSubService {
private _commandsManager: CommandsManager;
private _servicesManager: ServicesManager;
private _hotkeysManager: HotkeysManager;
private _serviceProvidersManager: ServiceProvidersManager;
private modulesMap: Record<string, unknown>;
private modules: Record<string, any[]>;
private registeredExtensionIds: string[];
@ -88,6 +91,7 @@ export default class ExtensionManager extends PubSubService {
constructor({
commandsManager,
servicesManager,
serviceProvidersManager,
hotkeysManager,
appConfig = {},
}: ExtensionConstructor) {
@ -98,6 +102,7 @@ export default class ExtensionManager extends PubSubService {
//
this._commandsManager = commandsManager;
this._servicesManager = servicesManager;
this._serviceProvidersManager = serviceProvidersManager;
this._hotkeysManager = hotkeysManager;
this._appConfig = appConfig;
@ -256,6 +261,7 @@ export default class ExtensionManager extends PubSubService {
if (extension.preRegistration) {
await extension.preRegistration({
servicesManager: this._servicesManager,
serviceProvidersManager: this._serviceProvidersManager,
commandsManager: this._commandsManager,
hotkeysManager: this._hotkeysManager,
extensionManager: this,

View File

@ -9,6 +9,7 @@ describe('Top level exports', () => {
'ExtensionManager',
'HotkeysManager',
'ServicesManager',
'ServiceProvidersManager',
//
'defaults',
'utils',

View File

@ -1,5 +1,5 @@
import { ExtensionManager, MODULE_TYPES } from './extensions';
import { ServicesManager } from './services';
import { ServiceProvidersManager, ServicesManager } from './services';
import classes, { CommandsManager, HotkeysManager } from './classes';
import DICOMWeb from './DICOMWeb';
@ -51,6 +51,7 @@ const OHIF = {
ExtensionManager,
HotkeysManager,
ServicesManager,
ServiceProvidersManager,
//
defaults,
utils,
@ -92,6 +93,7 @@ export {
ExtensionManager,
HotkeysManager,
ServicesManager,
ServiceProvidersManager,
//
defaults,
utils,

View File

@ -0,0 +1,31 @@
import log from './../log.js';
/**
* The ServiceProvidersManager allows for a React context provider class to be registered
* for a particular service. This allows for extensions to register services
* with context providers and the providers will be instantiated and added to the
* DOM dynamically.
*/
export default class ServiceProvidersManager {
public providers = {};
public constructor() {
this.providers = {};
}
registerProvider(serviceName, provider) {
if (!serviceName) {
log.warn(
'Attempting to register a provider to a null/undefined service name. Exiting early.'
);
return;
}
if (!provider) {
log.warn('Attempting to register a null/undefined provider. Exiting early.');
return;
}
this.providers[serviceName] = provider;
}
}

View File

@ -42,6 +42,9 @@ class UIModalService {
closeButton = true,
title = null,
customClassName = null,
movable = false,
containerDimensions = null,
contentDimensions = null,
}) {
return serviceImplementation._show({
content,
@ -51,6 +54,9 @@ class UIModalService {
closeButton,
title,
customClassName,
movable,
containerDimensions,
contentDimensions,
});
}

View File

@ -1,5 +1,6 @@
import MeasurementService from './MeasurementService';
import ServicesManager from './ServicesManager';
import ServiceProvidersManager from './ServiceProvidersManager';
import UIDialogService from './UIDialogService';
import UIModalService from './UIModalService';
import UINotificationService from './UINotificationService';
@ -22,6 +23,7 @@ export {
Services,
MeasurementService,
ServicesManager,
ServiceProvidersManager,
CustomizationService,
StateSyncService,
UIDialogService,

View File

@ -133,6 +133,7 @@ export type SyncGroup = {
id: string;
source?: boolean;
target?: boolean;
options?: object;
};
/** Declares a custom option, that is a computed type value */

View File

@ -33,4 +33,5 @@ export default interface Services {
cornerstoneCacheService?: unknown;
segmentationService?: unknown;
panelService?: unknown;
colorbarService?: unknown;
}

View File

@ -189,6 +189,7 @@ if auth headers are used, a preflight request is required.
- `allowMultiSelectExport`: (default to false), if set to true, the user will be able to select the datasource to export the report to.
- `activateViewportBeforeInteraction`: (default to true), if set to false, tools can be used directly without the need to click and activate the viewport.
- `autoPlayCine`: (default to false), if set to true, data sets with the DICOM frame time tag (i.e. (0018,1063)) will auto play when displayed
- `addWindowLevelActionMenu`: (default to true), if set to false, the window level action menu item is NOT added to the viewport action corners
- `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.<br/>
Points to consider while using `dangerouslyUseDynamicConfig`:<br/>
- User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`.

View File

@ -1,4 +1,5 @@
{
"Back to": "Back to {{location}}",
"Close": "Close",
"Image": "Image",
"Layout": "Layout",

View File

@ -0,0 +1,5 @@
{
"Back to Display Options": "Back to Display Options",
"Modality Presets": "{{modality}} Presets",
"Modality Window Presets": "{{modality}} Window Presets"
}

View File

@ -20,6 +20,7 @@ import TrackedCornerstoneViewport from './TrackedCornerstoneViewport.json';
import UserPreferencesModal from './UserPreferencesModal.json';
import ViewportDownloadForm from './ViewportDownloadForm.json';
import Messages from './Messages.json';
import WindowLevelActionMenu from './WindowLevelActionMenu.json';
export default {
'en-US': {
@ -45,5 +46,6 @@ export default {
UserPreferencesModal,
ViewportDownloadForm,
Messages,
WindowLevelActionMenu,
},
};

View File

@ -0,0 +1,5 @@
{
"Back to Display Options": "Test Back to Display Options",
"Modality Presets": "Test {{modality}} Presets",
"Modality Window Presets": "Test {{modality}} Window Presets"
}

View File

@ -22,6 +22,7 @@ import ThumbnailTracked from './ThumbnailTracked.json';
import TrackedCornerstoneViewport from './TrackedCornerstoneViewport.json';
import UserPreferencesModal from './UserPreferencesModal.json';
import ViewportDownloadForm from './ViewportDownloadForm.json';
import WindowLevelActionMenu from './WindowLevelActionMenu.json';
export default {
'test-LNG': {
@ -49,5 +50,6 @@ export default {
TrackedCornerstoneViewport,
UserPreferencesModal,
ViewportDownloadForm,
WindowLevelActionMenu,
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Some files were not shown because too many files have changed in this diff Show More