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/,
|
test: /\.wasm/,
|
||||||
type: 'asset/resource',
|
type: 'asset/resource',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpe?g|gif|svg)$/i,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: 'assets/images/[name].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
], //.concat(vtkRules),
|
], //.concat(vtkRules),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import OHIF, { utils } from '@ohif/core';
|
import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
|
||||||
import { ViewportActionBar, useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
|
|
||||||
|
|
||||||
import promptHydrateRT from '../utils/promptHydrateRT';
|
import promptHydrateRT from '../utils/promptHydrateRT';
|
||||||
import _getStatusComponent from './_getStatusComponent';
|
import _getStatusComponent from './_getStatusComponent';
|
||||||
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
|
||||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||||
|
|
||||||
function OHIFCornerstoneRTViewport(props) {
|
function OHIFCornerstoneRTViewport(props) {
|
||||||
@ -15,7 +13,6 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
children,
|
children,
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportOptions,
|
viewportOptions,
|
||||||
viewportLabel,
|
|
||||||
servicesManager,
|
servicesManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
@ -27,6 +24,7 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
segmentationService,
|
segmentationService,
|
||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
customizationService,
|
customizationService,
|
||||||
|
viewportActionCornersService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const viewportId = viewportOptions.viewportId;
|
const viewportId = viewportOptions.viewportId;
|
||||||
@ -126,7 +124,6 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
|
|
||||||
const onSegmentChange = useCallback(
|
const onSegmentChange = useCallback(
|
||||||
direction => {
|
direction => {
|
||||||
direction = direction === 'left' ? -1 : 1;
|
|
||||||
const segmentationId = rtDisplaySet.displaySetInstanceUID;
|
const segmentationId = rtDisplaySet.displaySetInstanceUID;
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
|
||||||
@ -282,20 +279,7 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const onStatusClick = useCallback(async () => {
|
||||||
PatientID,
|
|
||||||
PatientName,
|
|
||||||
PatientSex,
|
|
||||||
PatientAge,
|
|
||||||
SliceThickness,
|
|
||||||
ManufacturerModelName,
|
|
||||||
StudyDate,
|
|
||||||
SeriesDescription,
|
|
||||||
SpacingBetweenSlices,
|
|
||||||
SeriesNumber,
|
|
||||||
} = referencedDisplaySetRef.current.metadata;
|
|
||||||
|
|
||||||
const onStatusClick = async () => {
|
|
||||||
// Before hydrating a RT and make it added to all viewports in the grid
|
// 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
|
// 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
|
// presentation state, so that we can restore it after hydrating the RT. This is
|
||||||
@ -309,41 +293,47 @@ function OHIFCornerstoneRTViewport(props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setIsHydrated(isHydrated);
|
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 (
|
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">
|
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||||
{rtIsLoading && (
|
{rtIsLoading && (
|
||||||
<LoadingIndicatorTotalPercent
|
<LoadingIndicatorTotalPercent
|
||||||
|
|||||||
@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
|||||||
let ToolTipMessage = null;
|
let ToolTipMessage = null;
|
||||||
let StatusIcon = null;
|
let StatusIcon = null;
|
||||||
|
|
||||||
const { t } = useTranslation('Common');
|
|
||||||
const loadStr = t('LOAD');
|
|
||||||
|
|
||||||
switch (isHydrated) {
|
switch (isHydrated) {
|
||||||
case true:
|
case true:
|
||||||
StatusIcon = () => <Icon name="status-alert" />;
|
StatusIcon = () => <Icon name="status-alert" />;
|
||||||
@ -26,7 +23,11 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
|||||||
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
|
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusArea = () => (
|
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="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">
|
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||||
<StatusIcon />
|
<StatusIcon />
|
||||||
@ -43,6 +44,7 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import OHIF, { utils } from '@ohif/core';
|
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||||
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionBar } from '@ohif/ui';
|
|
||||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||||
import _getStatusComponent from './_getStatusComponent';
|
import _getStatusComponent from './_getStatusComponent';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
|
||||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||||
|
|
||||||
function OHIFCornerstoneSEGViewport(props) {
|
function OHIFCornerstoneSEGViewport(props) {
|
||||||
@ -15,7 +13,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
children,
|
children,
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportOptions,
|
viewportOptions,
|
||||||
viewportLabel,
|
|
||||||
servicesManager,
|
servicesManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
@ -30,6 +27,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
segmentationService,
|
segmentationService,
|
||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
customizationService,
|
customizationService,
|
||||||
|
viewportActionCornersService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
||||||
@ -122,7 +120,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
|
|
||||||
const onSegmentChange = useCallback(
|
const onSegmentChange = useCallback(
|
||||||
direction => {
|
direction => {
|
||||||
direction = direction === 'left' ? -1 : 1;
|
|
||||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
|
||||||
@ -247,6 +244,69 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
};
|
};
|
||||||
}, [segDisplaySet]);
|
}, [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;
|
let childrenWithProps = null;
|
||||||
|
|
||||||
@ -282,61 +342,8 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
SpacingBetweenSlices,
|
SpacingBetweenSlices,
|
||||||
} = referencedDisplaySetRef.current.metadata;
|
} = 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 (
|
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">
|
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||||
{segIsLoading && (
|
{segIsLoading && (
|
||||||
<LoadingIndicatorTotalPercent
|
<LoadingIndicatorTotalPercent
|
||||||
|
|||||||
@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
|||||||
let ToolTipMessage = null;
|
let ToolTipMessage = null;
|
||||||
let StatusIcon = null;
|
let StatusIcon = null;
|
||||||
|
|
||||||
const { t } = useTranslation('Common');
|
|
||||||
const loadStr = t('LOAD');
|
|
||||||
|
|
||||||
switch (isHydrated) {
|
switch (isHydrated) {
|
||||||
case true:
|
case true:
|
||||||
StatusIcon = () => <Icon name="status-alert" />;
|
StatusIcon = () => <Icon name="status-alert" />;
|
||||||
@ -26,7 +23,11 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
|||||||
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
|
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusArea = () => (
|
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="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">
|
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||||
<StatusIcon />
|
<StatusIcon />
|
||||||
@ -43,6 +44,7 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,35 +1,30 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { 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 hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
|
||||||
|
|
||||||
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
||||||
|
|
||||||
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||||
|
|
||||||
function OHIFCornerstoneSRViewport(props) {
|
function OHIFCornerstoneSRViewport(props) {
|
||||||
const {
|
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
|
||||||
children,
|
props;
|
||||||
dataSource,
|
|
||||||
displaySets,
|
|
||||||
viewportLabel,
|
|
||||||
viewportOptions,
|
|
||||||
servicesManager,
|
|
||||||
extensionManager,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
const { displaySetService, cornerstoneViewportService, measurementService } =
|
const {
|
||||||
servicesManager.services;
|
displaySetService,
|
||||||
|
cornerstoneViewportService,
|
||||||
|
measurementService,
|
||||||
|
viewportActionCornersService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
const viewportId = viewportOptions.viewportId;
|
const viewportId = viewportOptions.viewportId;
|
||||||
|
|
||||||
@ -48,6 +43,8 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
const [element, setElement] = useState(null);
|
const [element, setElement] = useState(null);
|
||||||
const { viewports, activeViewportId } = viewportGrid;
|
const { viewports, activeViewportId } = viewportGrid;
|
||||||
|
|
||||||
|
const { t } = useTranslation('Common');
|
||||||
|
|
||||||
// Optional hook into tracking extension, if present.
|
// Optional hook into tracking extension, if present.
|
||||||
let trackedMeasurements;
|
let trackedMeasurements;
|
||||||
let sendTrackedMeasurementsEvent;
|
let sendTrackedMeasurementsEvent;
|
||||||
@ -213,19 +210,12 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
direction => {
|
direction => {
|
||||||
let newMeasurementSelected = measurementSelected;
|
let newMeasurementSelected = measurementSelected;
|
||||||
|
|
||||||
if (direction === 'right') {
|
newMeasurementSelected += direction;
|
||||||
newMeasurementSelected++;
|
|
||||||
|
|
||||||
if (newMeasurementSelected >= measurementCount) {
|
if (newMeasurementSelected >= measurementCount) {
|
||||||
newMeasurementSelected = 0;
|
newMeasurementSelected = 0;
|
||||||
}
|
} else if (newMeasurementSelected < 0) {
|
||||||
} else {
|
|
||||||
newMeasurementSelected--;
|
|
||||||
|
|
||||||
if (newMeasurementSelected < 0) {
|
|
||||||
newMeasurementSelected = measurementCount - 1;
|
newMeasurementSelected = measurementCount - 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
setTrackingIdentifiers(newMeasurementSelected);
|
setTrackingIdentifiers(newMeasurementSelected);
|
||||||
updateViewport(newMeasurementSelected);
|
updateViewport(newMeasurementSelected);
|
||||||
@ -298,6 +288,46 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
updateViewport(measurementSelected);
|
updateViewport(measurementSelected);
|
||||||
}, [dataSource, srDisplaySet]);
|
}, [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;
|
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 (
|
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">
|
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||||
{getCornerstoneViewport()}
|
{getCornerstoneViewport()}
|
||||||
{childrenWithProps}
|
{childrenWithProps}
|
||||||
@ -384,7 +365,6 @@ OHIFCornerstoneSRViewport.propTypes = {
|
|||||||
viewportLabel: PropTypes.string,
|
viewportLabel: PropTypes.string,
|
||||||
customProps: PropTypes.object,
|
customProps: PropTypes.object,
|
||||||
viewportOptions: PropTypes.object,
|
viewportOptions: PropTypes.object,
|
||||||
viewportLabel: PropTypes.string,
|
|
||||||
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
||||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||||
};
|
};
|
||||||
@ -429,6 +409,7 @@ function _getStatusComponent({
|
|||||||
isRehydratable,
|
isRehydratable,
|
||||||
isLocked,
|
isLocked,
|
||||||
sendTrackedMeasurementsEvent,
|
sendTrackedMeasurementsEvent,
|
||||||
|
t,
|
||||||
}) {
|
}) {
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
||||||
@ -437,7 +418,6 @@ function _getStatusComponent({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { t } = useTranslation('Common');
|
|
||||||
const loadStr = t('LOAD');
|
const loadStr = t('LOAD');
|
||||||
|
|
||||||
// 1 - Incompatible
|
// 1 - Incompatible
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
utilities as csUtils,
|
utilities as csUtils,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import { MeasurementService } from '@ohif/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 { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
|
||||||
|
|
||||||
import { setEnabledElement } from '../state';
|
import { setEnabledElement } from '../state';
|
||||||
@ -21,6 +21,11 @@ import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/
|
|||||||
import CornerstoneServices from '../types/CornerstoneServices';
|
import CornerstoneServices from '../types/CornerstoneServices';
|
||||||
import CinePlayer from '../components/CinePlayer';
|
import CinePlayer from '../components/CinePlayer';
|
||||||
import { Types } from '@ohif/core';
|
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';
|
import { LutPresentation, PositionPresentation } from '../types/Presentation';
|
||||||
|
|
||||||
const STACK = 'stack';
|
const STACK = 'stack';
|
||||||
@ -116,6 +121,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||||
const elementRef = useRef();
|
const elementRef = useRef();
|
||||||
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
measurementService,
|
measurementService,
|
||||||
@ -127,12 +133,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
cornerstoneCacheService,
|
cornerstoneCacheService,
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
stateSyncService,
|
stateSyncService,
|
||||||
|
viewportActionCornersService,
|
||||||
|
customizationService,
|
||||||
} = servicesManager.services as CornerstoneServices;
|
} = servicesManager.services as CornerstoneServices;
|
||||||
|
|
||||||
const [viewportDialogState] = useViewportDialog();
|
const [viewportDialogState] = useViewportDialog();
|
||||||
// useCallback for scroll bar height calculation
|
// useCallback for scroll bar height calculation
|
||||||
const setImageScrollBarHeight = useCallback(() => {
|
const setImageScrollBarHeight = useCallback(() => {
|
||||||
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
|
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
|
||||||
setScrollbarHeight(scrollbarHeight);
|
setScrollbarHeight(scrollbarHeight);
|
||||||
}, [elementRef]);
|
}, [elementRef]);
|
||||||
|
|
||||||
@ -152,6 +160,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
||||||
|
|
||||||
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
||||||
|
|
||||||
|
viewportActionCornersService.clear(viewportId);
|
||||||
},
|
},
|
||||||
[viewportId]
|
[viewportId]
|
||||||
);
|
);
|
||||||
@ -361,6 +371,34 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
};
|
};
|
||||||
}, [displaySets, elementRef, viewportId]);
|
}, [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 (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="viewport-wrapper">
|
<div className="viewport-wrapper">
|
||||||
@ -388,7 +426,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute w-full">
|
{/* top offset of 24px to account for ViewportActionCorners. */}
|
||||||
|
<div className="absolute top-[24px] w-full">
|
||||||
{viewportDialogState.viewportId === viewportId && (
|
{viewportDialogState.viewportId === viewportId && (
|
||||||
<Notification
|
<Notification
|
||||||
id="viewport-notification"
|
id="viewport-notification"
|
||||||
@ -400,6 +439,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
|
||||||
|
<OHIFViewportActionCorners viewportId={viewportId} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}, areEqual);
|
}, areEqual);
|
||||||
|
|||||||
@ -80,10 +80,13 @@ function CustomizableViewportOverlay({
|
|||||||
'cornerstoneOverlayBottomRight'
|
'cornerstoneOverlayBottomRight'
|
||||||
);
|
);
|
||||||
|
|
||||||
const instance = useMemo(
|
const instance = useMemo(() => {
|
||||||
() => (viewportData ? getViewportInstance(viewportData, imageIndex) : null),
|
if (viewportData != null) {
|
||||||
[viewportData, imageIndex]
|
return _getViewportInstance(viewportData, imageIndex);
|
||||||
);
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [viewportData, imageIndex]);
|
||||||
|
|
||||||
const instanceNumber = useMemo(
|
const instanceNumber = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -220,7 +223,7 @@ function CustomizableViewportOverlay({
|
|||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<div key={`${keyPrefix}_${index}`}>
|
<div key={`${keyPrefix}_${index}`}>
|
||||||
{item?.condition
|
{item?.condition
|
||||||
? item.condition()
|
? item.condition({ instance, formatters: { formatDate: formatDICOMDate } })
|
||||||
? _renderOverlayItem(item)
|
? _renderOverlayItem(item)
|
||||||
: null
|
: null
|
||||||
: _renderOverlayItem(item)}
|
: _renderOverlayItem(item)}
|
||||||
@ -240,6 +243,31 @@ function CustomizableViewportOverlay({
|
|||||||
*/
|
*/
|
||||||
getContent(
|
getContent(
|
||||||
topLeftCustomization,
|
topLeftCustomization,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'StudyDate',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: '',
|
||||||
|
title: 'Study date',
|
||||||
|
condition: ({ instance }) => instance && instance.StudyDate,
|
||||||
|
contentF: ({ instance, formatters: { formatDate } }) =>
|
||||||
|
formatDate(instance.StudyDate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'SeriesDescription',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: '',
|
||||||
|
title: 'Series description',
|
||||||
|
attribute: 'SeriesDescription',
|
||||||
|
condition: ({ instance }) => instance && instance.SeriesDescription,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'topLeftOverlayItem'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
topRight={getContent(topRightCustomization, [], 'topRightOverlayItem')}
|
||||||
|
bottomLeft={getContent(
|
||||||
|
bottomLeftCustomization,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'WindowLevel',
|
id: 'WindowLevel',
|
||||||
@ -254,44 +282,35 @@ function CustomizableViewportOverlay({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'topLeftOverlayItem'
|
'bottomLeftOverlayItem'
|
||||||
)
|
)}
|
||||||
}
|
bottomRight={getContent(
|
||||||
topRight={getContent(
|
bottomRightCustomization,
|
||||||
topRightCustomization,
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'InstanceNumber',
|
id: 'InstanceNumber',
|
||||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'topRightOverlayItem'
|
'bottomRightOverlayItem'
|
||||||
)}
|
)}
|
||||||
bottomLeft={getContent(bottomLeftCustomization, [null], 'bottomLeftOverlayItem')}
|
|
||||||
bottomRight={getContent(bottomRightCustomization, [null], 'bottomRightOverlayItem')}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getViewportInstance = (viewportData, imageIndex) => {
|
function _getViewportInstance(viewportData, imageIndex) {
|
||||||
const { viewportType, data } = viewportData;
|
|
||||||
let imageId = null;
|
let imageId = null;
|
||||||
|
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||||
switch (viewportType) {
|
imageId = viewportData.data.imageIds[imageIndex];
|
||||||
case Enums.ViewportType.STACK:
|
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||||
imageId = data.imageIds[imageIndex];
|
const volumes = viewportData.data;
|
||||||
break;
|
if (volumes && volumes.length == 1) {
|
||||||
case Enums.ViewportType.ORTHOGRAPHIC:
|
const volume = volumes[0];
|
||||||
if (data?.length === 1) {
|
imageId = volume.imageIds[imageIndex];
|
||||||
imageId = data[0].imageIds[imageIndex];
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageId ? metaData.get('instance', imageId) || {} : {};
|
return imageId ? metaData.get('instance', imageId) || {} : {};
|
||||||
};
|
}
|
||||||
|
|
||||||
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
|
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
|
||||||
let instanceNumber;
|
let instanceNumber;
|
||||||
@ -389,9 +408,9 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
|||||||
style={{ color: (customization && customization.color) || undefined }}
|
style={{ color: (customization && customization.color) || undefined }}
|
||||||
>
|
>
|
||||||
<span className="mr-1 shrink-0">W:</span>
|
<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="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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -406,7 +425,7 @@ function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
|
|||||||
style={{ color: (customization && customization.color) || undefined }}
|
style={{ color: (customization && customization.color) || undefined }}
|
||||||
>
|
>
|
||||||
<span className="mr-1 shrink-0">Zoom:</span>
|
<span className="mr-1 shrink-0">Zoom:</span>
|
||||||
<span className="font-light">{scale.toFixed(2)}x</span>
|
<span>{scale.toFixed(2)}x</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -427,7 +446,7 @@ function InstanceNumberOverlayItem({
|
|||||||
style={{ color: (customization && customization.color) || undefined }}
|
style={{ color: (customization && customization.color) || undefined }}
|
||||||
>
|
>
|
||||||
<span className="mr-1 shrink-0">I:</span>
|
<span className="mr-1 shrink-0">I:</span>
|
||||||
<span className="font-light">
|
<span>
|
||||||
{instanceNumber !== undefined && instanceNumber !== null
|
{instanceNumber !== undefined && instanceNumber !== null
|
||||||
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
|
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
|
||||||
: `${imageIndex + 1}/${numberOfSlices}`}
|
: `${imageIndex + 1}/${numberOfSlices}`}
|
||||||
|
|||||||
@ -108,17 +108,15 @@ function ViewportOrientationMarkers({
|
|||||||
console.log('ViewportOrientationMarkers::No viewport');
|
console.log('ViewportOrientationMarkers::No viewport');
|
||||||
return null;
|
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) => (
|
return orientationMarkers.map((m, index) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
'overlay-text',
|
||||||
`${m}-mid orientation-marker`,
|
`${m}-mid orientation-marker`,
|
||||||
isLight ? 'text-[#726F7E]' : 'text-[#ccc]'
|
'text-aqua-pale',
|
||||||
|
'text-[13px]',
|
||||||
|
'leading-5'
|
||||||
)}
|
)}
|
||||||
key={`${m}-mid orientation-marker`}
|
key={`${m}-mid orientation-marker`}
|
||||||
>
|
>
|
||||||
@ -135,7 +133,7 @@ function ViewportOrientationMarkers({
|
|||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <div className="ViewportOrientationMarkers noselect">{markers}</div>;
|
return <div className="ViewportOrientationMarkers select-none">{markers}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewportOrientationMarkers.propTypes = {
|
ViewportOrientationMarkers.propTypes = {
|
||||||
|
|||||||
@ -40,6 +40,8 @@ function commandsModule({
|
|||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
measurementService,
|
measurementService,
|
||||||
|
colorbarService,
|
||||||
|
hangingProtocolService,
|
||||||
syncGroupService,
|
syncGroupService,
|
||||||
} = servicesManager.services as CornerstoneServices;
|
} = servicesManager.services as CornerstoneServices;
|
||||||
|
|
||||||
@ -239,18 +241,12 @@ function commandsModule({
|
|||||||
cineService.setIsCineEnabled(!isCineEnabled);
|
cineService.setIsCineEnabled(!isCineEnabled);
|
||||||
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
|
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
|
||||||
},
|
},
|
||||||
setWindowLevel({ window, level, toolGroupId }) {
|
|
||||||
|
setViewportWindowLevel({ viewportId, window, level }) {
|
||||||
// convert to numbers
|
// convert to numbers
|
||||||
const windowWidthNum = Number(window);
|
const windowWidthNum = Number(window);
|
||||||
const windowCenterNum = Number(level);
|
const windowCenterNum = Number(level);
|
||||||
|
|
||||||
const { viewportId } = _getActiveViewportEnabledElement();
|
|
||||||
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
|
|
||||||
|
|
||||||
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get actor from the viewport
|
// get actor from the viewport
|
||||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
@ -265,6 +261,27 @@ function commandsModule({
|
|||||||
});
|
});
|
||||||
viewport.render();
|
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 }) => {
|
setToolEnabled: ({ toolName, toggle }) => {
|
||||||
const { viewports } = viewportGridService.getState();
|
const { viewports } = viewportGridService.getState();
|
||||||
|
|
||||||
@ -514,18 +531,51 @@ function commandsModule({
|
|||||||
|
|
||||||
cstUtils.scroll(viewport, options);
|
cstUtils.scroll(viewport, options);
|
||||||
},
|
},
|
||||||
setViewportColormap: ({ viewportId, displaySetInstanceUID, colormap, immediate = false }) => {
|
setViewportColormap: ({
|
||||||
|
viewportId,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
colormap,
|
||||||
|
opacity = 1,
|
||||||
|
immediate = false,
|
||||||
|
}) => {
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
const actorEntries = viewport.getActors();
|
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 => {
|
if (displaySetsInfo) {
|
||||||
return actorEntry.uid.includes(displaySetInstanceUID);
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HP takes priority over the default opacity
|
||||||
|
colormap = { ...colormap, opacity: hpOpacity || opacity };
|
||||||
|
|
||||||
|
const setViewportProperties = (viewport, uid) => {
|
||||||
|
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
|
||||||
const { actor: volumeActor, uid: volumeId } = actorEntry;
|
const { actor: volumeActor, uid: volumeId } = actorEntry;
|
||||||
|
|
||||||
viewport.setProperties({ colormap, volumeActor }, volumeId);
|
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) {
|
if (immediate) {
|
||||||
viewport.render();
|
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 }) => {
|
resetCrosshairs: ({ viewportId }) => {
|
||||||
const crosshairInstances = [];
|
const crosshairInstances = [];
|
||||||
|
|
||||||
@ -650,6 +801,11 @@ function commandsModule({
|
|||||||
},
|
},
|
||||||
getNearbyAnnotation: {
|
getNearbyAnnotation: {
|
||||||
commandFn: actions.getNearbyAnnotation,
|
commandFn: actions.getNearbyAnnotation,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
toggleViewportColorbar: {
|
||||||
|
commandFn: actions.toggleViewportColorbar,
|
||||||
},
|
},
|
||||||
deleteMeasurement: {
|
deleteMeasurement: {
|
||||||
commandFn: actions.deleteMeasurement,
|
commandFn: actions.deleteMeasurement,
|
||||||
@ -660,6 +816,9 @@ function commandsModule({
|
|||||||
updateMeasurement: {
|
updateMeasurement: {
|
||||||
commandFn: actions.updateMeasurement,
|
commandFn: actions.updateMeasurement,
|
||||||
},
|
},
|
||||||
|
setViewportWindowLevel: {
|
||||||
|
commandFn: actions.setViewportWindowLevel,
|
||||||
|
},
|
||||||
setWindowLevel: {
|
setWindowLevel: {
|
||||||
commandFn: actions.setWindowLevel,
|
commandFn: actions.setWindowLevel,
|
||||||
},
|
},
|
||||||
@ -754,6 +913,18 @@ function commandsModule({
|
|||||||
attachProtocolViewportDataListener: {
|
attachProtocolViewportDataListener: {
|
||||||
commandFn: actions.attachProtocolViewportDataListener,
|
commandFn: actions.attachProtocolViewportDataListener,
|
||||||
},
|
},
|
||||||
|
setViewportPreset: {
|
||||||
|
commandFn: actions.setViewportPreset,
|
||||||
|
},
|
||||||
|
setVolumeRenderingQulaity: {
|
||||||
|
commandFn: actions.setVolumeRenderingQulaity,
|
||||||
|
},
|
||||||
|
shiftVolumeOpacityPoints: {
|
||||||
|
commandFn: actions.shiftVolumeOpacityPoints,
|
||||||
|
},
|
||||||
|
setVolumeLighting: {
|
||||||
|
commandFn: actions.setVolumeLighting,
|
||||||
|
},
|
||||||
resetCrosshairs: {
|
resetCrosshairs: {
|
||||||
commandFn: actions.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 { Enums } from '@cornerstonejs/tools';
|
||||||
import { toolNames } from './initCornerstoneTools';
|
import { toolNames } from './initCornerstoneTools';
|
||||||
import DicomUpload from './components/DicomUpload/DicomUpload';
|
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 = {
|
const tools = {
|
||||||
active: [
|
active: [
|
||||||
@ -37,6 +43,27 @@ function getCustomizationModule() {
|
|||||||
id: 'cornerstone.overlayViewportTools',
|
id: 'cornerstone.overlayViewportTools',
|
||||||
tools,
|
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',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -94,6 +97,9 @@ export const fourUp = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -117,6 +123,9 @@ export const fourUp = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -97,6 +97,9 @@ export const main3D = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -120,6 +123,9 @@ export const main3D = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -143,6 +149,9 @@ export const main3D = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -87,6 +87,9 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -111,6 +114,9 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -135,6 +141,9 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -59,6 +59,9 @@ export const mprAnd3DVolumeViewport = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -100,6 +103,9 @@ export const mprAnd3DVolumeViewport = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -123,6 +129,9 @@ export const mprAnd3DVolumeViewport = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -97,6 +97,9 @@ export const primary3D = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -120,6 +123,9 @@ export const primary3D = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -143,6 +149,9 @@ export const primary3D = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -72,6 +72,9 @@ export const primaryAxial = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -95,6 +98,9 @@ export const primaryAxial = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -118,6 +124,9 @@ export const primaryAxial = {
|
|||||||
id: 'mpr',
|
id: 'mpr',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import SyncGroupService from './services/SyncGroupService';
|
|||||||
import SegmentationService from './services/SegmentationService';
|
import SegmentationService from './services/SegmentationService';
|
||||||
import CornerstoneCacheService from './services/CornerstoneCacheService';
|
import CornerstoneCacheService from './services/CornerstoneCacheService';
|
||||||
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
|
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
|
||||||
|
import ColorbarService from './services/ColorbarService';
|
||||||
import * as CornerstoneExtensionTypes from './types';
|
import * as CornerstoneExtensionTypes from './types';
|
||||||
|
|
||||||
import { toolNames } from './initCornerstoneTools';
|
import { toolNames } from './initCornerstoneTools';
|
||||||
@ -30,6 +31,8 @@ import { id } from './id';
|
|||||||
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||||
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||||
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||||
|
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
||||||
|
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
|
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
|
||||||
@ -69,13 +72,19 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
||||||
*/
|
*/
|
||||||
preRegistration: function (props: Types.Extensions.ExtensionParams): Promise<void> {
|
preRegistration: function (props: Types.Extensions.ExtensionParams): Promise<void> {
|
||||||
const { servicesManager } = props;
|
const { servicesManager, serviceProvidersManager } = props;
|
||||||
servicesManager.registerService(CornerstoneViewportService.REGISTRATION);
|
servicesManager.registerService(CornerstoneViewportService.REGISTRATION);
|
||||||
servicesManager.registerService(ToolGroupService.REGISTRATION);
|
servicesManager.registerService(ToolGroupService.REGISTRATION);
|
||||||
servicesManager.registerService(SyncGroupService.REGISTRATION);
|
servicesManager.registerService(SyncGroupService.REGISTRATION);
|
||||||
servicesManager.registerService(SegmentationService.REGISTRATION);
|
servicesManager.registerService(SegmentationService.REGISTRATION);
|
||||||
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
|
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
|
||||||
|
servicesManager.registerService(ViewportActionCornersService.REGISTRATION);
|
||||||
|
servicesManager.registerService(ColorbarService.REGISTRATION);
|
||||||
|
|
||||||
|
serviceProvidersManager.registerProvider(
|
||||||
|
ViewportActionCornersService.REGISTRATION.name,
|
||||||
|
ViewportActionCornersProvider
|
||||||
|
);
|
||||||
return init.call(this, props);
|
return init.call(this, props);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,10 @@ import initContextMenu from './initContextMenu';
|
|||||||
import initDoubleClick from './initDoubleClick';
|
import initDoubleClick from './initDoubleClick';
|
||||||
import { CornerstoneServices } from './types';
|
import { CornerstoneServices } from './types';
|
||||||
import initViewTiming from './utils/initViewTiming';
|
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?
|
// TODO: Cypress tests are currently grabbing this from the window?
|
||||||
window.cornerstone = cornerstone;
|
window.cornerstone = cornerstone;
|
||||||
@ -282,10 +286,7 @@ export default async function init({
|
|||||||
|
|
||||||
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
|
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
|
||||||
|
|
||||||
// viewportGridService.subscribe(
|
colormaps.forEach(registerColormap);
|
||||||
// viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
|
||||||
// activeViewportEventListener
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CPUModal() {
|
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 SegmentationService from '../services/SegmentationService';
|
||||||
import CornerstoneCacheService from '../services/CornerstoneCacheService';
|
import CornerstoneCacheService from '../services/CornerstoneCacheService';
|
||||||
import CornerstoneViewportService from '../services/ViewportService/CornerstoneViewportService';
|
import CornerstoneViewportService from '../services/ViewportService/CornerstoneViewportService';
|
||||||
|
import ViewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService';
|
||||||
|
|
||||||
interface CornerstoneServices extends Types.Services {
|
interface CornerstoneServices extends Types.Services {
|
||||||
cornerstoneViewportService: CornerstoneViewportService;
|
cornerstoneViewportService: CornerstoneViewportService;
|
||||||
@ -11,6 +12,7 @@ interface CornerstoneServices extends Types.Services {
|
|||||||
syncGroupService: SyncGroupService;
|
syncGroupService: SyncGroupService;
|
||||||
segmentationService: SegmentationService;
|
segmentationService: SegmentationService;
|
||||||
cornerstoneCacheService: CornerstoneCacheService;
|
cornerstoneCacheService: CornerstoneCacheService;
|
||||||
|
viewportActionCornersService: ViewportActionCornersService;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CornerstoneServices;
|
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}
|
isReturnEnabled={!!appConfig.showStudyList}
|
||||||
onClickReturnButton={onClickReturnButton}
|
onClickReturnButton={onClickReturnButton}
|
||||||
WhiteLabeling={appConfig.whiteLabeling}
|
WhiteLabeling={appConfig.whiteLabeling}
|
||||||
|
showPatientInfo={appConfig.showPatientInfo}
|
||||||
|
servicesManager={servicesManager}
|
||||||
>
|
>
|
||||||
<ErrorBoundary context="Primary Toolbar">
|
<ErrorBoundary context="Primary Toolbar">
|
||||||
<div className="relative flex justify-center">
|
<div className="relative flex justify-center">
|
||||||
|
|||||||
@ -401,15 +401,6 @@ function _mapDisplaySets(
|
|||||||
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
|
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
|
||||||
const componentType = _getComponentType(ds);
|
const componentType = _getComponentType(ds);
|
||||||
const numPanes = viewportGridService.getNumViewportPanes();
|
const numPanes = viewportGridService.getNumViewportPanes();
|
||||||
const viewportIdentificator = [];
|
|
||||||
|
|
||||||
if (numPanes !== 1) {
|
|
||||||
viewports.forEach(viewportData => {
|
|
||||||
if (viewportData?.displaySetInstanceUIDs?.includes(ds.displaySetInstanceUID)) {
|
|
||||||
viewportIdentificator.push(viewportData.viewportLabel);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const array =
|
const array =
|
||||||
componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets;
|
componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets;
|
||||||
@ -435,7 +426,6 @@ function _mapDisplaySets(
|
|||||||
},
|
},
|
||||||
isTracked: trackedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID),
|
isTracked: trackedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID),
|
||||||
isHydratedForDerivedDisplaySet: ds.isHydrated,
|
isHydratedForDerivedDisplaySet: ds.isHydrated,
|
||||||
viewportIdentificator,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (componentType === 'thumbnailNoImage') {
|
if (componentType === 'thumbnailNoImage') {
|
||||||
|
|||||||
@ -1,27 +1,29 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import OHIF, { utils } from '@ohif/core';
|
|
||||||
|
|
||||||
import { ViewportActionBar, Tooltip, Icon } from '@ohif/ui';
|
import { Tooltip, Icon, ViewportActionArrows, useViewportGrid } from '@ohif/ui';
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { annotation } from '@cornerstonejs/tools';
|
import { annotation } from '@cornerstonejs/tools';
|
||||||
import { useTrackedMeasurements } from './../getContextModule';
|
import { useTrackedMeasurements } from './../getContextModule';
|
||||||
import { BaseVolumeViewport, Enums } from '@cornerstonejs/core';
|
import { BaseVolumeViewport, Enums } from '@cornerstonejs/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
const { formatDate } = utils;
|
|
||||||
|
|
||||||
function TrackedCornerstoneViewport(props) {
|
function TrackedCornerstoneViewport(props) {
|
||||||
const { displaySets, viewportId, viewportLabel, servicesManager, extensionManager } = props;
|
const { displaySets, viewportId, servicesManager, extensionManager } = props;
|
||||||
|
|
||||||
const { t } = useTranslation('Common');
|
const {
|
||||||
|
measurementService,
|
||||||
const { measurementService, cornerstoneViewportService, viewportGridService } =
|
cornerstoneViewportService,
|
||||||
servicesManager.services;
|
viewportGridService,
|
||||||
|
viewportActionCornersService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
// Todo: handling more than one displaySet on the same viewport
|
// Todo: handling more than one displaySet on the same viewport
|
||||||
const displaySet = displaySets[0];
|
const displaySet = displaySets[0];
|
||||||
|
const { t } = useTranslation('Common');
|
||||||
|
|
||||||
|
const [viewportGrid] = useViewportGrid();
|
||||||
|
const { activeViewportId } = viewportGrid;
|
||||||
|
|
||||||
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
||||||
|
|
||||||
@ -31,18 +33,7 @@ function TrackedCornerstoneViewport(props) {
|
|||||||
|
|
||||||
const { trackedSeries } = trackedMeasurements.context;
|
const { trackedSeries } = trackedMeasurements.context;
|
||||||
|
|
||||||
const { SeriesDate, SeriesDescription, SeriesInstanceUID, SeriesNumber } = displaySet;
|
const { SeriesInstanceUID } = displaySet;
|
||||||
|
|
||||||
const {
|
|
||||||
PatientID,
|
|
||||||
PatientName,
|
|
||||||
PatientSex,
|
|
||||||
PatientAge,
|
|
||||||
SliceThickness,
|
|
||||||
SpacingBetweenSlices,
|
|
||||||
StudyDate,
|
|
||||||
ManufacturerModelName,
|
|
||||||
} = displaySet.images[0];
|
|
||||||
|
|
||||||
const updateIsTracked = useCallback(() => {
|
const updateIsTracked = useCallback(() => {
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
@ -170,7 +161,8 @@ function TrackedCornerstoneViewport(props) {
|
|||||||
};
|
};
|
||||||
}, [measurementService, sendTrackedMeasurementsEvent, viewportId, viewportGridService]);
|
}, [measurementService, sendTrackedMeasurementsEvent, viewportId, viewportGridService]);
|
||||||
|
|
||||||
function switchMeasurement(direction) {
|
const switchMeasurement = useCallback(
|
||||||
|
direction => {
|
||||||
const newTrackedMeasurementUID = _getNextMeasurementUID(
|
const newTrackedMeasurementUID = _getNextMeasurementUID(
|
||||||
direction,
|
direction,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
@ -185,7 +177,35 @@ function TrackedCornerstoneViewport(props) {
|
|||||||
setTrackedMeasurementUID(newTrackedMeasurementUID);
|
setTrackedMeasurementUID(newTrackedMeasurementUID);
|
||||||
|
|
||||||
measurementService.jumpToMeasurement(viewportId, newTrackedMeasurementUID);
|
measurementService.jumpToMeasurement(viewportId, newTrackedMeasurementUID);
|
||||||
}
|
},
|
||||||
|
[measurementService, servicesManager, trackedMeasurementUID, trackedMeasurements, viewportId]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const statusComponent = _getStatusComponent(isTracked, t);
|
||||||
|
const arrowsComponent = _getArrowsComponent(
|
||||||
|
isTracked,
|
||||||
|
switchMeasurement,
|
||||||
|
viewportId === activeViewportId
|
||||||
|
);
|
||||||
|
|
||||||
|
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 getCornerstoneViewport = () => {
|
||||||
const { component: Component } = extensionManager.getModuleEntry(
|
const { component: Component } = extensionManager.getModuleEntry(
|
||||||
@ -203,35 +223,6 @@ function TrackedCornerstoneViewport(props) {
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||||
{getCornerstoneViewport()}
|
{getCornerstoneViewport()}
|
||||||
</div>
|
</div>
|
||||||
@ -291,29 +282,36 @@ function _getNextMeasurementUID(
|
|||||||
// Not tracking a measurement, or previous measurement now deleted, revert to 0.
|
// Not tracking a measurement, or previous measurement now deleted, revert to 0.
|
||||||
measurementIndex = 0;
|
measurementIndex = 0;
|
||||||
} else {
|
} else {
|
||||||
if (direction === 'left') {
|
measurementIndex += direction;
|
||||||
measurementIndex--;
|
|
||||||
|
|
||||||
if (measurementIndex < 0) {
|
if (measurementIndex < 0) {
|
||||||
measurementIndex = measurementCount - 1;
|
measurementIndex = measurementCount - 1;
|
||||||
}
|
} else if (measurementIndex === measurementCount) {
|
||||||
} else if (direction === 'right') {
|
|
||||||
measurementIndex++;
|
|
||||||
|
|
||||||
if (measurementIndex === measurementCount) {
|
|
||||||
measurementIndex = 0;
|
measurementIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const newTrackedMeasurementId = uids[measurementIndex];
|
const newTrackedMeasurementId = uids[measurementIndex];
|
||||||
|
|
||||||
return newTrackedMeasurementId;
|
return newTrackedMeasurementId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getStatusComponent(isTracked) {
|
const _getArrowsComponent = (isTracked, switchMeasurement, isActiveViewport) => {
|
||||||
const { t } = useTranslation('TrackedCornerstoneViewport');
|
if (!isTracked) {
|
||||||
const trackedIcon = isTracked ? 'status-tracked' : 'status-untracked';
|
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 (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -344,7 +342,7 @@ function _getStatusComponent(isTracked) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={trackedIcon}
|
name={'viewport-status-tracked'}
|
||||||
className="text-aqua-pale"
|
className="text-aqua-pale"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -508,13 +508,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
displaySetInstanceUID: ptDisplaySet.displaySetInstanceUID,
|
displaySetInstanceUID: ptDisplaySet.displaySetInstanceUID,
|
||||||
colormap: {
|
colormap: {
|
||||||
name: 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 { addTool, RectangleROIStartEndThresholdTool } from '@cornerstonejs/tools';
|
||||||
import { utilities } from '@cornerstonejs/core';
|
|
||||||
|
|
||||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
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_NAME = 'Cornerstone3DTools';
|
||||||
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||||
@ -38,5 +34,4 @@ export default function init({ servicesManager }) {
|
|||||||
RectangleROIStartEndThreshold.toAnnotation,
|
RectangleROIStartEndThreshold.toAnnotation,
|
||||||
RectangleROIStartEndThreshold.toMeasurement
|
RectangleROIStartEndThreshold.toMeasurement
|
||||||
);
|
);
|
||||||
colormaps.forEach(registerColormap);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,9 @@ const ctAXIAL = {
|
|||||||
id: 'ctWLSync',
|
id: 'ctWLSync',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -48,6 +51,9 @@ const ctSAGITTAL = {
|
|||||||
id: 'ctWLSync',
|
id: 'ctWLSync',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -75,6 +81,9 @@ const ctCORONAL = {
|
|||||||
id: 'ctWLSync',
|
id: 'ctWLSync',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -108,6 +117,9 @@ const ptAXIAL = {
|
|||||||
id: 'ptWLSync',
|
id: 'ptWLSync',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'voi',
|
type: 'voi',
|
||||||
@ -152,6 +164,9 @@ const ptSAGITTAL = {
|
|||||||
id: 'ptWLSync',
|
id: 'ptWLSync',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'voi',
|
type: 'voi',
|
||||||
@ -196,6 +211,9 @@ const ptCORONAL = {
|
|||||||
id: 'ptWLSync',
|
id: 'ptWLSync',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'voi',
|
type: 'voi',
|
||||||
@ -249,6 +267,9 @@ const fusionAXIAL = {
|
|||||||
id: 'fusionWLSync',
|
id: 'fusionWLSync',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'voi',
|
type: 'voi',
|
||||||
@ -312,6 +333,9 @@ const fusionSAGITTAL = {
|
|||||||
id: 'fusionWLSync',
|
id: 'fusionWLSync',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'voi',
|
type: 'voi',
|
||||||
@ -375,6 +399,9 @@ const fusionCORONAL = {
|
|||||||
id: 'fusionWLSync',
|
id: 'fusionWLSync',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'voi',
|
type: 'voi',
|
||||||
@ -423,6 +450,9 @@ const mipSAGITTAL = {
|
|||||||
id: 'ptWLSync',
|
id: 'ptWLSync',
|
||||||
source: true,
|
source: true,
|
||||||
target: true,
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncColormap: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'voi',
|
type: 'voi',
|
||||||
|
|||||||
@ -114,7 +114,7 @@ const toolbarButtons: Button[] = [
|
|||||||
evaluate: 'evaluate.cornerstoneTool',
|
evaluate: 'evaluate.cornerstoneTool',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Window Level + Presets...
|
// Window Level
|
||||||
{
|
{
|
||||||
id: 'WindowLevel',
|
id: 'WindowLevel',
|
||||||
uiType: 'ohif.splitButton',
|
uiType: 'ohif.splitButton',
|
||||||
|
|||||||
@ -7,29 +7,6 @@ import type { Button } from '@ohif/core/types';
|
|||||||
const { windowLevelPresets } = defaults;
|
const { windowLevelPresets } = defaults;
|
||||||
const { createButton } = ToolbarService;
|
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 = {
|
export const setToolActiveToolbar = {
|
||||||
commandName: 'setToolActiveToolbar',
|
commandName: 'setToolActiveToolbar',
|
||||||
commandOptions: {
|
commandOptions: {
|
||||||
@ -111,33 +88,15 @@ const toolbarButtons: Button[] = [
|
|||||||
evaluate: 'evaluate.cornerstoneTool',
|
evaluate: 'evaluate.cornerstoneTool',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Window Level + Presets...
|
// Window Level
|
||||||
{
|
{
|
||||||
id: 'WindowLevel',
|
id: 'WindowLevel',
|
||||||
uiType: 'ohif.splitButton',
|
uiType: 'ohif.radioGroup',
|
||||||
props: {
|
props: {
|
||||||
groupId: 'WindowLevel',
|
|
||||||
primary: createButton({
|
|
||||||
id: 'WindowLevel',
|
|
||||||
icon: 'tool-window-level',
|
icon: 'tool-window-level',
|
||||||
label: 'Window Level',
|
label: 'Window Level',
|
||||||
tooltip: 'Window Level',
|
|
||||||
commands: setToolActiveToolbar,
|
commands: setToolActiveToolbar,
|
||||||
evaluate: 'evaluate.cornerstoneTool',
|
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'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Pan...
|
// Pan...
|
||||||
|
|||||||
@ -49,29 +49,12 @@ const toolbarButtons: Button[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'WindowLevel',
|
id: 'WindowLevel',
|
||||||
uiType: 'ohif.splitButton',
|
uiType: 'ohif.radioGroup',
|
||||||
props: {
|
props: {
|
||||||
groupId: 'WindowLevel',
|
|
||||||
primary: createButton({
|
|
||||||
id: 'WindowLevel',
|
|
||||||
icon: 'tool-window-level',
|
icon: 'tool-window-level',
|
||||||
label: 'Window Level',
|
label: 'Window Level',
|
||||||
tooltip: 'Window Level',
|
|
||||||
commands: setToolActiveToolbar,
|
commands: setToolActiveToolbar,
|
||||||
evaluate: 'evaluate.cornerstoneTool',
|
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'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -90,7 +90,6 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
'Pan',
|
'Pan',
|
||||||
'SyncToggle',
|
'SyncToggle',
|
||||||
'RectangleROIStartEndThreshold',
|
'RectangleROIStartEndThreshold',
|
||||||
'fusionPTColormap',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// For the hanging protocol we need to decide on the window level
|
// For the hanging protocol we need to decide on the window level
|
||||||
|
|||||||
@ -103,29 +103,12 @@ const toolbarButtons = [
|
|||||||
// Window Level + Presets
|
// Window Level + Presets
|
||||||
{
|
{
|
||||||
id: 'WindowLevel',
|
id: 'WindowLevel',
|
||||||
uiType: 'ohif.splitButton',
|
uiType: 'ohif.radioGroup',
|
||||||
props: {
|
props: {
|
||||||
groupId: 'WindowLevel',
|
|
||||||
primary: ToolbarService.createButton({
|
|
||||||
id: 'WindowLevel',
|
|
||||||
icon: 'tool-window-level',
|
icon: 'tool-window-level',
|
||||||
label: 'Window Level',
|
label: 'Window Level',
|
||||||
tooltip: 'Window Level',
|
|
||||||
commands: setToolActiveToolbar,
|
commands: setToolActiveToolbar,
|
||||||
evaluate: 'evaluate.cornerstoneTool',
|
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'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Crosshairs Button
|
// Crosshairs Button
|
||||||
@ -161,38 +144,6 @@ const toolbarButtons = [
|
|||||||
evaluate: 'evaluate.cornerstoneTool',
|
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;
|
export default toolbarButtons;
|
||||||
|
|||||||
@ -15,7 +15,7 @@ describe('OHIF Multi Study', () => {
|
|||||||
cy.get('[data-cy="viewport-pane"]').as('viewportPane');
|
cy.get('[data-cy="viewport-pane"]').as('viewportPane');
|
||||||
cy.get('@viewportPane').its('length').should('be.eq', 4);
|
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 => {
|
cy.get('@studyDate').should(studyDate => {
|
||||||
expect(studyDate.length).to.be.eq(4);
|
expect(studyDate.length).to.be.eq(4);
|
||||||
|
|||||||
@ -36,6 +36,6 @@ describe('OHIF HP', () => {
|
|||||||
cy.initCommonElementsAliases();
|
cy.initCommonElementsAliases();
|
||||||
|
|
||||||
// The specified series/sop UID's are index 101, so ensure that image is displayed
|
// 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', () => {
|
it('checks if hotkeys "ArrowUp" and "ArrowDown" can navigate in the stack', () => {
|
||||||
// Hotkey ArrowDown
|
// Hotkey ArrowDown
|
||||||
cy.get('body').type('{downarrow}');
|
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
|
// Hotkey ArrowUp
|
||||||
cy.get('body').type('{uparrow}');
|
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', () => {
|
it('checks if hotkeys "V" and "H" can flip the image', () => {
|
||||||
|
|||||||
@ -83,7 +83,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
|||||||
.trigger('mouseup', { buttons: 1 });
|
.trigger('mouseup', { buttons: 1 });
|
||||||
|
|
||||||
// The exact text is slightly dependent on the viewport resolution, so leave a range
|
// 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();
|
const text = $txt.text();
|
||||||
expect(text).to.include('W:118').include('L:479');
|
expect(text).to.include('W:118').include('L:479');
|
||||||
});
|
});
|
||||||
@ -143,7 +143,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
|||||||
cy.resetViewport();
|
cy.resetViewport();
|
||||||
|
|
||||||
const expectedText = 'W:958L:479';
|
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', () => {
|
/*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 () {
|
it('scrolls series stack using scrollbar', function () {
|
||||||
cy.scrollToIndex(13);
|
cy.scrollToIndex(13);
|
||||||
|
|
||||||
cy.get('@viewportInfoTopRight').should('contains.text', '14');
|
cy.get('@viewportInfoBottomRight').should('contains.text', '14');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('performs right click to zoom', function () {
|
it('performs right click to zoom', function () {
|
||||||
@ -32,7 +32,7 @@ describe('OHIF General Viewer', function () {
|
|||||||
.trigger('mouseup');
|
.trigger('mouseup');
|
||||||
|
|
||||||
// make sure the new zoom level is less than the initial
|
// 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);
|
const zoomLevelFinal = $viewportInfo.text().substring(6, 9);
|
||||||
expect(zoomLevelFinal < zoomLevelInitial).to.eq(true);
|
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
|
// Reset to default tool so that the new add length works
|
||||||
cy.addLengthMeasurement([100, 100], [200, 200]); //Adding measurement in the viewport
|
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
|
// Click on first measurement item
|
||||||
cy.get('[data-cy="measurement-item"]').eq(0).click();
|
cy.get('[data-cy="measurement-item"]').eq(0).click();
|
||||||
|
|
||||||
cy.get('@viewportInfoTopRight').should('contains.text', '(1/');
|
cy.get('@viewportInfoBottomRight').should('contains.text', '(1/');
|
||||||
cy.get('@viewportInfoTopRight').should('not.contains.text', '(14/');
|
cy.get('@viewportInfoBottomRight').should('not.contains.text', '(14/');
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -31,31 +31,8 @@ describe('OHIF MPR', () => {
|
|||||||
cy.wait(250);
|
cy.wait(250);
|
||||||
cy.get('[data-cy="MPR"]').click();
|
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('.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
|
// check cornerstone to see if each has images
|
||||||
// we can later do visual testing to match the images with a baseline
|
// we can later do visual testing to match the images with a baseline
|
||||||
cy.window()
|
cy.window()
|
||||||
@ -81,9 +58,6 @@ describe('OHIF MPR', () => {
|
|||||||
cy.get('[data-cy="MPR"]').click();
|
cy.get('[data-cy="MPR"]').click();
|
||||||
|
|
||||||
cy.get('.cornerstone-canvas').should('have.length', 1);
|
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', () => {
|
it('should correctly render Crosshairs for MPR', () => {
|
||||||
|
|||||||
@ -5,7 +5,13 @@ import i18n from '@ohif/i18n';
|
|||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import Compose from './routes/Mode/Compose';
|
import Compose from './routes/Mode/Compose';
|
||||||
import { ServicesManager, ExtensionManager, CommandsManager, HotkeysManager } from '@ohif/core';
|
import {
|
||||||
|
ServicesManager,
|
||||||
|
ExtensionManager,
|
||||||
|
CommandsManager,
|
||||||
|
HotkeysManager,
|
||||||
|
ServiceProvidersManager,
|
||||||
|
} from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
DialogProvider,
|
DialogProvider,
|
||||||
Modal,
|
Modal,
|
||||||
@ -28,6 +34,7 @@ import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes';
|
|||||||
let commandsManager: CommandsManager,
|
let commandsManager: CommandsManager,
|
||||||
extensionManager: ExtensionManager,
|
extensionManager: ExtensionManager,
|
||||||
servicesManager: ServicesManager,
|
servicesManager: ServicesManager,
|
||||||
|
serviceProvidersManager: ServiceProvidersManager,
|
||||||
hotkeysManager: HotkeysManager;
|
hotkeysManager: HotkeysManager;
|
||||||
|
|
||||||
function App({ config, defaultExtensions, defaultModes }) {
|
function App({ config, defaultExtensions, defaultModes }) {
|
||||||
@ -48,6 +55,7 @@ function App({ config, defaultExtensions, defaultModes }) {
|
|||||||
commandsManager = init.commandsManager;
|
commandsManager = init.commandsManager;
|
||||||
extensionManager = init.extensionManager;
|
extensionManager = init.extensionManager;
|
||||||
servicesManager = init.servicesManager;
|
servicesManager = init.servicesManager;
|
||||||
|
serviceProvidersManager = init.serviceProvidersManager;
|
||||||
hotkeysManager = init.hotkeysManager;
|
hotkeysManager = init.hotkeysManager;
|
||||||
|
|
||||||
// Set appConfig
|
// Set appConfig
|
||||||
@ -78,6 +86,15 @@ function App({ config, defaultExtensions, defaultModes }) {
|
|||||||
[DialogProvider, { service: uiDialogService }],
|
[DialogProvider, { service: uiDialogService }],
|
||||||
[ModalProvider, { service: uiModalService, modal: Modal }],
|
[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 });
|
const CombinedProviders = ({ children }) => Compose({ components: providers, children });
|
||||||
|
|
||||||
let authRoutes = null;
|
let authRoutes = null;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import {
|
|||||||
CommandsManager,
|
CommandsManager,
|
||||||
ExtensionManager,
|
ExtensionManager,
|
||||||
ServicesManager,
|
ServicesManager,
|
||||||
|
ServiceProvidersManager,
|
||||||
HotkeysManager,
|
HotkeysManager,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
@ -34,6 +35,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
|||||||
|
|
||||||
const commandsManager = new CommandsManager(commandsManagerConfig);
|
const commandsManager = new CommandsManager(commandsManagerConfig);
|
||||||
const servicesManager = new ServicesManager(commandsManager);
|
const servicesManager = new ServicesManager(commandsManager);
|
||||||
|
const serviceProvidersManager = new ServiceProvidersManager();
|
||||||
const hotkeysManager = new HotkeysManager(commandsManager, servicesManager);
|
const hotkeysManager = new HotkeysManager(commandsManager, servicesManager);
|
||||||
|
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
@ -45,6 +47,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
|||||||
const extensionManager = new ExtensionManager({
|
const extensionManager = new ExtensionManager({
|
||||||
commandsManager,
|
commandsManager,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
|
serviceProvidersManager,
|
||||||
hotkeysManager,
|
hotkeysManager,
|
||||||
appConfig,
|
appConfig,
|
||||||
});
|
});
|
||||||
@ -130,6 +133,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
|||||||
commandsManager,
|
commandsManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
|
serviceProvidersManager,
|
||||||
hotkeysManager,
|
hotkeysManager,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,12 @@ import {
|
|||||||
ButtonEnums,
|
ButtonEnums,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
|
|
||||||
|
import { Types } from '@ohif/ui';
|
||||||
|
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
|
|
||||||
|
const PatientInfoVisibility = Types.PatientInfoVisibility;
|
||||||
|
|
||||||
const { sortBySeriesDate } = utils;
|
const { sortBySeriesDate } = utils;
|
||||||
|
|
||||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
||||||
@ -527,6 +531,7 @@ function WorkList({
|
|||||||
menuOptions={menuOptions}
|
menuOptions={menuOptions}
|
||||||
isReturnEnabled={false}
|
isReturnEnabled={false}
|
||||||
WhiteLabeling={appConfig.whiteLabeling}
|
WhiteLabeling={appConfig.whiteLabeling}
|
||||||
|
showPatientInfo={PatientInfoVisibility.DISABLED}
|
||||||
/>
|
/>
|
||||||
<InvestigationalUseDialog dialogConfiguration={appConfig?.investigationalUseDialog} />
|
<InvestigationalUseDialog dialogConfiguration={appConfig?.investigationalUseDialog} />
|
||||||
<div className="ohif-scrollbar flex grow flex-col overflow-y-auto">
|
<div className="ohif-scrollbar flex grow flex-col overflow-y-auto">
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import MODULE_TYPES from './MODULE_TYPES';
|
import MODULE_TYPES from './MODULE_TYPES';
|
||||||
import log from '../log';
|
import log from '../log';
|
||||||
import { AppConfig } from '../types/AppConfig';
|
import { AppConfig } from '../types/AppConfig';
|
||||||
import { PubSubService, ServicesManager } from '../services';
|
import { PubSubService, ServiceProvidersManager, ServicesManager } from '../services';
|
||||||
import { HotkeysManager, CommandsManager } from '../classes';
|
import { HotkeysManager, CommandsManager } from '../classes';
|
||||||
import { DataSourceDefinition } from '../types';
|
import { DataSourceDefinition } from '../types';
|
||||||
|
|
||||||
@ -10,6 +10,7 @@ import { DataSourceDefinition } from '../types';
|
|||||||
*/
|
*/
|
||||||
export interface ExtensionConstructor {
|
export interface ExtensionConstructor {
|
||||||
servicesManager: ServicesManager;
|
servicesManager: ServicesManager;
|
||||||
|
serviceProvidersManager: ServiceProvidersManager;
|
||||||
commandsManager: CommandsManager;
|
commandsManager: CommandsManager;
|
||||||
hotkeysManager: HotkeysManager;
|
hotkeysManager: HotkeysManager;
|
||||||
appConfig: AppConfig;
|
appConfig: AppConfig;
|
||||||
@ -28,6 +29,7 @@ export type ExtensionConfiguration = Record<string, unknown>;
|
|||||||
export interface ExtensionParams extends ExtensionConstructor {
|
export interface ExtensionParams extends ExtensionConstructor {
|
||||||
extensionManager: ExtensionManager;
|
extensionManager: ExtensionManager;
|
||||||
servicesManager: ServicesManager;
|
servicesManager: ServicesManager;
|
||||||
|
serviceProvidersManager: ServiceProvidersManager;
|
||||||
configuration?: ExtensionConfiguration;
|
configuration?: ExtensionConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +73,7 @@ export default class ExtensionManager extends PubSubService {
|
|||||||
private _commandsManager: CommandsManager;
|
private _commandsManager: CommandsManager;
|
||||||
private _servicesManager: ServicesManager;
|
private _servicesManager: ServicesManager;
|
||||||
private _hotkeysManager: HotkeysManager;
|
private _hotkeysManager: HotkeysManager;
|
||||||
|
private _serviceProvidersManager: ServiceProvidersManager;
|
||||||
private modulesMap: Record<string, unknown>;
|
private modulesMap: Record<string, unknown>;
|
||||||
private modules: Record<string, any[]>;
|
private modules: Record<string, any[]>;
|
||||||
private registeredExtensionIds: string[];
|
private registeredExtensionIds: string[];
|
||||||
@ -88,6 +91,7 @@ export default class ExtensionManager extends PubSubService {
|
|||||||
constructor({
|
constructor({
|
||||||
commandsManager,
|
commandsManager,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
|
serviceProvidersManager,
|
||||||
hotkeysManager,
|
hotkeysManager,
|
||||||
appConfig = {},
|
appConfig = {},
|
||||||
}: ExtensionConstructor) {
|
}: ExtensionConstructor) {
|
||||||
@ -98,6 +102,7 @@ export default class ExtensionManager extends PubSubService {
|
|||||||
//
|
//
|
||||||
this._commandsManager = commandsManager;
|
this._commandsManager = commandsManager;
|
||||||
this._servicesManager = servicesManager;
|
this._servicesManager = servicesManager;
|
||||||
|
this._serviceProvidersManager = serviceProvidersManager;
|
||||||
this._hotkeysManager = hotkeysManager;
|
this._hotkeysManager = hotkeysManager;
|
||||||
this._appConfig = appConfig;
|
this._appConfig = appConfig;
|
||||||
|
|
||||||
@ -256,6 +261,7 @@ export default class ExtensionManager extends PubSubService {
|
|||||||
if (extension.preRegistration) {
|
if (extension.preRegistration) {
|
||||||
await extension.preRegistration({
|
await extension.preRegistration({
|
||||||
servicesManager: this._servicesManager,
|
servicesManager: this._servicesManager,
|
||||||
|
serviceProvidersManager: this._serviceProvidersManager,
|
||||||
commandsManager: this._commandsManager,
|
commandsManager: this._commandsManager,
|
||||||
hotkeysManager: this._hotkeysManager,
|
hotkeysManager: this._hotkeysManager,
|
||||||
extensionManager: this,
|
extensionManager: this,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ describe('Top level exports', () => {
|
|||||||
'ExtensionManager',
|
'ExtensionManager',
|
||||||
'HotkeysManager',
|
'HotkeysManager',
|
||||||
'ServicesManager',
|
'ServicesManager',
|
||||||
|
'ServiceProvidersManager',
|
||||||
//
|
//
|
||||||
'defaults',
|
'defaults',
|
||||||
'utils',
|
'utils',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ExtensionManager, MODULE_TYPES } from './extensions';
|
import { ExtensionManager, MODULE_TYPES } from './extensions';
|
||||||
import { ServicesManager } from './services';
|
import { ServiceProvidersManager, ServicesManager } from './services';
|
||||||
import classes, { CommandsManager, HotkeysManager } from './classes';
|
import classes, { CommandsManager, HotkeysManager } from './classes';
|
||||||
|
|
||||||
import DICOMWeb from './DICOMWeb';
|
import DICOMWeb from './DICOMWeb';
|
||||||
@ -51,6 +51,7 @@ const OHIF = {
|
|||||||
ExtensionManager,
|
ExtensionManager,
|
||||||
HotkeysManager,
|
HotkeysManager,
|
||||||
ServicesManager,
|
ServicesManager,
|
||||||
|
ServiceProvidersManager,
|
||||||
//
|
//
|
||||||
defaults,
|
defaults,
|
||||||
utils,
|
utils,
|
||||||
@ -92,6 +93,7 @@ export {
|
|||||||
ExtensionManager,
|
ExtensionManager,
|
||||||
HotkeysManager,
|
HotkeysManager,
|
||||||
ServicesManager,
|
ServicesManager,
|
||||||
|
ServiceProvidersManager,
|
||||||
//
|
//
|
||||||
defaults,
|
defaults,
|
||||||
utils,
|
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,
|
closeButton = true,
|
||||||
title = null,
|
title = null,
|
||||||
customClassName = null,
|
customClassName = null,
|
||||||
|
movable = false,
|
||||||
|
containerDimensions = null,
|
||||||
|
contentDimensions = null,
|
||||||
}) {
|
}) {
|
||||||
return serviceImplementation._show({
|
return serviceImplementation._show({
|
||||||
content,
|
content,
|
||||||
@ -51,6 +54,9 @@ class UIModalService {
|
|||||||
closeButton,
|
closeButton,
|
||||||
title,
|
title,
|
||||||
customClassName,
|
customClassName,
|
||||||
|
movable,
|
||||||
|
containerDimensions,
|
||||||
|
contentDimensions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import MeasurementService from './MeasurementService';
|
import MeasurementService from './MeasurementService';
|
||||||
import ServicesManager from './ServicesManager';
|
import ServicesManager from './ServicesManager';
|
||||||
|
import ServiceProvidersManager from './ServiceProvidersManager';
|
||||||
import UIDialogService from './UIDialogService';
|
import UIDialogService from './UIDialogService';
|
||||||
import UIModalService from './UIModalService';
|
import UIModalService from './UIModalService';
|
||||||
import UINotificationService from './UINotificationService';
|
import UINotificationService from './UINotificationService';
|
||||||
@ -22,6 +23,7 @@ export {
|
|||||||
Services,
|
Services,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
ServicesManager,
|
ServicesManager,
|
||||||
|
ServiceProvidersManager,
|
||||||
CustomizationService,
|
CustomizationService,
|
||||||
StateSyncService,
|
StateSyncService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
|
|||||||
@ -133,6 +133,7 @@ export type SyncGroup = {
|
|||||||
id: string;
|
id: string;
|
||||||
source?: boolean;
|
source?: boolean;
|
||||||
target?: boolean;
|
target?: boolean;
|
||||||
|
options?: object;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Declares a custom option, that is a computed type value */
|
/** Declares a custom option, that is a computed type value */
|
||||||
|
|||||||
@ -33,4 +33,5 @@ export default interface Services {
|
|||||||
cornerstoneCacheService?: unknown;
|
cornerstoneCacheService?: unknown;
|
||||||
segmentationService?: unknown;
|
segmentationService?: unknown;
|
||||||
panelService?: 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.
|
- `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.
|
- `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
|
- `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/>
|
- `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/>
|
Points to consider while using `dangerouslyUseDynamicConfig`:<br/>
|
||||||
- User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`.
|
- 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",
|
"Close": "Close",
|
||||||
"Image": "Image",
|
"Image": "Image",
|
||||||
"Layout": "Layout",
|
"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 UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||||
import Messages from './Messages.json';
|
import Messages from './Messages.json';
|
||||||
|
import WindowLevelActionMenu from './WindowLevelActionMenu.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'en-US': {
|
'en-US': {
|
||||||
@ -45,5 +46,6 @@ export default {
|
|||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
ViewportDownloadForm,
|
ViewportDownloadForm,
|
||||||
Messages,
|
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 TrackedCornerstoneViewport from './TrackedCornerstoneViewport.json';
|
||||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||||
|
import WindowLevelActionMenu from './WindowLevelActionMenu.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'test-LNG': {
|
'test-LNG': {
|
||||||
@ -49,5 +50,6 @@ export default {
|
|||||||
TrackedCornerstoneViewport,
|
TrackedCornerstoneViewport,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
ViewportDownloadForm,
|
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 |