feat(app): Decouple colorbar UI from service and enable action menu programatic control (#5018)
This commit is contained in:
parent
f14d9ebdd7
commit
1445e12124
@ -13,11 +13,11 @@ import CinePlayer from '../components/CinePlayer';
|
|||||||
import type { Types } from '@ohif/core';
|
import type { Types } from '@ohif/core';
|
||||||
|
|
||||||
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
|
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
|
||||||
|
import ViewportColorbarsContainer from '../components/ViewportColorbar';
|
||||||
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
|
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
|
||||||
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
|
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
|
||||||
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
|
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
|
||||||
import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewportService';
|
import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewportService';
|
||||||
import { useViewportActionCorners } from '../hooks';
|
|
||||||
|
|
||||||
const STACK = 'stack';
|
const STACK = 'stack';
|
||||||
|
|
||||||
@ -38,7 +38,6 @@ const OHIFCornerstoneViewport = React.memo(
|
|||||||
viewportOptions,
|
viewportOptions,
|
||||||
displaySetOptions,
|
displaySetOptions,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
|
||||||
onElementEnabled,
|
onElementEnabled,
|
||||||
// eslint-disable-next-line react/prop-types
|
// eslint-disable-next-line react/prop-types
|
||||||
onElementDisabled,
|
onElementDisabled,
|
||||||
@ -319,16 +318,6 @@ const OHIFCornerstoneViewport = React.memo(
|
|||||||
};
|
};
|
||||||
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
|
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
|
||||||
|
|
||||||
// Set up the window level action menu in the viewport action corners using the custom hook
|
|
||||||
useViewportActionCorners({
|
|
||||||
viewportId,
|
|
||||||
elementRef,
|
|
||||||
displaySets,
|
|
||||||
viewportActionCornersService,
|
|
||||||
customizationService,
|
|
||||||
commandsManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Notification = customizationService.getCustomization('ui.notificationComponent');
|
const Notification = customizationService.getCustomization('ui.notificationComponent');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -357,6 +346,10 @@ const OHIFCornerstoneViewport = React.memo(
|
|||||||
viewportId={viewportId}
|
viewportId={viewportId}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
|
<ViewportColorbarsContainer
|
||||||
|
viewportId={viewportId}
|
||||||
|
viewportElementRef={elementRef}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* top offset of 24px to account for ViewportActionCorners. */}
|
{/* top offset of 24px to account for ViewportActionCorners. */}
|
||||||
<div className="absolute top-[24px] w-full">
|
<div className="absolute top-[24px] w-full">
|
||||||
|
|||||||
@ -1,31 +1,54 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
|
import {
|
||||||
import { useSystem } from '@ohif/core';
|
useViewportActionCorners,
|
||||||
import { useViewportGrid } from '@ohif/ui-next';
|
ViewportActionCorners,
|
||||||
|
ViewportActionCornersLocations,
|
||||||
|
} from '@ohif/ui-next';
|
||||||
|
|
||||||
export type OHIFViewportActionCornersProps = {
|
export type OHIFViewportActionCornersProps = {
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
|
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
|
||||||
const { servicesManager } = useSystem();
|
const [state] = useViewportActionCorners();
|
||||||
const [viewportActionCornersState] = useViewportActionCornersContext();
|
|
||||||
|
|
||||||
const [viewportGrid] = useViewportGrid();
|
if (!state.viewports[viewportId]) {
|
||||||
const isActiveViewport = viewportGrid.activeViewportId === viewportId;
|
|
||||||
|
|
||||||
const ViewportActionCorners =
|
|
||||||
servicesManager.services.customizationService.getCustomization('ui.viewportActionCorner');
|
|
||||||
|
|
||||||
if (!viewportActionCornersState[viewportId]) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const components = state.viewports[viewportId];
|
||||||
|
|
||||||
|
const renderCorner = (location: ViewportActionCornersLocations, CornerComponent) => {
|
||||||
|
const cornerComponents = components[location];
|
||||||
|
if (!cornerComponents?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CornerComponent>
|
||||||
|
{cornerComponents
|
||||||
|
.filter(componentInfo => componentInfo.isVisible !== false)
|
||||||
|
.map(componentInfo => (
|
||||||
|
<div
|
||||||
|
key={componentInfo.id}
|
||||||
|
className={
|
||||||
|
componentInfo.isLocked === true ? 'pointer-events-none opacity-50' : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{componentInfo.component}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CornerComponent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewportActionCorners
|
<ViewportActionCorners.Container>
|
||||||
cornerComponents={viewportActionCornersState[viewportId]}
|
{renderCorner(ViewportActionCornersLocations.topLeft, ViewportActionCorners.TopLeft)}
|
||||||
isActiveViewport={isActiveViewport}
|
{renderCorner(ViewportActionCornersLocations.topRight, ViewportActionCorners.TopRight)}
|
||||||
/>
|
{renderCorner(ViewportActionCornersLocations.bottomLeft, ViewportActionCorners.BottomLeft)}
|
||||||
|
{renderCorner(ViewportActionCornersLocations.bottomRight, ViewportActionCorners.BottomRight)}
|
||||||
|
</ViewportActionCorners.Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,117 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, Icons } from '@ohif/ui-next';
|
||||||
|
import ViewportColorbar from './ViewportColorbar';
|
||||||
|
import {
|
||||||
|
ColorbarCustomization,
|
||||||
|
ColorbarPositionType,
|
||||||
|
TickPositionType,
|
||||||
|
} from '../../types/Colorbar';
|
||||||
|
import type { ColorMapPreset } from '../../types/Colormap';
|
||||||
|
|
||||||
|
type ColorbarData = {
|
||||||
|
colorbar: {
|
||||||
|
activeColormapName: string;
|
||||||
|
colormaps: ColorMapPreset[];
|
||||||
|
volumeId?: string;
|
||||||
|
};
|
||||||
|
displaySetInstanceUID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AdvancedColorbarWithControlsProps = {
|
||||||
|
viewportId: string;
|
||||||
|
colorbars: ColorbarData[];
|
||||||
|
position: string;
|
||||||
|
tickPosition: string;
|
||||||
|
colorbarCustomization: ColorbarCustomization;
|
||||||
|
onClose: (displaySetInstanceUID?: string) => void;
|
||||||
|
viewportElementRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AdvancedColorbarWithControls Component
|
||||||
|
* A specialized colorbar component with additional control buttons for advanced window level controls
|
||||||
|
* This component handles the rendering portion of the ViewportColorbarsContainer
|
||||||
|
*/
|
||||||
|
const AdvancedColorbarWithControls = ({
|
||||||
|
viewportId,
|
||||||
|
colorbars,
|
||||||
|
position,
|
||||||
|
tickPosition,
|
||||||
|
colorbarCustomization,
|
||||||
|
onClose,
|
||||||
|
viewportElementRef,
|
||||||
|
}: AdvancedColorbarWithControlsProps) => {
|
||||||
|
const handleClose = (displaySetInstanceUID?: string) => {
|
||||||
|
onClose(displaySetInstanceUID);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get bottom position styles from customization
|
||||||
|
const positionStyles = colorbarCustomization?.positionStyles || {};
|
||||||
|
const bottomPositionStyles = positionStyles.bottom || {};
|
||||||
|
const heightStyle = bottomPositionStyles.height;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex h-[20px] w-1/2 flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-shrink-0 flex-row">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleClose()}
|
||||||
|
className="flex-shrink-0 p-0"
|
||||||
|
>
|
||||||
|
<Icons.Close />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="flex-shrink-0 p-0"
|
||||||
|
>
|
||||||
|
<Icons.ToolZoom />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="mx-2"
|
||||||
|
style={{ width: 'calc(75% - 16px)', height: heightStyle }}
|
||||||
|
>
|
||||||
|
{colorbars.map((colorbarInfo, index) => {
|
||||||
|
const { colorbar, displaySetInstanceUID } = colorbarInfo;
|
||||||
|
return (
|
||||||
|
<ViewportColorbar
|
||||||
|
key={`colorbar-${viewportId}-${displaySetInstanceUID}`}
|
||||||
|
viewportId={viewportId}
|
||||||
|
viewportElementRef={viewportElementRef}
|
||||||
|
displaySetInstanceUID={displaySetInstanceUID}
|
||||||
|
colormaps={colorbar.colormaps}
|
||||||
|
activeColormapName={colorbar.activeColormapName}
|
||||||
|
volumeId={colorbar.volumeId}
|
||||||
|
position={position as ColorbarPositionType}
|
||||||
|
tickPosition={tickPosition as TickPositionType}
|
||||||
|
tickStyles={colorbarCustomization?.tickStyles}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-shrink-0 flex-row">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="flex-shrink-0 p-0"
|
||||||
|
>
|
||||||
|
<Icons.Redo />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="flex-shrink-0 p-0"
|
||||||
|
>
|
||||||
|
<Icons.Pencil />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdvancedColorbarWithControls;
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { utilities } from '@cornerstonejs/tools';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
import {
|
||||||
|
ColorbarPositionType,
|
||||||
|
TickPositionType,
|
||||||
|
ColorbarCustomization,
|
||||||
|
TickStyleType,
|
||||||
|
ContainerStyleType,
|
||||||
|
} from '../../types/Colorbar';
|
||||||
|
import { ColorbarRangeTextPosition } from '@cornerstonejs/tools/utilities/voi/colorbar/enums/ColorbarRangeTextPosition';
|
||||||
|
|
||||||
|
const { ViewportColorbar: CornerstoneViewportColorbar } = utilities.voi.colorbar;
|
||||||
|
|
||||||
|
type ColorbarProps = {
|
||||||
|
viewportId: string;
|
||||||
|
displaySetInstanceUID: string;
|
||||||
|
colormap?: any;
|
||||||
|
colormaps: any[];
|
||||||
|
activeColormapName: string;
|
||||||
|
volumeId?: string;
|
||||||
|
position: ColorbarPositionType;
|
||||||
|
tickPosition: TickPositionType;
|
||||||
|
tickStyles?: TickStyleType;
|
||||||
|
containerStyles?: ContainerStyleType;
|
||||||
|
viewportElementRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewportColorbar Component
|
||||||
|
* A React wrapper for the cornerstone ViewportColorbar that adds a close button
|
||||||
|
* positioned appropriately based on the colorbar position.
|
||||||
|
*/
|
||||||
|
const ViewportColorbar = ({
|
||||||
|
viewportId,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
colormaps,
|
||||||
|
activeColormapName,
|
||||||
|
volumeId,
|
||||||
|
position,
|
||||||
|
tickPosition,
|
||||||
|
tickStyles,
|
||||||
|
viewportElementRef,
|
||||||
|
}: ColorbarProps) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { servicesManager } = useSystem();
|
||||||
|
const { customizationService } = servicesManager.services;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || !colormaps || !activeColormapName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportElement = viewportElementRef?.current;
|
||||||
|
|
||||||
|
if (!viewportElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorbarCustomization = customizationService.getCustomization(
|
||||||
|
'cornerstone.colorbar'
|
||||||
|
) as unknown as ColorbarCustomization;
|
||||||
|
|
||||||
|
const positionTickStyles = colorbarCustomization?.positionTickStyles?.[position];
|
||||||
|
const csColorbar = new CornerstoneViewportColorbar({
|
||||||
|
id: `Colorbar-${viewportId}-${displaySetInstanceUID}`,
|
||||||
|
element: viewportElement,
|
||||||
|
container: containerRef.current,
|
||||||
|
colormaps: colormaps,
|
||||||
|
activeColormapName: activeColormapName,
|
||||||
|
volumeId,
|
||||||
|
ticks: {
|
||||||
|
position: tickPosition as ColorbarRangeTextPosition,
|
||||||
|
style: {
|
||||||
|
...(colorbarCustomization?.tickStyles || {}),
|
||||||
|
...(positionTickStyles?.style || {}),
|
||||||
|
...(tickStyles || {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (csColorbar) {
|
||||||
|
csColorbar.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
viewportId,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
colormaps,
|
||||||
|
activeColormapName,
|
||||||
|
volumeId,
|
||||||
|
position,
|
||||||
|
tickPosition,
|
||||||
|
tickStyles,
|
||||||
|
viewportElementRef,
|
||||||
|
customizationService,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get position styles from customization service
|
||||||
|
const colorbarCustomization = customizationService.getCustomization(
|
||||||
|
'cornerstone.colorbar'
|
||||||
|
) as unknown as ColorbarCustomization;
|
||||||
|
|
||||||
|
const positionStylesFromConfig = colorbarCustomization?.positionStyles?.[position] || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={`colorbar-container-${viewportId}-${displaySetInstanceUID}`}
|
||||||
|
ref={containerRef}
|
||||||
|
style={{
|
||||||
|
width: position === 'bottom' ? '100%' : '20px',
|
||||||
|
height: position === 'bottom' ? '20px' : '100%',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1000,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
...positionStylesFromConfig,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewportColorbar;
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
import AdvancedColorbarWithControls from './AdvancedColorbarWithControls';
|
||||||
|
import { ColorbarCustomization } from '../../types/Colorbar';
|
||||||
|
import type { ColorMapPreset } from '../../types/Colormap';
|
||||||
|
import ViewportColorbar from './ViewportColorbar';
|
||||||
|
import { deepMerge } from '@cornerstonejs/core/utilities';
|
||||||
|
|
||||||
|
type ViewportColorbarsContainerProps = {
|
||||||
|
viewportId: string;
|
||||||
|
viewportElementRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ColorbarData = {
|
||||||
|
colorbar: {
|
||||||
|
activeColormapName: string;
|
||||||
|
colormaps: ColorMapPreset[];
|
||||||
|
volumeId?: string;
|
||||||
|
};
|
||||||
|
displaySetInstanceUID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container component that manages multiple colorbars for a viewport
|
||||||
|
* It interacts with the colorbarService to get/set colorbar states
|
||||||
|
*/
|
||||||
|
const ViewportColorbarsContainer = ({
|
||||||
|
viewportId,
|
||||||
|
viewportElementRef,
|
||||||
|
}: ViewportColorbarsContainerProps) => {
|
||||||
|
const [colorbars, setColorbars] = useState<ColorbarData[]>([]);
|
||||||
|
const { servicesManager } = useSystem();
|
||||||
|
const { colorbarService, customizationService } = servicesManager.services;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setColorbars(colorbarService.getViewportColorbar(viewportId) || []);
|
||||||
|
}, [viewportId, colorbarService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = colorbarService.subscribe(
|
||||||
|
colorbarService.EVENTS.STATE_CHANGED,
|
||||||
|
(event: { viewportId: string; displaySetInstanceUID?: string; changeType: string }) => {
|
||||||
|
if (event.viewportId === viewportId) {
|
||||||
|
setColorbars(colorbarService.getViewportColorbar(viewportId) || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [viewportId, colorbarService]);
|
||||||
|
|
||||||
|
const handleClose = (displaySetInstanceUID?: string): void => {
|
||||||
|
if (displaySetInstanceUID) {
|
||||||
|
colorbarService.removeColorbar(viewportId, displaySetInstanceUID);
|
||||||
|
} else {
|
||||||
|
colorbarService.removeColorbar(viewportId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!colorbars.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorbarCustomization = customizationService.getCustomization(
|
||||||
|
'cornerstone.colorbar'
|
||||||
|
) as unknown as ColorbarCustomization;
|
||||||
|
|
||||||
|
const defaultPosition = colorbarCustomization?.colorbarContainerPosition;
|
||||||
|
const defaultTickPosition = colorbarCustomization?.colorbarTickPosition;
|
||||||
|
|
||||||
|
const position = colorbarCustomization?.colorbarContainerPosition || defaultPosition;
|
||||||
|
const tickPosition = colorbarCustomization?.colorbarTickPosition || defaultTickPosition;
|
||||||
|
const positionStyles = colorbarCustomization?.positionStyles;
|
||||||
|
|
||||||
|
const positionStyle = positionStyles?.[position];
|
||||||
|
|
||||||
|
const defaultPositionStyle =
|
||||||
|
position !== 'bottom'
|
||||||
|
? {
|
||||||
|
width: '20px',
|
||||||
|
height: colorbars.length === 1 ? '55%' : '75%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
width: '100%',
|
||||||
|
height: '20px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalPositionStyle = deepMerge(defaultPositionStyle, positionStyle);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
...finalPositionStyle,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{position !== 'bottom' ? (
|
||||||
|
<div
|
||||||
|
className="flex h-full flex-col items-center justify-center"
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
|
>
|
||||||
|
{colorbars.map((colorbarInfo, index) => {
|
||||||
|
const { colorbar, displaySetInstanceUID } = colorbarInfo;
|
||||||
|
return (
|
||||||
|
<ViewportColorbar
|
||||||
|
key={`colorbar-${viewportId}-${displaySetInstanceUID}`}
|
||||||
|
viewportId={viewportId}
|
||||||
|
displaySetInstanceUID={displaySetInstanceUID}
|
||||||
|
colormaps={colorbar.colormaps}
|
||||||
|
activeColormapName={colorbar.activeColormapName}
|
||||||
|
volumeId={colorbar.volumeId}
|
||||||
|
viewportElementRef={viewportElementRef}
|
||||||
|
position={position}
|
||||||
|
tickPosition={tickPosition}
|
||||||
|
tickStyles={colorbarCustomization?.tickStyles}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<AdvancedColorbarWithControls
|
||||||
|
viewportId={viewportId}
|
||||||
|
colorbars={colorbars}
|
||||||
|
position={position}
|
||||||
|
tickPosition={defaultTickPosition}
|
||||||
|
colorbarCustomization={colorbarCustomization}
|
||||||
|
onClose={handleClose}
|
||||||
|
viewportElementRef={viewportElementRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewportColorbarsContainer;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import ViewportColorbar from './ViewportColorbar';
|
||||||
|
import ViewportColorbarsContainer from './ViewportColorbarsContainer';
|
||||||
|
import AdvancedColorbarWithControls from './AdvancedColorbarWithControls';
|
||||||
|
|
||||||
|
export { ViewportColorbar, ViewportColorbarsContainer, AdvancedColorbarWithControls };
|
||||||
|
export default ViewportColorbarsContainer;
|
||||||
@ -7,10 +7,11 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
|
useViewportActionCorners,
|
||||||
} from '@ohif/ui-next';
|
} from '@ohif/ui-next';
|
||||||
import ViewportDataOverlayMenu from './ViewportDataOverlayMenu';
|
import ViewportDataOverlayMenu from './ViewportDataOverlayMenu';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useSystem } from '@ohif/core';
|
import { MENU_IDS } from '../menus/menu-ids';
|
||||||
|
|
||||||
export function ViewportDataOverlayMenuWrapper({
|
export function ViewportDataOverlayMenuWrapper({
|
||||||
viewportId,
|
viewportId,
|
||||||
@ -19,15 +20,39 @@ export function ViewportDataOverlayMenuWrapper({
|
|||||||
}: withAppTypes<{
|
}: withAppTypes<{
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
|
location: string;
|
||||||
}>): ReactNode {
|
}>): ReactNode {
|
||||||
const { servicesManager } = useSystem();
|
|
||||||
const { viewportActionCornersService } = servicesManager.services;
|
|
||||||
const [viewportGrid] = useViewportGrid();
|
const [viewportGrid] = useViewportGrid();
|
||||||
|
const [actionCornerState, viewportActionCornersAPI] = useViewportActionCorners();
|
||||||
|
|
||||||
const { align, side } = viewportActionCornersService.getAlignAndSide(location);
|
const isMenuOpen =
|
||||||
|
actionCornerState.viewports[viewportId]?.[location]?.find(
|
||||||
|
item => item.id === MENU_IDS.DATA_OVERLAY_MENU
|
||||||
|
)?.isOpen ?? false;
|
||||||
|
|
||||||
|
const handleOpenChange = (openState: boolean) => {
|
||||||
|
if (openState) {
|
||||||
|
viewportActionCornersAPI.openItem?.(viewportId, MENU_IDS.DATA_OVERLAY_MENU);
|
||||||
|
} else {
|
||||||
|
viewportActionCornersAPI.closeItem?.(viewportId, MENU_IDS.DATA_OVERLAY_MENU);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get proper alignment and side based on the location
|
||||||
|
let align = 'center';
|
||||||
|
let side = 'bottom';
|
||||||
|
|
||||||
|
if (location !== undefined) {
|
||||||
|
const positioning = viewportActionCornersAPI.getAlignAndSide(location);
|
||||||
|
align = positioning.align;
|
||||||
|
side = positioning.side;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover
|
||||||
|
open={isMenuOpen}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
asChild
|
asChild
|
||||||
className="flex items-center justify-center"
|
className="flex items-center justify-center"
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export function getViewportDataOverlaySettingsMenu(
|
|||||||
props: withAppTypes<{
|
props: withAppTypes<{
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
|
location: string;
|
||||||
}>
|
}>
|
||||||
): ReactNode {
|
): ReactNode {
|
||||||
return <ViewportDataOverlayMenuWrapper {...props} />;
|
return <ViewportDataOverlayMenuWrapper {...props} />;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icons, useViewportGrid } from '@ohif/ui-next';
|
import { Icons, useViewportActionCorners, useViewportGrid } from '@ohif/ui-next';
|
||||||
import { useSystem } from '@ohif/core';
|
import { useSystem } from '@ohif/core';
|
||||||
import { Enums } from '@cornerstonejs/core';
|
import { Enums } from '@cornerstonejs/core';
|
||||||
import {
|
import {
|
||||||
@ -10,14 +10,21 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
Button,
|
Button,
|
||||||
} from '@ohif/ui-next';
|
} from '@ohif/ui-next';
|
||||||
|
import { MENU_IDS } from '../menus/menu-ids';
|
||||||
|
|
||||||
function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string }>) {
|
function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string }>) {
|
||||||
const { servicesManager, commandsManager } = useSystem();
|
const { servicesManager, commandsManager } = useSystem();
|
||||||
const [viewportGridState, viewportGridService] = useViewportGrid();
|
const [viewportGridState, viewportGridService] = useViewportGrid();
|
||||||
|
const [actionCornerState, viewportActionCornersServiceAPI] = useViewportActionCorners();
|
||||||
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
|
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
|
||||||
|
|
||||||
const viewportId = viewportGridState.activeViewportId;
|
const viewportId = viewportGridState.activeViewportId;
|
||||||
|
|
||||||
|
const isMenuOpen =
|
||||||
|
actionCornerState.viewports[viewportId]?.[location]?.find(
|
||||||
|
item => item.id === MENU_IDS.ORIENTATION_MENU
|
||||||
|
)?.isOpen ?? false;
|
||||||
|
|
||||||
const handleOrientationChange = (orientation: string) => {
|
const handleOrientationChange = (orientation: string) => {
|
||||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||||
const currentViewportType = viewportInfo?.getViewportType();
|
const currentViewportType = viewportInfo?.getViewportType();
|
||||||
@ -80,8 +87,19 @@ function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string
|
|||||||
orientation: orientationEnum,
|
orientation: orientationEnum,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the menu after selection
|
||||||
|
viewportActionCornersServiceAPI.closeItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
|
||||||
};
|
};
|
||||||
const { viewportActionCornersService } = servicesManager.services;
|
|
||||||
|
const handleOpenChange = (openState: boolean) => {
|
||||||
|
if (openState) {
|
||||||
|
viewportActionCornersServiceAPI.openItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
|
||||||
|
} else {
|
||||||
|
viewportActionCornersServiceAPI.closeItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||||
const displaySets = displaySetUIDs
|
const displaySets = displaySetUIDs
|
||||||
.map(uid => displaySetService.getDisplaySetByUID(uid))
|
.map(uid => displaySetService.getDisplaySetByUID(uid))
|
||||||
@ -98,13 +116,16 @@ function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string
|
|||||||
let side = 'bottom';
|
let side = 'bottom';
|
||||||
|
|
||||||
if (location !== undefined) {
|
if (location !== undefined) {
|
||||||
const positioning = viewportActionCornersService.getAlignAndSide(location);
|
const positioning = viewportActionCornersServiceAPI.getAlignAndSide(location);
|
||||||
align = positioning.align;
|
align = positioning.align;
|
||||||
side = positioning.side;
|
side = positioning.side;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu
|
||||||
|
open={isMenuOpen}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||||
import { Switch } from '@ohif/ui-next';
|
import { Switch } from '@ohif/ui-next';
|
||||||
import { StackViewport } from '@cornerstonejs/core';
|
import { ColorbarProps, ColorbarOptions } from '../../types/Colorbar';
|
||||||
import { ColorbarProps } from '../../types/Colorbar';
|
|
||||||
import { utilities } from '@cornerstonejs/core';
|
import { utilities } from '@cornerstonejs/core';
|
||||||
import { useSystem } from '@ohif/core';
|
import { useSystem } from '@ohif/core';
|
||||||
|
|
||||||
export function setViewportColorbar(
|
export function setViewportColorbar(
|
||||||
viewportId,
|
viewportId: string,
|
||||||
commandsManager,
|
commandsManager: any,
|
||||||
servicesManager: AppTypes.ServicesManager,
|
servicesManager: AppTypes.ServicesManager,
|
||||||
colorbarOptions
|
colorbarOptions: Partial<ColorbarOptions>
|
||||||
) {
|
) {
|
||||||
const { cornerstoneViewportService, viewportGridService } = servicesManager.services;
|
const { cornerstoneViewportService, viewportGridService } = servicesManager.services;
|
||||||
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import classNames from 'classnames';
|
import { AllInOneMenu, cn } from '@ohif/ui-next';
|
||||||
import { AllInOneMenu } from '@ohif/ui-next';
|
|
||||||
import { useViewportGrid } from '@ohif/ui-next';
|
import { useViewportGrid } from '@ohif/ui-next';
|
||||||
import { Colormap } from './Colormap';
|
import { Colormap } from './Colormap';
|
||||||
import { Colorbar } from './Colorbar';
|
import { Colorbar } from './Colorbar';
|
||||||
@ -25,6 +24,7 @@ export type WindowLevelActionMenuProps = {
|
|||||||
displaySets: Array<any>;
|
displaySets: Array<any>;
|
||||||
volumeRenderingPresets: Array<ViewportPreset>;
|
volumeRenderingPresets: Array<ViewportPreset>;
|
||||||
volumeRenderingQualityRange: VolumeRenderingQualityRange;
|
volumeRenderingQualityRange: VolumeRenderingQualityRange;
|
||||||
|
location: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WindowLevelActionMenu({
|
export function WindowLevelActionMenu({
|
||||||
@ -111,20 +111,13 @@ export function WindowLevelActionMenu({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AllInOneMenu.IconMenu
|
<AllInOneMenu.Menu
|
||||||
icon="viewport-window-level"
|
|
||||||
verticalDirection={verticalDirection}
|
|
||||||
horizontalDirection={horizontalDirection}
|
|
||||||
iconClassName={classNames(
|
|
||||||
activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible',
|
|
||||||
'flex shrink-0 cursor-pointer rounded active:text-foreground text-highlight',
|
|
||||||
isLight ? ' hover:bg-primary/30' : 'hover:bg-primary/30'
|
|
||||||
)}
|
|
||||||
menuStyle={{ maxHeight: vpHeight - 32, minWidth: 218 }}
|
|
||||||
onVisibilityChange={() => {
|
|
||||||
setVpHeight(element.clientHeight);
|
|
||||||
}}
|
|
||||||
menuKey={menuKey}
|
menuKey={menuKey}
|
||||||
|
key={menuKey}
|
||||||
|
// the visibility is handled by the parent component
|
||||||
|
isVisible={true}
|
||||||
|
horizontalDirection={horizontalDirection}
|
||||||
|
verticalDirection={verticalDirection}
|
||||||
>
|
>
|
||||||
<AllInOneMenu.ItemPanel>
|
<AllInOneMenu.ItemPanel>
|
||||||
{!is3DVolume && (
|
{!is3DVolume && (
|
||||||
@ -180,6 +173,6 @@ export function WindowLevelActionMenu({
|
|||||||
</AllInOneMenu.SubMenu>
|
</AllInOneMenu.SubMenu>
|
||||||
)}
|
)}
|
||||||
</AllInOneMenu.ItemPanel>
|
</AllInOneMenu.ItemPanel>
|
||||||
</AllInOneMenu.IconMenu>
|
</AllInOneMenu.Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,138 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
cn,
|
||||||
|
Icons,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
useViewportGrid,
|
||||||
|
useViewportActionCorners,
|
||||||
|
AllInOneMenu,
|
||||||
|
} from '@ohif/ui-next';
|
||||||
|
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
|
||||||
|
import { MENU_IDS } from '../menus/menu-ids';
|
||||||
|
|
||||||
|
export function WindowLevelActionMenuWrapper({
|
||||||
|
viewportId,
|
||||||
|
element,
|
||||||
|
location,
|
||||||
|
displaySets,
|
||||||
|
}: withAppTypes<{
|
||||||
|
viewportId: string;
|
||||||
|
element: HTMLElement;
|
||||||
|
location: string;
|
||||||
|
displaySets: Array<AppTypes.DisplaySet>;
|
||||||
|
}>): ReactNode {
|
||||||
|
const [viewportGrid] = useViewportGrid();
|
||||||
|
const [actionCornerState, viewportActionCornersAPI] = useViewportActionCorners();
|
||||||
|
const isActiveViewport = viewportId === viewportGrid.activeViewportId;
|
||||||
|
const { servicesManager } = useSystem();
|
||||||
|
const { customizationService } = servicesManager.services;
|
||||||
|
|
||||||
|
const presets = customizationService.getCustomization('cornerstone.windowLevelPresets');
|
||||||
|
const colorbarProperties = customizationService.getCustomization('cornerstone.colorbar');
|
||||||
|
const { volumeRenderingPresets, volumeRenderingQualityRange } =
|
||||||
|
customizationService.getCustomization('cornerstone.3dVolumeRendering');
|
||||||
|
|
||||||
|
const isMenuOpen =
|
||||||
|
actionCornerState.viewports[viewportId]?.[location]?.find(
|
||||||
|
item => item.id === MENU_IDS.WINDOW_LEVEL_MENU
|
||||||
|
)?.isOpen ?? false;
|
||||||
|
|
||||||
|
const handleOpenChange = (openState: boolean) => {
|
||||||
|
if (openState) {
|
||||||
|
viewportActionCornersAPI.openItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
|
||||||
|
} else {
|
||||||
|
viewportActionCornersAPI.closeItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { align, side, horizontalDirection, verticalDirection } = getMenuDirections(
|
||||||
|
location,
|
||||||
|
viewportActionCornersAPI
|
||||||
|
);
|
||||||
|
|
||||||
|
const displaySetPresets = displaySets
|
||||||
|
.filter(displaySet => presets[displaySet.Modality])
|
||||||
|
.map(displaySet => {
|
||||||
|
return { [displaySet.Modality]: presets[displaySet.Modality] };
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
|
||||||
|
|
||||||
|
if (modalities.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={isMenuOpen}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
>
|
||||||
|
<PopoverTrigger
|
||||||
|
asChild
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible',
|
||||||
|
'text-highlight'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icons.ByName name="viewport-window-level" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="border-none bg-transparent p-0 shadow-none"
|
||||||
|
side={side}
|
||||||
|
align={align}
|
||||||
|
alignOffset={0}
|
||||||
|
sideOffset={5}
|
||||||
|
>
|
||||||
|
<WindowLevelActionMenu
|
||||||
|
viewportId={viewportId}
|
||||||
|
element={element}
|
||||||
|
presets={displaySetPresets}
|
||||||
|
horizontalDirection={horizontalDirection}
|
||||||
|
verticalDirection={verticalDirection}
|
||||||
|
colorbarProperties={colorbarProperties}
|
||||||
|
displaySets={displaySets}
|
||||||
|
volumeRenderingPresets={volumeRenderingPresets}
|
||||||
|
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMenuDirections = (location, viewportActionCornersAPI) => {
|
||||||
|
let align = 'center';
|
||||||
|
let side = 'bottom';
|
||||||
|
|
||||||
|
if (location !== undefined) {
|
||||||
|
const positioning = viewportActionCornersAPI.getAlignAndSide(location);
|
||||||
|
align = positioning.align;
|
||||||
|
side = positioning.side;
|
||||||
|
}
|
||||||
|
|
||||||
|
let horizontalDirection;
|
||||||
|
let verticalDirection;
|
||||||
|
|
||||||
|
if (side === 'bottom') {
|
||||||
|
verticalDirection = AllInOneMenu.VerticalDirection.TopToBottom;
|
||||||
|
} else {
|
||||||
|
verticalDirection = AllInOneMenu.VerticalDirection.BottomToTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (align === 'start') {
|
||||||
|
horizontalDirection = AllInOneMenu.HorizontalDirection.LeftToRight;
|
||||||
|
} else {
|
||||||
|
horizontalDirection = AllInOneMenu.HorizontalDirection.RightToLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { align, side, horizontalDirection, verticalDirection };
|
||||||
|
};
|
||||||
@ -1,53 +1,13 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React from 'react';
|
||||||
import { WindowLevelActionMenuProps } from './WindowLevelActionMenu';
|
import { WindowLevelActionMenuWrapper } from './WindowLevelActionMenuWrapper';
|
||||||
import { useSystem } from '@ohif/core';
|
|
||||||
import { WindowLevelActionMenu as WindowLevelActionMenuComponent } from './WindowLevelActionMenu';
|
|
||||||
|
|
||||||
function WindowLevelActionMenu({
|
export function getWindowLevelActionMenu(
|
||||||
viewportId,
|
props: withAppTypes<{
|
||||||
element,
|
viewportId: string;
|
||||||
displaySets,
|
element: HTMLElement;
|
||||||
verticalDirection,
|
location: string;
|
||||||
horizontalDirection,
|
displaySets: Array<any>;
|
||||||
}: withAppTypes<{
|
}>
|
||||||
viewportId: string;
|
) {
|
||||||
element: HTMLElement;
|
return <WindowLevelActionMenuWrapper {...props} />;
|
||||||
displaySets: AppTypes.DisplaySet[];
|
|
||||||
}>): ReactNode {
|
|
||||||
const { servicesManager } = useSystem();
|
|
||||||
const { customizationService } = servicesManager.services;
|
|
||||||
|
|
||||||
const presets = customizationService.getCustomization('cornerstone.windowLevelPresets');
|
|
||||||
const colorbarProperties = customizationService.getCustomization('cornerstone.colorbar');
|
|
||||||
const { volumeRenderingPresets, volumeRenderingQualityRange } =
|
|
||||||
customizationService.getCustomization('cornerstone.3dVolumeRendering');
|
|
||||||
const displaySetPresets = displaySets
|
|
||||||
.filter(displaySet => presets[displaySet.Modality])
|
|
||||||
.map(displaySet => {
|
|
||||||
return { [displaySet.Modality]: presets[displaySet.Modality] };
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
|
|
||||||
|
|
||||||
if (modalities.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WindowLevelActionMenuComponent
|
|
||||||
viewportId={viewportId}
|
|
||||||
element={element}
|
|
||||||
presets={displaySetPresets}
|
|
||||||
verticalDirection={verticalDirection}
|
|
||||||
horizontalDirection={horizontalDirection}
|
|
||||||
colorbarProperties={colorbarProperties}
|
|
||||||
displaySets={displaySets}
|
|
||||||
volumeRenderingPresets={volumeRenderingPresets}
|
|
||||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWindowLevelActionMenu(props: WindowLevelActionMenuProps) {
|
|
||||||
return <WindowLevelActionMenu {...props} />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
export * from './Colorbar';
|
||||||
|
export * from './Colormap';
|
||||||
|
export * from './WindowLevel';
|
||||||
|
export * from './WindowLevelActionMenu';
|
||||||
|
export * from './WindowLevelActionMenuWrapper';
|
||||||
|
export * from './VolumeRenderingOptions';
|
||||||
|
export * from './VolumeRenderingPresets';
|
||||||
|
export * from './VolumeRenderingQuality';
|
||||||
|
export * from './VolumeLighting';
|
||||||
|
export * from './VolumeShade';
|
||||||
|
export * from './VolumeShift';
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import DicomUpload from './DicomUpload/DicomUpload';
|
||||||
export * from './AccordionGroup';
|
export * from './AccordionGroup';
|
||||||
export * from './MeasurementTableNested';
|
export * from './MeasurementTableNested';
|
||||||
export * from './StudyMeasurements';
|
export * from './StudyMeasurements';
|
||||||
@ -5,6 +6,8 @@ export * from './MeasurementsMenu';
|
|||||||
export * from './SeriesMeasurements';
|
export * from './SeriesMeasurements';
|
||||||
export * from './StudyMeasurementsActions';
|
export * from './StudyMeasurementsActions';
|
||||||
export * from './MeasurementsOrAdditionalFindings';
|
export * from './MeasurementsOrAdditionalFindings';
|
||||||
import DicomUpload from './DicomUpload/DicomUpload';
|
export * from './ViewportDataOverlaySettingMenu';
|
||||||
|
export * from './ViewportOrientationMenu';
|
||||||
|
export * from './WindowLevelActionMenu';
|
||||||
|
|
||||||
export { DicomUpload };
|
export { DicomUpload };
|
||||||
|
|||||||
5
extensions/cornerstone/src/components/menus/menu-ids.ts
Normal file
5
extensions/cornerstone/src/components/menus/menu-ids.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const MENU_IDS = {
|
||||||
|
ORIENTATION_MENU: 'orientationMenu',
|
||||||
|
DATA_OVERLAY_MENU: 'dataOverlayMenu',
|
||||||
|
WINDOW_LEVEL_MENU: 'windowLevelActionMenu',
|
||||||
|
};
|
||||||
@ -1,180 +0,0 @@
|
|||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useReducer,
|
|
||||||
} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { Types, ViewportActionCornersLocations } from '@ohif/ui-next';
|
|
||||||
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 'ADD_ACTION_COMPONENT': {
|
|
||||||
const { viewportId, id, component, location, indexPriority } = 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) {
|
|
||||||
let insertionIndex;
|
|
||||||
const isRightSide =
|
|
||||||
location === ViewportActionCornersLocations.topRight ||
|
|
||||||
location === ViewportActionCornersLocations.bottomRight;
|
|
||||||
|
|
||||||
if (indexPriority === undefined) {
|
|
||||||
// If no indexPriority is provided, add it to the appropriate end
|
|
||||||
insertionIndex = isRightSide ? 0 : locationComponents.length;
|
|
||||||
} else {
|
|
||||||
if (isRightSide) {
|
|
||||||
insertionIndex = locationComponents.findIndex(
|
|
||||||
component => indexPriority > component.indexPriority
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
insertionIndex = locationComponents.findIndex(
|
|
||||||
component => indexPriority <= component.indexPriority
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (insertionIndex === -1) {
|
|
||||||
// If no suitable position found, add to the appropriate end
|
|
||||||
insertionIndex = isRightSide ? 0 : locationComponents.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultPriority = isRightSide ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
|
|
||||||
|
|
||||||
locationComponents = [
|
|
||||||
...locationComponents.slice(0, insertionIndex),
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
component,
|
|
||||||
indexPriority: indexPriority ?? defaultPriority,
|
|
||||||
},
|
|
||||||
...locationComponents.slice(insertionIndex),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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 addComponent = useCallback(
|
|
||||||
(actionComponentInfo: ActionComponentInfo) => {
|
|
||||||
dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo });
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const addComponents = useCallback(
|
|
||||||
(actionComponentInfos: Array<ActionComponentInfo>) => {
|
|
||||||
actionComponentInfos.forEach(actionComponentInfo =>
|
|
||||||
dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo })
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const clear = useCallback(
|
|
||||||
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (service) {
|
|
||||||
service.setServiceImplementation({
|
|
||||||
getState,
|
|
||||||
addComponent,
|
|
||||||
addComponents,
|
|
||||||
clear,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [getState, service, addComponent, addComponents, clear]);
|
|
||||||
|
|
||||||
const viewportCornerActions = {
|
|
||||||
getState,
|
|
||||||
addComponent: props => service.addComponent(props),
|
|
||||||
addComponents: props => service.addComponents(props),
|
|
||||||
clear: props => service.clear(props),
|
|
||||||
};
|
|
||||||
|
|
||||||
const contextValue = useMemo(
|
|
||||||
() => [viewportActionCornersState, viewportCornerActions],
|
|
||||||
[viewportActionCornersState, viewportCornerActions]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewportActionCornersContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</ViewportActionCornersContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewportActionCornersProvider.propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
service: PropTypes.instanceOf(ViewportActionCornersService).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useViewportActionCornersContext = () => useContext(ViewportActionCornersContext);
|
|
||||||
@ -1,13 +1,93 @@
|
|||||||
import { colormaps } from '../utils/colormaps';
|
import { colormaps } from '../utils/colormaps';
|
||||||
|
import {
|
||||||
|
ColorbarPositionType,
|
||||||
|
TickPositionType,
|
||||||
|
PositionStylesMapType,
|
||||||
|
PositionTickStylesMapType,
|
||||||
|
ContainerStyleType,
|
||||||
|
TickStyleType,
|
||||||
|
ColorbarProperties,
|
||||||
|
} from '../types/Colorbar';
|
||||||
|
import { ColorMapPreset } from '../types/Colormap';
|
||||||
|
|
||||||
|
const defaultPosition: ColorbarPositionType = 'right';
|
||||||
const DefaultColormap = 'Grayscale';
|
const DefaultColormap = 'Grayscale';
|
||||||
|
|
||||||
export default {
|
const positionStyles: PositionStylesMapType = {
|
||||||
'cornerstone.colorbar': {
|
left: { left: '5%', width: '15px' },
|
||||||
width: '16px',
|
right: { right: '5%', width: '15px' },
|
||||||
colorbarTickPosition: 'left',
|
bottom: { bottom: '1%', height: '18px' },
|
||||||
colormaps,
|
};
|
||||||
colorbarContainerPosition: 'right',
|
|
||||||
colorbarInitialColormap: DefaultColormap,
|
// Typed position-specific tick styles
|
||||||
|
const positionTickStyles: PositionTickStylesMapType = {
|
||||||
|
bottom: {
|
||||||
|
position: 'top',
|
||||||
|
style: {
|
||||||
|
labelOffset: 5,
|
||||||
|
labelMargin: 13,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
position: 'left',
|
||||||
|
style: {
|
||||||
|
labelMargin: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
position: 'right',
|
||||||
|
style: {
|
||||||
|
labelMargin: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
position: 'bottom',
|
||||||
|
style: {
|
||||||
|
labelMargin: 5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get recommended tick position for a given colorbar position
|
||||||
|
const getTickPositionForPosition = (position: ColorbarPositionType): TickPositionType => {
|
||||||
|
if (position === 'bottom') {
|
||||||
|
return 'top';
|
||||||
|
} else if (position === 'top') {
|
||||||
|
return 'bottom';
|
||||||
|
} else if (position === 'left') {
|
||||||
|
return 'right';
|
||||||
|
} else if (position === 'right') {
|
||||||
|
return 'left';
|
||||||
|
}
|
||||||
|
|
||||||
|
return positionTickStyles[position]?.position || 'top';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Container styles for colorbar
|
||||||
|
const containerStyles: ContainerStyleType = {
|
||||||
|
cursor: 'initial',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tick styling
|
||||||
|
const tickStyles: TickStyleType = {
|
||||||
|
font: '12px Arial',
|
||||||
|
color: '#fff',
|
||||||
|
maxNumTicks: 6,
|
||||||
|
tickSize: 5,
|
||||||
|
tickWidth: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorbarConfig: Partial<ColorbarProperties> = {
|
||||||
|
colorbarTickPosition: getTickPositionForPosition(defaultPosition),
|
||||||
|
colormaps: colormaps as unknown as Record<string, ColorMapPreset>,
|
||||||
|
colorbarContainerPosition: defaultPosition,
|
||||||
|
colorbarInitialColormap: DefaultColormap,
|
||||||
|
positionStyles,
|
||||||
|
positionTickStyles,
|
||||||
|
containerStyles,
|
||||||
|
tickStyles,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'cornerstone.colorbar': colorbarConfig,
|
||||||
|
};
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
|
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
|
||||||
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
|
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
|
||||||
import { getViewportOrientationMenu } from '../components/ViewportOrientationMenu';
|
import { getViewportOrientationMenu } from '../components/ViewportOrientationMenu';
|
||||||
import { AllInOneMenu } from '@ohif/ui-next';
|
import { ViewportActionCorners } from '@ohif/ui-next';
|
||||||
|
import { MENU_IDS } from '../components/menus/menu-ids';
|
||||||
|
|
||||||
// Generate component renderer functions for each component type
|
// Generate component renderer functions for each component type
|
||||||
const createOrientationMenu = ({ viewportId, element, location }) => {
|
const createOrientationMenu = ({ viewportId, element, location }) => {
|
||||||
@ -21,31 +22,28 @@ const createDataOverlay = ({ viewportId, element, displaySets, location }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createWindowLevelMenu = ({ viewportId, element, displaySets }) => {
|
const createWindowLevelMenu = ({ viewportId, element, displaySets, location }) => {
|
||||||
return getWindowLevelActionMenu({
|
return getWindowLevelActionMenu({
|
||||||
viewportId,
|
viewportId,
|
||||||
element,
|
element,
|
||||||
displaySets,
|
displaySets,
|
||||||
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
|
location,
|
||||||
horizontalDirection: AllInOneMenu.HorizontalDirection.LeftToRight,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
'ui.viewportActionCorner': ViewportActionCorners,
|
||||||
'viewportActionMenu.topLeft': [
|
'viewportActionMenu.topLeft': [
|
||||||
{
|
{
|
||||||
id: 'orientationMenu',
|
id: MENU_IDS.ORIENTATION_MENU,
|
||||||
enabled: true,
|
|
||||||
component: createOrientationMenu,
|
component: createOrientationMenu,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dataOverlay',
|
id: MENU_IDS.DATA_OVERLAY_MENU,
|
||||||
enabled: true,
|
|
||||||
component: createDataOverlay,
|
component: createDataOverlay,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'windowLevelActionMenu',
|
id: MENU_IDS.WINDOW_LEVEL_MENU,
|
||||||
enabled: true,
|
|
||||||
component: createWindowLevelMenu,
|
component: createWindowLevelMenu,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export { default as useViewportActionCorners } from './useViewportActionCorners';
|
|
||||||
export * from './useViewportActionCorners';
|
export * from './useViewportActionCorners';
|
||||||
export * from './useActiveViewportSegmentationRepresentations';
|
export * from './useActiveViewportSegmentationRepresentations';
|
||||||
export * from './useMeasurements';
|
export * from './useMeasurements';
|
||||||
|
|||||||
@ -1,113 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
import { ViewportActionCornersLocations } from '@ohif/ui-next';
|
|
||||||
import { useSystem } from '@ohif/core/src';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Viewport action corner location type
|
|
||||||
*/
|
|
||||||
export interface ViewportActionCornerLocation {
|
|
||||||
type: 'VIEWPORT';
|
|
||||||
id: string;
|
|
||||||
corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Viewport action corner service interface
|
|
||||||
*/
|
|
||||||
export interface ViewportActionCornerService {
|
|
||||||
addComponent: (params: {
|
|
||||||
viewportId: string;
|
|
||||||
id: string;
|
|
||||||
component: React.ReactNode;
|
|
||||||
location: ViewportActionCornersLocations | string;
|
|
||||||
indexPriority?: number;
|
|
||||||
}) => void;
|
|
||||||
clear: (viewportId: string) => void;
|
|
||||||
getAlignAndSide: (location: ViewportActionCornersLocations | string) => {
|
|
||||||
align: 'start' | 'center' | 'end';
|
|
||||||
side: 'top' | 'right' | 'bottom' | 'left';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customization service interface
|
|
||||||
*/
|
|
||||||
export interface CustomizationService {
|
|
||||||
getCustomization: (name: string) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the useViewportActionCorners hook
|
|
||||||
*/
|
|
||||||
interface UseViewportActionCornersProps {
|
|
||||||
viewportId: string;
|
|
||||||
elementRef: React.MutableRefObject<HTMLDivElement>;
|
|
||||||
displaySets: AppTypes.DisplaySet[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map of customization keys to their corresponding enum values
|
|
||||||
const locationMap = {
|
|
||||||
'viewportActionMenu.topLeft': ViewportActionCornersLocations.topLeft,
|
|
||||||
'viewportActionMenu.topRight': ViewportActionCornersLocations.topRight,
|
|
||||||
'viewportActionMenu.bottomLeft': ViewportActionCornersLocations.bottomLeft,
|
|
||||||
'viewportActionMenu.bottomRight': ViewportActionCornersLocations.bottomRight,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to manage viewport action corners for a Cornerstone viewport
|
|
||||||
*/
|
|
||||||
export function useViewportActionCorners({
|
|
||||||
viewportId,
|
|
||||||
elementRef,
|
|
||||||
displaySets,
|
|
||||||
}: UseViewportActionCornersProps): void {
|
|
||||||
const { servicesManager } = useSystem();
|
|
||||||
const { viewportActionCornersService, customizationService } = servicesManager.services;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Process each location
|
|
||||||
Object.entries(locationMap).forEach(([locationKey, locationValue]) => {
|
|
||||||
const items = customizationService.getCustomization(locationKey);
|
|
||||||
|
|
||||||
if (!items || !items.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
if (!item.enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentId = item.id;
|
|
||||||
|
|
||||||
if (item.component) {
|
|
||||||
// Use the component renderer provided directly in the item
|
|
||||||
const component = item.component({
|
|
||||||
viewportId,
|
|
||||||
element: elementRef.current,
|
|
||||||
displaySets,
|
|
||||||
location: locationValue,
|
|
||||||
});
|
|
||||||
|
|
||||||
viewportActionCornersService.addComponent({
|
|
||||||
viewportId,
|
|
||||||
id: componentId,
|
|
||||||
component,
|
|
||||||
location: locationValue,
|
|
||||||
});
|
|
||||||
} else if (item.component) {
|
|
||||||
// Handle static components
|
|
||||||
viewportActionCornersService.addComponent({
|
|
||||||
viewportId,
|
|
||||||
id: item.id,
|
|
||||||
component: item.component,
|
|
||||||
location: locationValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [displaySets, viewportId, viewportActionCornersService, elementRef]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useViewportActionCorners;
|
|
||||||
@ -35,7 +35,6 @@ import RectangleROI from './utils/measurementServiceMappings/RectangleROI';
|
|||||||
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 ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
||||||
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
|
|
||||||
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||||
import { findNearbyToolData } from './utils/findNearbyToolData';
|
import { findNearbyToolData } from './utils/findNearbyToolData';
|
||||||
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
|
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
|
||||||
@ -153,7 +152,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
||||||
*/
|
*/
|
||||||
preRegistration: async function (props: Types.Extensions.ExtensionParams): Promise<void> {
|
preRegistration: async function (props: Types.Extensions.ExtensionParams): Promise<void> {
|
||||||
const { servicesManager, serviceProvidersManager } = props;
|
const { servicesManager } = 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);
|
||||||
@ -162,11 +161,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
servicesManager.registerService(ViewportActionCornersService.REGISTRATION);
|
servicesManager.registerService(ViewportActionCornersService.REGISTRATION);
|
||||||
servicesManager.registerService(ColorbarService.REGISTRATION);
|
servicesManager.registerService(ColorbarService.REGISTRATION);
|
||||||
|
|
||||||
serviceProvidersManager.registerProvider(
|
|
||||||
ViewportActionCornersService.REGISTRATION.name,
|
|
||||||
ViewportActionCornersProvider
|
|
||||||
);
|
|
||||||
|
|
||||||
const { syncGroupService } = servicesManager.services;
|
const { syncGroupService } = servicesManager.services;
|
||||||
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
|
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
|
||||||
|
|
||||||
|
|||||||
@ -1,47 +1,35 @@
|
|||||||
import { PubSubService } from '@ohif/core';
|
import { PubSubService, Types as OhifTypes } from '@ohif/core';
|
||||||
import { RENDERING_ENGINE_ID } from '../ViewportService/constants';
|
import { RENDERING_ENGINE_ID } from '../ViewportService/constants';
|
||||||
import { StackViewport, VolumeViewport, getRenderingEngine } from '@cornerstonejs/core';
|
import { getRenderingEngine } from '@cornerstonejs/core';
|
||||||
import { utilities } from '@cornerstonejs/tools';
|
|
||||||
import { ColorbarOptions, ChangeTypes } from '../../types/Colorbar';
|
import { ColorbarOptions, ChangeTypes } from '../../types/Colorbar';
|
||||||
const { ViewportColorbar } = utilities.voi.colorbar;
|
|
||||||
|
|
||||||
export default class ColorbarService extends PubSubService {
|
export default class ColorbarService extends PubSubService {
|
||||||
static EVENTS = {
|
static EVENTS = {
|
||||||
STATE_CHANGED: 'event::ColorbarService:stateChanged',
|
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 = {
|
public static REGISTRATION = {
|
||||||
name: 'colorbarService',
|
name: 'colorbarService',
|
||||||
create: ({ servicesManager }: OhifTypes.Extensions.ExtensionParams) => {
|
create: ({ servicesManager }: OhifTypes.Extensions.ExtensionParams) => {
|
||||||
return new ColorbarService(servicesManager);
|
return new ColorbarService(servicesManager);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure of colorbars state:
|
||||||
|
* {
|
||||||
|
* [viewportId]: [
|
||||||
|
* {
|
||||||
|
* displaySetInstanceUID: string,
|
||||||
|
* colorbar: {
|
||||||
|
* activeColormapName: string,
|
||||||
|
* colormaps: array,
|
||||||
|
* volumeId: string (optional),
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
colorbars = {};
|
colorbars = {};
|
||||||
servicesManager: AppTypes.ServicesManager;
|
servicesManager: AppTypes.ServicesManager;
|
||||||
|
|
||||||
@ -51,25 +39,37 @@ export default class ColorbarService extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the volume ID for a given identifier by searching through the viewport's volume IDs.
|
* Gets the appropriate data ID for a viewport and display set
|
||||||
* @param viewport - The viewport instance to search volumes in
|
* @param viewport - The viewport instance
|
||||||
* @param searchId - The identifier to search for within volume IDs
|
* @param displaySetInstanceUID - The display set instance UID to identify data
|
||||||
* @returns The matching volume ID if found, null otherwise
|
* @returns The appropriate data ID for the viewport type (volumeId for volume viewports, undefined for stack)
|
||||||
*/
|
*/
|
||||||
private getVolumeIdForIdentifier(viewport, searchId: string): string | null {
|
private getDataIdForViewport(viewport, displaySetInstanceUID: string): string | undefined {
|
||||||
const volumeIds = viewport.getAllVolumeIds?.() || [];
|
// For volume viewports, find the matching volumeId
|
||||||
return volumeIds.length > 0 ? volumeIds.find(id => id.includes(searchId)) || null : null;
|
if (viewport.getAllVolumeIds) {
|
||||||
|
const volumeIds = viewport.getAllVolumeIds() || [];
|
||||||
|
return volumeIds.length > 0
|
||||||
|
? volumeIds.find(id => id.includes(displaySetInstanceUID)) || undefined
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other viewports, no specific dataId is needed for now
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`.
|
* 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.
|
* This method prepares the colorbar state that will be used by the ViewportColorbarsContainer component.
|
||||||
*
|
*
|
||||||
* @param viewportId The identifier for the viewport where the colorbar will be added.
|
* @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 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.
|
* @param options Configuration options for the colorbar, including position, colormaps, active colormap name, ticks, and width.
|
||||||
*/
|
*/
|
||||||
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
|
public addColorbar(
|
||||||
|
viewportId: string,
|
||||||
|
displaySetInstanceUIDs: string[],
|
||||||
|
options = {} as ColorbarOptions
|
||||||
|
) {
|
||||||
const { displaySetService } = this.servicesManager.services;
|
const { displaySetService } = this.servicesManager.services;
|
||||||
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
|
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
@ -78,35 +78,24 @@ export default class ColorbarService extends PubSubService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { element } = viewport;
|
|
||||||
const actorEntries = viewport.getActors();
|
const actorEntries = viewport.getActors();
|
||||||
|
|
||||||
if (!actorEntries || actorEntries.length === 0) {
|
if (!actorEntries || actorEntries.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { position, width: thickness, activeColormapName, colormaps } = options;
|
const { activeColormapName, colormaps } = options;
|
||||||
|
|
||||||
const numContainers = displaySetInstanceUIDs.length;
|
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
|
||||||
|
|
||||||
const containers = this.createContainers(
|
|
||||||
numContainers,
|
|
||||||
element,
|
|
||||||
position,
|
|
||||||
thickness,
|
|
||||||
viewportId
|
|
||||||
);
|
|
||||||
|
|
||||||
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
|
|
||||||
// don't show colorbar for overlay display sets (e.g. segmentation)
|
// don't show colorbar for overlay display sets (e.g. segmentation)
|
||||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||||
if (displaySet.isOverlayDisplaySet) {
|
if (displaySet.isOverlayDisplaySet) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID);
|
const dataId = this.getDataIdForViewport(viewport, displaySetInstanceUID);
|
||||||
const properties = viewport?.getProperties(volumeId);
|
const properties = dataId ? viewport.getProperties(dataId) : viewport.getProperties();
|
||||||
const colormap = properties?.colormap;
|
const colormap = properties?.colormap;
|
||||||
|
|
||||||
if (activeColormapName && !colormap) {
|
if (activeColormapName && !colormap) {
|
||||||
this.setViewportColormap(
|
this.setViewportColormap(
|
||||||
viewportId,
|
viewportId,
|
||||||
@ -116,38 +105,43 @@ export default class ColorbarService extends PubSubService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorbarContainer = containers[index];
|
// Prepare colorbar data for the React component
|
||||||
|
const colorbarData = {
|
||||||
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',
|
activeColormapName: colormap?.name || options?.activeColormapName || 'Grayscale',
|
||||||
container: colorbarContainer,
|
colormaps: options.colormaps ? Object.values(options.colormaps) : [],
|
||||||
ticks: {
|
volumeId: dataId,
|
||||||
...ColorbarService.defaultTickStyles,
|
dataId,
|
||||||
...options.ticks,
|
};
|
||||||
},
|
|
||||||
volumeId: viewport instanceof VolumeViewport ? volumeId : undefined,
|
// Store the colorbar data in the service state
|
||||||
});
|
|
||||||
if (this.colorbars[viewportId]) {
|
if (this.colorbars[viewportId]) {
|
||||||
this.colorbars[viewportId].push({
|
// Check if there's already an entry for this displaySetInstanceUID
|
||||||
colorbar,
|
const existingIndex = this.colorbars[viewportId].findIndex(
|
||||||
container: colorbarContainer,
|
item => item.displaySetInstanceUID === displaySetInstanceUID
|
||||||
displaySetInstanceUID,
|
);
|
||||||
});
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
// Update existing colorbar
|
||||||
|
this.colorbars[viewportId][existingIndex].colorbar = colorbarData;
|
||||||
|
} else {
|
||||||
|
// Add new colorbar
|
||||||
|
this.colorbars[viewportId].push({
|
||||||
|
colorbar: colorbarData,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Create new colorbar array for this viewport
|
||||||
this.colorbars[viewportId] = [
|
this.colorbars[viewportId] = [
|
||||||
{
|
{
|
||||||
colorbar,
|
colorbar: colorbarData,
|
||||||
container: colorbarContainer,
|
|
||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Notify listeners about the state change
|
||||||
this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, {
|
this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, {
|
||||||
viewportId,
|
viewportId,
|
||||||
changeType: ChangeTypes.Added,
|
changeType: ChangeTypes.Added,
|
||||||
@ -175,10 +169,6 @@ export default class ColorbarService extends PubSubService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
// Remove only the specific colorbar container
|
|
||||||
const { container } = colorbarInfo[index];
|
|
||||||
container.parentNode.removeChild(container);
|
|
||||||
|
|
||||||
// Remove the colorbar from the array
|
// Remove the colorbar from the array
|
||||||
colorbarInfo.splice(index, 1);
|
colorbarInfo.splice(index, 1);
|
||||||
|
|
||||||
@ -188,11 +178,6 @@ export default class ColorbarService extends PubSubService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Remove all colorbars for the viewport (original behavior)
|
|
||||||
colorbarInfo.forEach(({ container }) => {
|
|
||||||
container.parentNode.removeChild(container);
|
|
||||||
});
|
|
||||||
|
|
||||||
delete this.colorbars[viewportId];
|
delete this.colorbars[viewportId];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,71 +244,15 @@ export default class ColorbarService extends PubSubService {
|
|||||||
if (!viewport || !actorEntries || actorEntries.length === 0) {
|
if (!viewport || !actorEntries || actorEntries.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const setViewportProperties = (viewport, uid) => {
|
|
||||||
const volumeId = this.getVolumeIdForIdentifier(viewport, uid);
|
|
||||||
viewport.setProperties({ colormap }, volumeId);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (viewport instanceof StackViewport) {
|
// Get the appropriate dataId for this viewport/displaySet combination
|
||||||
setViewportProperties(viewport, viewportId);
|
const dataId = this.getDataIdForViewport(viewport, displaySetInstanceUID);
|
||||||
}
|
|
||||||
|
|
||||||
if (viewport instanceof VolumeViewport) {
|
// Set properties with or without dataId based on what the viewport supports
|
||||||
setViewportProperties(viewport, displaySetInstanceUID);
|
viewport.setProperties({ colormap }, dataId);
|
||||||
}
|
|
||||||
|
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
viewport.render();
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,5 @@
|
|||||||
import { PubSubService } from '@ohif/core';
|
import { PubSubService } from '@ohif/core';
|
||||||
import { ViewportActionCornersLocations } from '@ohif/ui-next';
|
import { ViewportActionCornersLocations, Types } from '@ohif/ui-next';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export type ActionComponentInfo = {
|
|
||||||
viewportId: string;
|
|
||||||
id: string;
|
|
||||||
component: ReactNode;
|
|
||||||
location: ViewportActionCornersLocations;
|
|
||||||
indexPriority?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AlignAndSide = {
|
|
||||||
align: 'start' | 'end' | 'center';
|
|
||||||
side: 'top' | 'bottom' | 'left' | 'right';
|
|
||||||
};
|
|
||||||
|
|
||||||
class ViewportActionCornersService extends PubSubService {
|
class ViewportActionCornersService extends PubSubService {
|
||||||
public static readonly EVENTS = {};
|
public static readonly EVENTS = {};
|
||||||
@ -40,39 +26,121 @@ class ViewportActionCornersService extends PubSubService {
|
|||||||
getState: getStateImplementation,
|
getState: getStateImplementation,
|
||||||
addComponent: addComponentImplementation,
|
addComponent: addComponentImplementation,
|
||||||
addComponents: addComponentsImplementation,
|
addComponents: addComponentsImplementation,
|
||||||
clear: clearComponentsImplementation,
|
clear: clearImplementation,
|
||||||
|
lockItem: lockItemImplementation,
|
||||||
|
unlockItem: unlockItemImplementation,
|
||||||
|
toggleLock: toggleLockImplementation,
|
||||||
|
isItemLocked: isItemLockedImplementation,
|
||||||
|
showItem: showItemImplementation,
|
||||||
|
hideItem: hideItemImplementation,
|
||||||
|
toggleVisibility: toggleVisibilityImplementation,
|
||||||
|
isItemVisible: isItemVisibleImplementation,
|
||||||
|
openItem: openItemImplementation,
|
||||||
|
closeItem: closeItemImplementation,
|
||||||
|
closeAllItems: closeAllItemsImplementation,
|
||||||
|
isItemOpen: isItemOpenImplementation,
|
||||||
}): void {
|
}): void {
|
||||||
if (getStateImplementation) {
|
this.serviceImplementation._getState =
|
||||||
this.serviceImplementation._getState = getStateImplementation;
|
getStateImplementation ?? this.serviceImplementation._getState;
|
||||||
}
|
this.serviceImplementation._addComponent =
|
||||||
if (addComponentImplementation) {
|
addComponentImplementation ?? this.serviceImplementation._addComponent;
|
||||||
this.serviceImplementation._addComponent = addComponentImplementation;
|
this.serviceImplementation._addComponents =
|
||||||
}
|
addComponentsImplementation ?? this.serviceImplementation._addComponents;
|
||||||
if (addComponentsImplementation) {
|
this.serviceImplementation._clear = clearImplementation ?? this.serviceImplementation._clear;
|
||||||
this.serviceImplementation._addComponents = addComponentsImplementation;
|
this.serviceImplementation._lockItem =
|
||||||
}
|
lockItemImplementation ?? this.serviceImplementation._lockItem;
|
||||||
if (clearComponentsImplementation) {
|
this.serviceImplementation._unlockItem =
|
||||||
this.serviceImplementation._clear = clearComponentsImplementation;
|
unlockItemImplementation ?? this.serviceImplementation._unlockItem;
|
||||||
}
|
this.serviceImplementation._toggleLock =
|
||||||
|
toggleLockImplementation ?? this.serviceImplementation._toggleLock;
|
||||||
|
this.serviceImplementation._isItemLocked =
|
||||||
|
isItemLockedImplementation ?? this.serviceImplementation._isItemLocked;
|
||||||
|
this.serviceImplementation._showItem =
|
||||||
|
showItemImplementation ?? this.serviceImplementation._showItem;
|
||||||
|
this.serviceImplementation._hideItem =
|
||||||
|
hideItemImplementation ?? this.serviceImplementation._hideItem;
|
||||||
|
this.serviceImplementation._toggleVisibility =
|
||||||
|
toggleVisibilityImplementation ?? this.serviceImplementation._toggleVisibility;
|
||||||
|
this.serviceImplementation._isItemVisible =
|
||||||
|
isItemVisibleImplementation ?? this.serviceImplementation._isItemVisible;
|
||||||
|
this.serviceImplementation._openItem =
|
||||||
|
openItemImplementation ?? this.serviceImplementation._openItem;
|
||||||
|
this.serviceImplementation._closeItem =
|
||||||
|
closeItemImplementation ?? this.serviceImplementation._closeItem;
|
||||||
|
this.serviceImplementation._closeAllItems =
|
||||||
|
closeAllItemsImplementation ?? this.serviceImplementation._closeAllItems;
|
||||||
|
this.serviceImplementation._isItemOpen =
|
||||||
|
isItemOpenImplementation ?? this.serviceImplementation._isItemOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getState() {
|
public getState() {
|
||||||
return this.serviceImplementation._getState();
|
return this.serviceImplementation._getState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public addComponent(component: ActionComponentInfo) {
|
public addComponent(component: Types.ActionComponentInfo) {
|
||||||
this.serviceImplementation._addComponent(component);
|
this.serviceImplementation._addComponent(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addComponents(components: Array<ActionComponentInfo>) {
|
public addComponents(components: Array<Types.ActionComponentInfo>) {
|
||||||
this.serviceImplementation._addComponents(components);
|
this.serviceImplementation._addComponents(components);
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear(viewportId: string) {
|
public clear(viewportId: string): void {
|
||||||
this.serviceImplementation._clear(viewportId);
|
this.serviceImplementation._clear(viewportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAlignAndSide(location: ViewportActionCornersLocations): AlignAndSide {
|
/* lock / unlock */
|
||||||
|
public lockItem(viewportId: string, itemId: string): void {
|
||||||
|
this.serviceImplementation._lockItem(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unlockItem(viewportId: string, itemId: string): void {
|
||||||
|
this.serviceImplementation._unlockItem(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleLock(viewportId: string, itemId: string): void {
|
||||||
|
this.serviceImplementation._toggleLock(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isItemLocked(viewportId: string, itemId: string): boolean {
|
||||||
|
return this.serviceImplementation._isItemLocked(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visibility */
|
||||||
|
public showItem(viewportId: string, itemId: string): void {
|
||||||
|
this.serviceImplementation._showItem(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public hideItem(viewportId: string, itemId: string): void {
|
||||||
|
this.serviceImplementation._hideItem(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleVisibility(viewportId: string, itemId: string): void {
|
||||||
|
this.serviceImplementation._toggleVisibility(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isItemVisible(viewportId: string, itemId: string): boolean {
|
||||||
|
return this.serviceImplementation._isItemVisible(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open / close */
|
||||||
|
public openItem(viewportId: string, itemId: string): void {
|
||||||
|
this.serviceImplementation._openItem(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeItem(viewportId: string, itemId: string): void {
|
||||||
|
this.serviceImplementation._closeItem(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeAllItems(viewportId: string): void {
|
||||||
|
this.serviceImplementation._closeAllItems(viewportId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isItemOpen(viewportId: string, itemId: string): boolean {
|
||||||
|
return this.serviceImplementation._isItemOpen(viewportId, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAlignAndSide(location: ViewportActionCornersLocations): Types.AlignAndSide {
|
||||||
switch (location) {
|
switch (location) {
|
||||||
case ViewportActionCornersLocations.topLeft:
|
case ViewportActionCornersLocations.topLeft:
|
||||||
return { align: 'start', side: 'bottom' };
|
return { align: 'start', side: 'bottom' };
|
||||||
|
|||||||
@ -1,27 +1,112 @@
|
|||||||
import { ColorMapPreset } from './Colormap';
|
import { ColorMapPreset } from './Colormap';
|
||||||
|
|
||||||
|
// Position options
|
||||||
|
export type ColorbarPositionType = 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
|
||||||
|
// Tick position options
|
||||||
|
export type TickPositionType = 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
|
||||||
|
// CSS style properties for ticks
|
||||||
|
export type TickStyleType = {
|
||||||
|
font?: string;
|
||||||
|
color?: string;
|
||||||
|
maxNumTicks?: number;
|
||||||
|
tickSize?: number;
|
||||||
|
tickWidth?: number;
|
||||||
|
labelMargin?: number;
|
||||||
|
labelOffset?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Position-specific styles
|
||||||
|
export type PositionStyleType = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Styles for a specific position (like bottom)
|
||||||
|
export type PositionTickStyleType = {
|
||||||
|
position: TickPositionType;
|
||||||
|
style?: TickStyleType;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map of position to position-specific styles
|
||||||
|
export type PositionTickStylesMapType = {
|
||||||
|
[key in ColorbarPositionType]?: PositionTickStyleType;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map of position to position-specific CSS
|
||||||
|
export type PositionStylesMapType = {
|
||||||
|
[key in ColorbarPositionType]?: PositionStyleType;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Container styles
|
||||||
|
export type ContainerStyleType = {
|
||||||
|
position?: string;
|
||||||
|
boxSizing?: string;
|
||||||
|
border?: string;
|
||||||
|
cursor?: string;
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dimension configuration
|
||||||
|
export type DimensionConfigType = {
|
||||||
|
bottomHeight: string;
|
||||||
|
defaultVerticalWidth: string;
|
||||||
|
defaultHorizontalHeight: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tick configuration
|
||||||
|
export type TickConfigType = {
|
||||||
|
position: TickPositionType;
|
||||||
|
style?: TickStyleType;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base options for colorbar
|
||||||
export type ColorbarOptions = {
|
export type ColorbarOptions = {
|
||||||
position: string;
|
position: ColorbarPositionType;
|
||||||
colormaps: Array<ColorMapPreset>;
|
colormaps: Record<string, ColorMapPreset>;
|
||||||
activeColormapName: string;
|
activeColormapName: string;
|
||||||
ticks: object;
|
ticks?: TickConfigType;
|
||||||
width: string;
|
width: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Props for the Colorbar component
|
||||||
export type ColorbarProps = {
|
export type ColorbarProps = {
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
displaySets: Array<any>;
|
displaySets: Array<any>;
|
||||||
colorbarProperties: ColorbarProperties;
|
colorbarProperties: ColorbarProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extended properties with styling options
|
||||||
export type ColorbarProperties = {
|
export type ColorbarProperties = {
|
||||||
width: string;
|
width: string;
|
||||||
colorbarTickPosition: string;
|
colorbarTickPosition: TickPositionType;
|
||||||
colorbarContainerPosition: string;
|
colorbarContainerPosition: ColorbarPositionType;
|
||||||
colormaps: Array<ColorMapPreset>;
|
colormaps: Record<string, ColorMapPreset>;
|
||||||
colorbarInitialColormap: string;
|
colorbarInitialColormap: string;
|
||||||
|
|
||||||
|
// Styling properties
|
||||||
|
positionStyles?: PositionStylesMapType;
|
||||||
|
positionTickStyles?: PositionTickStylesMapType;
|
||||||
|
containerStyles?: ContainerStyleType;
|
||||||
|
tickStyles?: TickStyleType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Type for the customization object from the customization service
|
||||||
|
export interface ColorbarCustomization {
|
||||||
|
width: string;
|
||||||
|
colorbarTickPosition: TickPositionType;
|
||||||
|
colorbarContainerPosition: ColorbarPositionType;
|
||||||
|
colormaps: Record<string, ColorMapPreset>;
|
||||||
|
colorbarInitialColormap: string;
|
||||||
|
|
||||||
|
// Styling properties
|
||||||
|
positionStyles: PositionStylesMapType;
|
||||||
|
positionTickStyles: PositionTickStylesMapType;
|
||||||
|
containerStyles: ContainerStyleType;
|
||||||
|
tickStyles: TickStyleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event types for colorbar changes
|
||||||
export enum ChangeTypes {
|
export enum ChangeTypes {
|
||||||
Removed = 'removed',
|
Removed = 'removed',
|
||||||
Added = 'added',
|
Added = 'added',
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
import { ViewportActionCorners } from '@ohif/ui-next';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
'ui.viewportActionCorner': ViewportActionCorners,
|
|
||||||
};
|
|
||||||
@ -15,7 +15,6 @@ import onDropHandlerCustomization from './customizations/onDropHandlerCustomizat
|
|||||||
import loadingIndicatorProgressCustomization from './customizations/loadingIndicatorProgressCustomization';
|
import loadingIndicatorProgressCustomization from './customizations/loadingIndicatorProgressCustomization';
|
||||||
import loadingIndicatorTotalPercentCustomization from './customizations/loadingIndicatorTotalPercentCustomization';
|
import loadingIndicatorTotalPercentCustomization from './customizations/loadingIndicatorTotalPercentCustomization';
|
||||||
import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization';
|
import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization';
|
||||||
import viewportActionCornersCustomization from './customizations/viewportActionCornersCustomization';
|
|
||||||
import labellingFlowCustomization from './customizations/labellingFlowCustomization';
|
import labellingFlowCustomization from './customizations/labellingFlowCustomization';
|
||||||
import viewportNotificationCustomization from './customizations/notificationCustomization';
|
import viewportNotificationCustomization from './customizations/notificationCustomization';
|
||||||
import aboutModalCustomization from './customizations/aboutModalCustomization';
|
import aboutModalCustomization from './customizations/aboutModalCustomization';
|
||||||
@ -62,7 +61,6 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
|||||||
...loadingIndicatorProgressCustomization,
|
...loadingIndicatorProgressCustomization,
|
||||||
...loadingIndicatorTotalPercentCustomization,
|
...loadingIndicatorTotalPercentCustomization,
|
||||||
...progressLoadingBarCustomization,
|
...progressLoadingBarCustomization,
|
||||||
...viewportActionCornersCustomization,
|
|
||||||
...labellingFlowCustomization,
|
...labellingFlowCustomization,
|
||||||
...contextMenuUICustomization,
|
...contextMenuUICustomization,
|
||||||
...viewportNotificationCustomization,
|
...viewportNotificationCustomization,
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
ModalProvider,
|
ModalProvider,
|
||||||
ViewportDialogProvider,
|
ViewportDialogProvider,
|
||||||
UserAuthenticationProvider,
|
UserAuthenticationProvider,
|
||||||
|
ViewportActionCornersProvider,
|
||||||
} from '@ohif/ui-next';
|
} from '@ohif/ui-next';
|
||||||
// Viewer Project
|
// Viewer Project
|
||||||
// TODO: Should this influence study list?
|
// TODO: Should this influence study list?
|
||||||
@ -104,6 +105,7 @@ function App({
|
|||||||
userAuthenticationService,
|
userAuthenticationService,
|
||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
customizationService,
|
customizationService,
|
||||||
|
viewportActionCornersService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const providers = [
|
const providers = [
|
||||||
@ -119,6 +121,7 @@ function App({
|
|||||||
[TooltipProvider],
|
[TooltipProvider],
|
||||||
[DialogProvider, { service: uiDialogService, dialog: ManagedDialog }],
|
[DialogProvider, { service: uiDialogService, dialog: ManagedDialog }],
|
||||||
[ModalProvider, { service: uiModalService, modal: ModalNext }],
|
[ModalProvider, { service: uiModalService, modal: ModalNext }],
|
||||||
|
[ViewportActionCornersProvider, { service: viewportActionCornersService }],
|
||||||
[ShepherdJourneyProvider],
|
[ShepherdJourneyProvider],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import { ViewportGrid, ViewportPane } from '@ohif/ui-next';
|
|||||||
import { useViewportGrid } from '@ohif/ui-next';
|
import { useViewportGrid } from '@ohif/ui-next';
|
||||||
import EmptyViewport from './EmptyViewport';
|
import EmptyViewport from './EmptyViewport';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
|
import { useViewportActionCornersWithGrid } from '../hooks';
|
||||||
|
|
||||||
function ViewerViewportGrid(props: withAppTypes) {
|
function ViewerViewportGrid(props: withAppTypes) {
|
||||||
const { servicesManager, viewportComponents = [], dataSource, commandsManager } = props;
|
const { servicesManager, viewportComponents = [], dataSource, commandsManager } = props;
|
||||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
|
const { initializeViewportCorners } = useViewportActionCornersWithGrid();
|
||||||
|
|
||||||
const { layout, activeViewportId, viewports, isHangingProtocolLayout } = viewportGrid;
|
const { layout, activeViewportId, viewports, isHangingProtocolLayout } = viewportGrid;
|
||||||
const { numCols, numRows } = layout;
|
const { numCols, numRows } = layout;
|
||||||
@ -415,8 +417,14 @@ function ViewerViewportGrid(props: withAppTypes) {
|
|||||||
displaySetOptions={displaySetOptions}
|
displaySetOptions={displaySetOptions}
|
||||||
needsRerendering={displaySetsNeedsRerendering}
|
needsRerendering={displaySetsNeedsRerendering}
|
||||||
isHangingProtocolLayout={isHangingProtocolLayout}
|
isHangingProtocolLayout={isHangingProtocolLayout}
|
||||||
onElementEnabled={() => {
|
onElementEnabled={evt => {
|
||||||
viewportGridService.setViewportIsReady(viewportId, true);
|
viewportGridService.setViewportIsReady(viewportId, true);
|
||||||
|
|
||||||
|
// Initialize the viewport action corners for this viewport
|
||||||
|
if (evt?.detail?.element) {
|
||||||
|
const elementRef = { current: evt.detail.element };
|
||||||
|
initializeViewportCorners(viewportId, elementRef, displaySets, commandsManager);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -425,7 +433,7 @@ function ViewerViewportGrid(props: withAppTypes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return viewportPanes;
|
return viewportPanes;
|
||||||
}, [viewports, activeViewportId, viewportComponents, dataSource]);
|
}, [viewports, activeViewportId, viewportComponents, dataSource, initializeViewportCorners]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loading indicator until numCols and numRows are gotten from the HangingProtocolService
|
* Loading indicator until numCols and numRows are gotten from the HangingProtocolService
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import useDebounce from './useDebounce.js';
|
import useDebounce from './useDebounce';
|
||||||
import useSearchParams from './useSearchParams';
|
import useSearchParams from './useSearchParams';
|
||||||
|
import useViewportActionCornersWithGrid from './useViewportActionCornersWithGrid';
|
||||||
|
|
||||||
export { useDebounce, useSearchParams };
|
export { useDebounce, useSearchParams, useViewportActionCornersWithGrid };
|
||||||
|
|||||||
5
platform/app/src/hooks/index.ts
Normal file
5
platform/app/src/hooks/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import useViewportActionCornersWithGrid from './useViewportActionCornersWithGrid';
|
||||||
|
import useDebounce from './useDebounce';
|
||||||
|
import useSearchParams from './useSearchParams';
|
||||||
|
|
||||||
|
export { useViewportActionCornersWithGrid, useDebounce, useSearchParams };
|
||||||
112
platform/app/src/hooks/useViewportActionCornersWithGrid.ts
Normal file
112
platform/app/src/hooks/useViewportActionCornersWithGrid.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { useCallback, MutableRefObject, useRef } from 'react';
|
||||||
|
import { useViewportActionCorners, ViewportActionCornersLocations } from '@ohif/ui-next';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that manages viewport action corner components for all viewports in the grid
|
||||||
|
* @returns A function that can be called to initialize action corners for a viewport
|
||||||
|
*/
|
||||||
|
export default function useViewportActionCornersWithGrid() {
|
||||||
|
const [, api] = useViewportActionCorners();
|
||||||
|
const { servicesManager } = useSystem();
|
||||||
|
const { customizationService } = servicesManager.services;
|
||||||
|
|
||||||
|
// Keep a ref to track processed viewports to avoid duplicates
|
||||||
|
const processedViewports = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Map of customization keys to their corresponding enum values
|
||||||
|
const locationMap = {
|
||||||
|
'viewportActionMenu.topLeft': ViewportActionCornersLocations.topLeft,
|
||||||
|
'viewportActionMenu.topRight': ViewportActionCornersLocations.topRight,
|
||||||
|
'viewportActionMenu.bottomLeft': ViewportActionCornersLocations.bottomLeft,
|
||||||
|
'viewportActionMenu.bottomRight': ViewportActionCornersLocations.bottomRight,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to process customizations for a viewport
|
||||||
|
const initializeViewportCorners = useCallback(
|
||||||
|
(
|
||||||
|
viewportId: string,
|
||||||
|
elementRef: MutableRefObject<HTMLDivElement>,
|
||||||
|
displaySets: any[],
|
||||||
|
commandsManager: any
|
||||||
|
) => {
|
||||||
|
if (!viewportId || !elementRef?.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate processing
|
||||||
|
if (processedViewports.current.has(viewportId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark this viewport as processed
|
||||||
|
processedViewports.current.add(viewportId);
|
||||||
|
|
||||||
|
// Clear any existing components for this viewport
|
||||||
|
api.clear(viewportId);
|
||||||
|
|
||||||
|
// Process each location
|
||||||
|
Object.entries(locationMap).forEach(([locationKey, locationValue]) => {
|
||||||
|
const items = customizationService.getCustomization(locationKey);
|
||||||
|
if (!items || !items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
try {
|
||||||
|
if (typeof item.component === 'function') {
|
||||||
|
// Use the component renderer provided directly in the item
|
||||||
|
const component = item.component({
|
||||||
|
viewportId,
|
||||||
|
element: elementRef.current,
|
||||||
|
displaySets,
|
||||||
|
location: locationValue,
|
||||||
|
commandsManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (component) {
|
||||||
|
api.addComponent({
|
||||||
|
viewportId,
|
||||||
|
id: item.id,
|
||||||
|
component,
|
||||||
|
location: locationValue,
|
||||||
|
indexPriority: item.indexPriority,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (item.component) {
|
||||||
|
// Handle static components
|
||||||
|
api.addComponent({
|
||||||
|
viewportId,
|
||||||
|
id: item.id,
|
||||||
|
component: item.component,
|
||||||
|
location: locationValue,
|
||||||
|
indexPriority: item.indexPriority,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error adding component ${item.id} to viewport corner:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[api, customizationService]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cleanup function for unmounting viewports
|
||||||
|
const cleanupViewportCorners = useCallback(
|
||||||
|
(viewportId: string) => {
|
||||||
|
if (!viewportId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from processed set
|
||||||
|
processedViewports.current.delete(viewportId);
|
||||||
|
|
||||||
|
// Clear from the store
|
||||||
|
api.clear(viewportId);
|
||||||
|
},
|
||||||
|
[api]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { initializeViewportCorners, cleanupViewportCorners };
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import { useParams, useLocation, useNavigate } from 'react-router';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
import { ImageViewerProvider, DragAndDropProvider } from '@ohif/ui-next';
|
import { ImageViewerProvider, DragAndDropProvider } from '@ohif/ui-next';
|
||||||
import { useSearchParams } from '@hooks';
|
import { useSearchParams } from '../../hooks';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
import ViewportGrid from '@components/ViewportGrid';
|
import ViewportGrid from '@components/ViewportGrid';
|
||||||
import Compose from './Compose';
|
import Compose from './Compose';
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
//
|
//
|
||||||
import filtersMeta from './filtersMeta.js';
|
import filtersMeta from './filtersMeta.js';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
import { useDebounce, useSearchParams } from '@hooks';
|
import { useDebounce, useSearchParams } from '../../hooks';
|
||||||
import { utils, Types as coreTypes } from '@ohif/core';
|
import { utils, Types as coreTypes } from '@ohif/core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -16,6 +16,8 @@ export type DisplaySet = {
|
|||||||
label?: string;
|
label?: string;
|
||||||
/** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */
|
/** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */
|
||||||
isOverlayDisplaySet?: boolean;
|
isOverlayDisplaySet?: boolean;
|
||||||
|
/** flag indicating if it supports window level */
|
||||||
|
supportsWindowLevel?: boolean;
|
||||||
|
|
||||||
// Details about how to display:
|
// Details about how to display:
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -201,6 +201,31 @@ window.config = {
|
|||||||
],
|
],
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'viewportActionMenu.windowLevelActionMenu',
|
||||||
|
description:
|
||||||
|
'Configures the display and location of the window level action menu in the viewport.',
|
||||||
|
image: windowLevelActionMenu,
|
||||||
|
default: null,
|
||||||
|
configuration: `
|
||||||
|
window.config = {
|
||||||
|
// rest of window config
|
||||||
|
customizationService: [
|
||||||
|
{
|
||||||
|
'viewportActionMenu.windowLevelActionMenu': {
|
||||||
|
$merge: {
|
||||||
|
location: 0, // Set the location of the menu in the viewport.
|
||||||
|
// 0: topLeft
|
||||||
|
// 1: topRight
|
||||||
|
// 2: bottomLeft
|
||||||
|
// 3: bottomRight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'measurementLabels',
|
id: 'measurementLabels',
|
||||||
description: 'Labels for measurement tools in the viewer that are automatically asked for.',
|
description: 'Labels for measurement tools in the viewer that are automatically asked for.',
|
||||||
@ -937,7 +962,7 @@ window.config = {
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'viewportActionMenu.dataOverlay',
|
id: 'viewportActionMenu.dataOverlayMenu',
|
||||||
description: 'Configures the display and location of the data overlay in the viewport.',
|
description: 'Configures the display and location of the data overlay in the viewport.',
|
||||||
image: segmentationOverlay,
|
image: segmentationOverlay,
|
||||||
default: null,
|
default: null,
|
||||||
@ -946,7 +971,7 @@ window.config = {
|
|||||||
// rest of window config
|
// rest of window config
|
||||||
customizationService: [
|
customizationService: [
|
||||||
{
|
{
|
||||||
'viewportActionMenu.dataOverlay': {
|
'viewportActionMenu.dataOverlayMenu': {
|
||||||
$merge: {
|
$merge: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
location: 1, // Set the location of the overlay in the viewport.
|
location: 1, // Set the location of the overlay in the viewport.
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import React from 'react';
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
ReactNode,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
} from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A small container that can render multiple "corner" items (like icons, status)
|
|
||||||
* in each corner of the viewport: top-left, top-right, bottom-left, bottom-right.
|
|
||||||
*/
|
|
||||||
export enum ViewportActionCornersLocations {
|
export enum ViewportActionCornersLocations {
|
||||||
topLeft,
|
topLeft,
|
||||||
topRight,
|
topRight,
|
||||||
@ -33,35 +35,102 @@ const locationClasses = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
function ViewportActionCorners({ cornerComponents }) {
|
const ViewportActionCornersContext = createContext<{
|
||||||
if (!cornerComponents) {
|
registerCorner: (location: ViewportActionCornersLocations, children: ReactNode) => void;
|
||||||
return null;
|
} | null>(null);
|
||||||
}
|
|
||||||
|
function Container({ children }: { children: ReactNode }) {
|
||||||
|
const [corners, setCorners] = useState({
|
||||||
|
[ViewportActionCornersLocations.topLeft]: null,
|
||||||
|
[ViewportActionCornersLocations.topRight]: null,
|
||||||
|
[ViewportActionCornersLocations.bottomLeft]: null,
|
||||||
|
[ViewportActionCornersLocations.bottomRight]: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerCorner = useCallback(
|
||||||
|
(location: ViewportActionCornersLocations, children: ReactNode) => {
|
||||||
|
setCorners(prev => {
|
||||||
|
// Only update if the children are different to avoid unnecessary renders
|
||||||
|
if (prev[location] === children) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[location]: children,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<ViewportActionCornersContext.Provider value={{ registerCorner }}>
|
||||||
className="pointer-events-none absolute h-full w-full select-none"
|
{children}
|
||||||
onDoubleClick={event => {
|
<div
|
||||||
event.preventDefault();
|
className="pointer-events-none absolute h-full w-full select-none"
|
||||||
event.stopPropagation();
|
onDoubleClick={event => {
|
||||||
}}
|
event.preventDefault();
|
||||||
>
|
event.stopPropagation();
|
||||||
{Object.entries(cornerComponents).map(([location, locationArray]) => (
|
}}
|
||||||
<div
|
>
|
||||||
key={location}
|
{Object.entries(corners).map(([location, children]) => {
|
||||||
className={locationClasses[location]}
|
if (!children) {
|
||||||
>
|
return null;
|
||||||
{locationArray.map(componentInfo => (
|
}
|
||||||
<div key={componentInfo.id}>{componentInfo.component}</div>
|
return (
|
||||||
))}
|
<div
|
||||||
</div>
|
key={location}
|
||||||
))}
|
className={locationClasses[location]}
|
||||||
</div>
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ViewportActionCornersContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewportActionCorners.propTypes = {
|
function Corner({
|
||||||
cornerComponents: PropTypes.object.isRequired,
|
location,
|
||||||
};
|
children,
|
||||||
|
}: {
|
||||||
|
location: ViewportActionCornersLocations;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const context = useContext(ViewportActionCornersContext);
|
||||||
|
|
||||||
export { ViewportActionCorners };
|
if (!context) {
|
||||||
|
throw new Error('Corner component must be used within a ViewportActionCorners.Container');
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
context.registerCorner(location, children);
|
||||||
|
}, [context, location, children]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopLeft({ children }: { children: ReactNode }) {
|
||||||
|
return <Corner location={ViewportActionCornersLocations.topLeft}>{children}</Corner>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopRight({ children }: { children: ReactNode }) {
|
||||||
|
return <Corner location={ViewportActionCornersLocations.topRight}>{children}</Corner>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BottomLeft({ children }: { children: ReactNode }) {
|
||||||
|
return <Corner location={ViewportActionCornersLocations.bottomLeft}>{children}</Corner>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BottomRight({ children }: { children: ReactNode }) {
|
||||||
|
return <Corner location={ViewportActionCornersLocations.bottomRight}>{children}</Corner>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ViewportActionCorners = {
|
||||||
|
Container,
|
||||||
|
TopLeft,
|
||||||
|
TopRight,
|
||||||
|
BottomLeft,
|
||||||
|
BottomRight,
|
||||||
|
};
|
||||||
|
|||||||
@ -0,0 +1,550 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useReducer,
|
||||||
|
ReactNode,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
|
import { ViewportActionCornersLocations } from '../components/Viewport/ViewportActionCorners';
|
||||||
|
import { ActionComponentInfo, AlignAndSide } from '../types';
|
||||||
|
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
viewports: {} as Record<string, Record<ViewportActionCornersLocations, ActionComponentInfo[]>>,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API for managing viewport action corner items.
|
||||||
|
* These methods control the state and behavior of the corner items themselves,
|
||||||
|
* not the content they might affect in the viewport.
|
||||||
|
*/
|
||||||
|
interface ViewportActionCornersApi {
|
||||||
|
getState: () => typeof DEFAULT_STATE;
|
||||||
|
addComponent: (component: ActionComponentInfo) => void;
|
||||||
|
addComponents: (components: Array<ActionComponentInfo>) => void;
|
||||||
|
clear: (viewportId: string) => void;
|
||||||
|
getAlignAndSide: (location: ViewportActionCornersLocations) => AlignAndSide;
|
||||||
|
lockItem: (viewportId: string, itemId: string) => void;
|
||||||
|
unlockItem: (viewportId: string, itemId: string) => void;
|
||||||
|
toggleLock: (viewportId: string, itemId: string) => void;
|
||||||
|
isItemLocked: (viewportId: string, itemId: string) => boolean;
|
||||||
|
showItem: (viewportId: string, itemId: string) => void;
|
||||||
|
hideItem: (viewportId: string, itemId: string) => void;
|
||||||
|
toggleVisibility: (viewportId: string, itemId: string) => void;
|
||||||
|
isItemVisible: (viewportId: string, itemId: string) => boolean;
|
||||||
|
openItem: (viewportId: string, itemId: string) => void;
|
||||||
|
closeItem: (viewportId: string, itemId: string) => void;
|
||||||
|
closeAllItems: (viewportId: string) => void;
|
||||||
|
isItemOpen: (viewportId: string, itemId: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ViewportActionCornersContext = createContext<
|
||||||
|
[typeof DEFAULT_STATE, ViewportActionCornersApi]
|
||||||
|
>([DEFAULT_STATE, {} as ViewportActionCornersApi]);
|
||||||
|
|
||||||
|
interface ViewportActionCornersProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
service: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ViewportActionCornersProvider({
|
||||||
|
children,
|
||||||
|
service,
|
||||||
|
}: ViewportActionCornersProviderProps) {
|
||||||
|
const viewportActionCornersReducer = (state = DEFAULT_STATE, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_COMPONENT': {
|
||||||
|
const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
|
||||||
|
|
||||||
|
const newState = { ...state };
|
||||||
|
|
||||||
|
if (!newState.viewports[viewportId]) {
|
||||||
|
newState.viewports[viewportId] = {
|
||||||
|
[ViewportActionCornersLocations.topLeft]: [],
|
||||||
|
[ViewportActionCornersLocations.topRight]: [],
|
||||||
|
[ViewportActionCornersLocations.bottomLeft]: [],
|
||||||
|
[ViewportActionCornersLocations.bottomRight]: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newState.viewports[viewportId][location]) {
|
||||||
|
newState.viewports[viewportId][location] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentInfo = {
|
||||||
|
id,
|
||||||
|
component,
|
||||||
|
indexPriority,
|
||||||
|
isOpen: false,
|
||||||
|
isVisible: true,
|
||||||
|
isLocked: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const components = [...newState.viewports[viewportId][location]];
|
||||||
|
const index = components.findIndex(item => item.indexPriority > indexPriority);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
components.push(componentInfo);
|
||||||
|
} else {
|
||||||
|
components.splice(index, 0, componentInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
newState.viewports[viewportId][location] = components;
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ADD_COMPONENTS': {
|
||||||
|
const components = action.payload;
|
||||||
|
let newState = { ...state };
|
||||||
|
|
||||||
|
components.forEach(component => {
|
||||||
|
newState = viewportActionCornersReducer(newState, {
|
||||||
|
type: 'ADD_COMPONENT',
|
||||||
|
payload: component,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'CLEAR': {
|
||||||
|
const viewportId = action.payload;
|
||||||
|
const newState = { ...state };
|
||||||
|
|
||||||
|
if (newState.viewports[viewportId]) {
|
||||||
|
const newViewports = { ...newState.viewports };
|
||||||
|
delete newViewports[viewportId];
|
||||||
|
newState.viewports = newViewports;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_LOCKED': {
|
||||||
|
const { viewportId, itemId, lockedStatus } = action.payload;
|
||||||
|
const newState = { ...state };
|
||||||
|
|
||||||
|
if (!newState.viewports[viewportId]) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportCopy = { ...newState.viewports[viewportId] };
|
||||||
|
|
||||||
|
Object.keys(viewportCopy).forEach(locationKey => {
|
||||||
|
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||||
|
const components = [...viewportCopy[location]];
|
||||||
|
|
||||||
|
const itemIndex = components.findIndex(item => item.id === itemId);
|
||||||
|
if (itemIndex !== -1) {
|
||||||
|
const updatedItem = { ...components[itemIndex], isLocked: lockedStatus };
|
||||||
|
components[itemIndex] = updatedItem;
|
||||||
|
viewportCopy[location] = components;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newState.viewports[viewportId] = viewportCopy;
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_VISIBLE': {
|
||||||
|
const { viewportId, itemId, visibleStatus } = action.payload;
|
||||||
|
const newState = { ...state };
|
||||||
|
|
||||||
|
if (!newState.viewports[viewportId]) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportCopy = { ...newState.viewports[viewportId] };
|
||||||
|
|
||||||
|
Object.keys(viewportCopy).forEach(locationKey => {
|
||||||
|
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||||
|
const components = [...viewportCopy[location]];
|
||||||
|
|
||||||
|
const itemIndex = components.findIndex(item => item.id === itemId);
|
||||||
|
if (itemIndex !== -1) {
|
||||||
|
const updatedItem = { ...components[itemIndex], isVisible: visibleStatus };
|
||||||
|
components[itemIndex] = updatedItem;
|
||||||
|
viewportCopy[location] = components;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newState.viewports[viewportId] = viewportCopy;
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'OPEN_ITEM': {
|
||||||
|
const { viewportId, itemId } = action.payload;
|
||||||
|
const newState = { ...state };
|
||||||
|
|
||||||
|
if (!newState.viewports[viewportId]) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportCopy = { ...newState.viewports[viewportId] };
|
||||||
|
|
||||||
|
// Update isOpen flag for the component with matching id in any location
|
||||||
|
Object.keys(viewportCopy).forEach(locationKey => {
|
||||||
|
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||||
|
const components = [...viewportCopy[location]];
|
||||||
|
|
||||||
|
const itemIndex = components.findIndex(item => item.id === itemId);
|
||||||
|
if (itemIndex !== -1) {
|
||||||
|
const updatedItem = { ...components[itemIndex], isOpen: true };
|
||||||
|
components[itemIndex] = updatedItem;
|
||||||
|
viewportCopy[location] = components;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newState.viewports[viewportId] = viewportCopy;
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'CLOSE_ITEM': {
|
||||||
|
const { viewportId, itemId } = action.payload;
|
||||||
|
const newState = { ...state };
|
||||||
|
|
||||||
|
if (!newState.viewports[viewportId]) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportCopy = { ...newState.viewports[viewportId] };
|
||||||
|
|
||||||
|
// Update isOpen flag for the component with matching id in any location
|
||||||
|
Object.keys(viewportCopy).forEach(locationKey => {
|
||||||
|
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||||
|
const components = [...viewportCopy[location]];
|
||||||
|
|
||||||
|
const itemIndex = components.findIndex(item => item.id === itemId);
|
||||||
|
if (itemIndex !== -1) {
|
||||||
|
const updatedItem = { ...components[itemIndex], isOpen: false };
|
||||||
|
components[itemIndex] = updatedItem;
|
||||||
|
viewportCopy[location] = components;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newState.viewports[viewportId] = viewportCopy;
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'CLOSE_ALL_ITEMS': {
|
||||||
|
const viewportId = action.payload;
|
||||||
|
const newState = { ...state };
|
||||||
|
|
||||||
|
if (!newState.viewports[viewportId]) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportCopy = { ...newState.viewports[viewportId] };
|
||||||
|
|
||||||
|
// Set isOpen to false for all components in the viewport
|
||||||
|
Object.keys(viewportCopy).forEach(locationKey => {
|
||||||
|
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||||
|
const components = viewportCopy[location].map(item => ({
|
||||||
|
...item,
|
||||||
|
isOpen: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
viewportCopy[location] = components;
|
||||||
|
});
|
||||||
|
|
||||||
|
newState.viewports[viewportId] = viewportCopy;
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(viewportActionCornersReducer, DEFAULT_STATE);
|
||||||
|
|
||||||
|
const getState = useCallback(() => {
|
||||||
|
return state;
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
const addComponent = useCallback(
|
||||||
|
(component: ActionComponentInfo) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'ADD_COMPONENT',
|
||||||
|
payload: component,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addComponents = useCallback(
|
||||||
|
(components: Array<ActionComponentInfo>) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'ADD_COMPONENTS',
|
||||||
|
payload: components,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clear = useCallback(
|
||||||
|
(viewportId: string) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'CLEAR',
|
||||||
|
payload: viewportId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const lockItem = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_LOCKED',
|
||||||
|
payload: { viewportId, itemId, lockedStatus: true },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const unlockItem = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_LOCKED',
|
||||||
|
payload: { viewportId, itemId, lockedStatus: false },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleLock = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
const currentLocked = isItemLocked(viewportId, itemId);
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_LOCKED',
|
||||||
|
payload: { viewportId, itemId, lockedStatus: !currentLocked },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isItemLocked = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
if (!state.viewports[viewportId]) {
|
||||||
|
return false; // Default to unlocked if viewport doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const locationKey in state.viewports[viewportId]) {
|
||||||
|
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||||
|
const components = state.viewports[viewportId][location];
|
||||||
|
const item = components.find(item => item.id === itemId);
|
||||||
|
|
||||||
|
if (item && item.isLocked === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // Default to unlocked if item not found or isLocked is undefined
|
||||||
|
},
|
||||||
|
[state.viewports]
|
||||||
|
);
|
||||||
|
|
||||||
|
const showItem = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_VISIBLE',
|
||||||
|
payload: { viewportId, itemId, visibleStatus: true },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hideItem = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_VISIBLE',
|
||||||
|
payload: { viewportId, itemId, visibleStatus: false },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleVisibility = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
const currentVisible = isItemVisible(viewportId, itemId);
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_VISIBLE',
|
||||||
|
payload: { viewportId, itemId, visibleStatus: !currentVisible },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isItemVisible = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
if (!state.viewports[viewportId]) {
|
||||||
|
return true; // Default to visible if viewport doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const locationKey in state.viewports[viewportId]) {
|
||||||
|
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||||
|
const components = state.viewports[viewportId][location];
|
||||||
|
const item = components.find(item => item.id === itemId);
|
||||||
|
|
||||||
|
if (item && item.isVisible === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Default to visible if item not found or isVisible is undefined
|
||||||
|
},
|
||||||
|
[state.viewports]
|
||||||
|
);
|
||||||
|
|
||||||
|
const openItem = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'OPEN_ITEM',
|
||||||
|
payload: { viewportId, itemId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeItem = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'CLOSE_ITEM',
|
||||||
|
payload: { viewportId, itemId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeAllItems = useCallback(
|
||||||
|
(viewportId: string) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'CLOSE_ALL_ITEMS',
|
||||||
|
payload: viewportId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isItemOpen = useCallback(
|
||||||
|
(viewportId: string, itemId: string) => {
|
||||||
|
if (!state.viewports[viewportId]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const locationKey in state.viewports[viewportId]) {
|
||||||
|
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||||
|
const components = state.viewports[viewportId][location];
|
||||||
|
const item = components.find(item => item.id === itemId);
|
||||||
|
|
||||||
|
if (item && item.isOpen === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[state.viewports]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getAlignAndSide = useCallback(
|
||||||
|
(location: ViewportActionCornersLocations) => {
|
||||||
|
return service.getAlignAndSide(location);
|
||||||
|
},
|
||||||
|
[service]
|
||||||
|
);
|
||||||
|
|
||||||
|
const api = useMemo(() => {
|
||||||
|
return {
|
||||||
|
getState,
|
||||||
|
addComponent,
|
||||||
|
addComponents,
|
||||||
|
clear,
|
||||||
|
getAlignAndSide,
|
||||||
|
lockItem,
|
||||||
|
unlockItem,
|
||||||
|
toggleLock,
|
||||||
|
isItemLocked,
|
||||||
|
showItem,
|
||||||
|
hideItem,
|
||||||
|
toggleVisibility,
|
||||||
|
isItemVisible,
|
||||||
|
openItem,
|
||||||
|
closeItem,
|
||||||
|
closeAllItems,
|
||||||
|
isItemOpen,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
getState,
|
||||||
|
addComponent,
|
||||||
|
addComponents,
|
||||||
|
clear,
|
||||||
|
getAlignAndSide,
|
||||||
|
lockItem,
|
||||||
|
unlockItem,
|
||||||
|
toggleLock,
|
||||||
|
isItemLocked,
|
||||||
|
showItem,
|
||||||
|
hideItem,
|
||||||
|
toggleVisibility,
|
||||||
|
isItemVisible,
|
||||||
|
openItem,
|
||||||
|
closeItem,
|
||||||
|
closeAllItems,
|
||||||
|
isItemOpen,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (service && service.setServiceImplementation) {
|
||||||
|
const implementation = {
|
||||||
|
getState,
|
||||||
|
addComponent,
|
||||||
|
addComponents,
|
||||||
|
clear,
|
||||||
|
lockItem,
|
||||||
|
unlockItem,
|
||||||
|
toggleLock,
|
||||||
|
isItemLocked,
|
||||||
|
showItem,
|
||||||
|
hideItem,
|
||||||
|
toggleVisibility,
|
||||||
|
isItemVisible,
|
||||||
|
openItem,
|
||||||
|
closeItem,
|
||||||
|
closeAllItems,
|
||||||
|
isItemOpen,
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setServiceImplementation(implementation);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
service,
|
||||||
|
getState,
|
||||||
|
addComponent,
|
||||||
|
addComponents,
|
||||||
|
clear,
|
||||||
|
lockItem,
|
||||||
|
unlockItem,
|
||||||
|
toggleLock,
|
||||||
|
isItemLocked,
|
||||||
|
showItem,
|
||||||
|
hideItem,
|
||||||
|
toggleVisibility,
|
||||||
|
isItemVisible,
|
||||||
|
openItem,
|
||||||
|
closeItem,
|
||||||
|
closeAllItems,
|
||||||
|
isItemOpen,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewportActionCornersContext.Provider value={[state, api]}>
|
||||||
|
{children}
|
||||||
|
</ViewportActionCornersContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom hook to use the ViewportActionCorners context
|
||||||
|
export const useViewportActionCorners = () => useContext(ViewportActionCornersContext);
|
||||||
@ -9,6 +9,10 @@ import { UserAuthenticationProvider, useUserAuthentication } from './UserAuthent
|
|||||||
import { ImageViewerContext, ImageViewerProvider, useImageViewer } from './ImageViewerProvider';
|
import { ImageViewerContext, ImageViewerProvider, useImageViewer } from './ImageViewerProvider';
|
||||||
import DragAndDropProvider from './DragAndDropProvider';
|
import DragAndDropProvider from './DragAndDropProvider';
|
||||||
import CineProvider, { useCine } from './CineProvider';
|
import CineProvider, { useCine } from './CineProvider';
|
||||||
|
import {
|
||||||
|
ViewportActionCornersProvider,
|
||||||
|
useViewportActionCorners,
|
||||||
|
} from './ViewportActionCornersProvider';
|
||||||
|
|
||||||
export { useNotification, NotificationProvider };
|
export { useNotification, NotificationProvider };
|
||||||
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
|
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
|
||||||
@ -20,3 +24,4 @@ export { UserAuthenticationProvider, useUserAuthentication };
|
|||||||
export { ImageViewerContext, ImageViewerProvider, useImageViewer };
|
export { ImageViewerContext, ImageViewerProvider, useImageViewer };
|
||||||
export { DragAndDropProvider };
|
export { DragAndDropProvider };
|
||||||
export { CineProvider, useCine };
|
export { CineProvider, useCine };
|
||||||
|
export { ViewportActionCornersProvider, useViewportActionCorners };
|
||||||
|
|||||||
18
platform/ui-next/src/types/ActionCorners.ts
Normal file
18
platform/ui-next/src/types/ActionCorners.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { ViewportActionCornersLocations } from '../components/Viewport/ViewportActionCorners';
|
||||||
|
|
||||||
|
export type ActionComponentInfo = {
|
||||||
|
viewportId: string;
|
||||||
|
id: string;
|
||||||
|
component: ReactNode;
|
||||||
|
location: ViewportActionCornersLocations;
|
||||||
|
indexPriority?: number;
|
||||||
|
isLocked?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
|
isVisible?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AlignAndSide = {
|
||||||
|
align: 'start' | 'end' | 'center';
|
||||||
|
side: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
};
|
||||||
@ -5,6 +5,7 @@ import ThumbnailType from './ThumbnailType';
|
|||||||
export * from './Predicate';
|
export * from './Predicate';
|
||||||
export * from './ContextMenuItem';
|
export * from './ContextMenuItem';
|
||||||
export * from './ViewportActionCornersTypes';
|
export * from './ViewportActionCornersTypes';
|
||||||
|
export * from './ActionCorners';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* StringNumber often comes back from DICOMweb for integer valued items.
|
* StringNumber often comes back from DICOMweb for integer valued items.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user