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