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