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 OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
|
||||
import ViewportColorbarsContainer from '../components/ViewportColorbar';
|
||||
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
|
||||
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
|
||||
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
|
||||
import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewportService';
|
||||
import { useViewportActionCorners } from '../hooks';
|
||||
|
||||
const STACK = 'stack';
|
||||
|
||||
@ -38,7 +38,6 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
onElementEnabled,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
onElementDisabled,
|
||||
@ -319,16 +318,6 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
};
|
||||
}, [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');
|
||||
|
||||
return (
|
||||
@ -357,6 +346,10 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
viewportId={viewportId}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
<ViewportColorbarsContainer
|
||||
viewportId={viewportId}
|
||||
viewportElementRef={elementRef}
|
||||
/>
|
||||
</div>
|
||||
{/* top offset of 24px to account for ViewportActionCorners. */}
|
||||
<div className="absolute top-[24px] w-full">
|
||||
|
||||
@ -1,31 +1,54 @@
|
||||
import React from 'react';
|
||||
import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import {
|
||||
useViewportActionCorners,
|
||||
ViewportActionCorners,
|
||||
ViewportActionCornersLocations,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
export type OHIFViewportActionCornersProps = {
|
||||
viewportId: string;
|
||||
};
|
||||
|
||||
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
|
||||
const { servicesManager } = useSystem();
|
||||
const [viewportActionCornersState] = useViewportActionCornersContext();
|
||||
const [state] = useViewportActionCorners();
|
||||
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const isActiveViewport = viewportGrid.activeViewportId === viewportId;
|
||||
|
||||
const ViewportActionCorners =
|
||||
servicesManager.services.customizationService.getCustomization('ui.viewportActionCorner');
|
||||
|
||||
if (!viewportActionCornersState[viewportId]) {
|
||||
if (!state.viewports[viewportId]) {
|
||||
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 (
|
||||
<ViewportActionCorners
|
||||
cornerComponents={viewportActionCornersState[viewportId]}
|
||||
isActiveViewport={isActiveViewport}
|
||||
/>
|
||||
<ViewportActionCorners.Container>
|
||||
{renderCorner(ViewportActionCornersLocations.topLeft, ViewportActionCorners.TopLeft)}
|
||||
{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,
|
||||
PopoverTrigger,
|
||||
useViewportGrid,
|
||||
useViewportActionCorners,
|
||||
} from '@ohif/ui-next';
|
||||
import ViewportDataOverlayMenu from './ViewportDataOverlayMenu';
|
||||
import classNames from 'classnames';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { MENU_IDS } from '../menus/menu-ids';
|
||||
|
||||
export function ViewportDataOverlayMenuWrapper({
|
||||
viewportId,
|
||||
@ -19,15 +20,39 @@ export function ViewportDataOverlayMenuWrapper({
|
||||
}: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
location: string;
|
||||
}>): ReactNode {
|
||||
const { servicesManager } = useSystem();
|
||||
const { viewportActionCornersService } = servicesManager.services;
|
||||
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 (
|
||||
<Popover>
|
||||
<Popover
|
||||
open={isMenuOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
className="flex items-center justify-center"
|
||||
|
||||
@ -5,6 +5,7 @@ export function getViewportDataOverlaySettingsMenu(
|
||||
props: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
location: string;
|
||||
}>
|
||||
): ReactNode {
|
||||
return <ViewportDataOverlayMenuWrapper {...props} />;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { Enums } from '@cornerstonejs/core';
|
||||
import {
|
||||
@ -10,14 +10,21 @@ import {
|
||||
DropdownMenuLabel,
|
||||
Button,
|
||||
} from '@ohif/ui-next';
|
||||
import { MENU_IDS } from '../menus/menu-ids';
|
||||
|
||||
function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string }>) {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const [viewportGridState, viewportGridService] = useViewportGrid();
|
||||
const [actionCornerState, viewportActionCornersServiceAPI] = useViewportActionCorners();
|
||||
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
|
||||
|
||||
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 viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const currentViewportType = viewportInfo?.getViewportType();
|
||||
@ -80,8 +87,19 @@ function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string
|
||||
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 displaySets = displaySetUIDs
|
||||
.map(uid => displaySetService.getDisplaySetByUID(uid))
|
||||
@ -98,13 +116,16 @@ function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string
|
||||
let side = 'bottom';
|
||||
|
||||
if (location !== undefined) {
|
||||
const positioning = viewportActionCornersService.getAlignAndSide(location);
|
||||
const positioning = viewportActionCornersServiceAPI.getAlignAndSide(location);
|
||||
align = positioning.align;
|
||||
side = positioning.side;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu
|
||||
open={isMenuOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { Switch } from '@ohif/ui-next';
|
||||
import { StackViewport } from '@cornerstonejs/core';
|
||||
import { ColorbarProps } from '../../types/Colorbar';
|
||||
import { ColorbarProps, ColorbarOptions } from '../../types/Colorbar';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function setViewportColorbar(
|
||||
viewportId,
|
||||
commandsManager,
|
||||
viewportId: string,
|
||||
commandsManager: any,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
colorbarOptions
|
||||
colorbarOptions: Partial<ColorbarOptions>
|
||||
) {
|
||||
const { cornerstoneViewportService, viewportGridService } = servicesManager.services;
|
||||
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import { AllInOneMenu } from '@ohif/ui-next';
|
||||
import { AllInOneMenu, cn } from '@ohif/ui-next';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { Colormap } from './Colormap';
|
||||
import { Colorbar } from './Colorbar';
|
||||
@ -25,6 +24,7 @@ export type WindowLevelActionMenuProps = {
|
||||
displaySets: Array<any>;
|
||||
volumeRenderingPresets: Array<ViewportPreset>;
|
||||
volumeRenderingQualityRange: VolumeRenderingQualityRange;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export function WindowLevelActionMenu({
|
||||
@ -111,20 +111,13 @@ export function WindowLevelActionMenu({
|
||||
]);
|
||||
|
||||
return (
|
||||
<AllInOneMenu.IconMenu
|
||||
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);
|
||||
}}
|
||||
<AllInOneMenu.Menu
|
||||
menuKey={menuKey}
|
||||
key={menuKey}
|
||||
// the visibility is handled by the parent component
|
||||
isVisible={true}
|
||||
horizontalDirection={horizontalDirection}
|
||||
verticalDirection={verticalDirection}
|
||||
>
|
||||
<AllInOneMenu.ItemPanel>
|
||||
{!is3DVolume && (
|
||||
@ -180,6 +173,6 @@ export function WindowLevelActionMenu({
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
</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 { WindowLevelActionMenuProps } from './WindowLevelActionMenu';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { WindowLevelActionMenu as WindowLevelActionMenuComponent } from './WindowLevelActionMenu';
|
||||
import React from 'react';
|
||||
import { WindowLevelActionMenuWrapper } from './WindowLevelActionMenuWrapper';
|
||||
|
||||
function WindowLevelActionMenu({
|
||||
viewportId,
|
||||
element,
|
||||
displaySets,
|
||||
verticalDirection,
|
||||
horizontalDirection,
|
||||
}: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
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} />;
|
||||
export function getWindowLevelActionMenu(
|
||||
props: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
location: string;
|
||||
displaySets: Array<any>;
|
||||
}>
|
||||
) {
|
||||
return <WindowLevelActionMenuWrapper {...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 './MeasurementTableNested';
|
||||
export * from './StudyMeasurements';
|
||||
@ -5,6 +6,8 @@ export * from './MeasurementsMenu';
|
||||
export * from './SeriesMeasurements';
|
||||
export * from './StudyMeasurementsActions';
|
||||
export * from './MeasurementsOrAdditionalFindings';
|
||||
import DicomUpload from './DicomUpload/DicomUpload';
|
||||
export * from './ViewportDataOverlaySettingMenu';
|
||||
export * from './ViewportOrientationMenu';
|
||||
export * from './WindowLevelActionMenu';
|
||||
|
||||
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 {
|
||||
ColorbarPositionType,
|
||||
TickPositionType,
|
||||
PositionStylesMapType,
|
||||
PositionTickStylesMapType,
|
||||
ContainerStyleType,
|
||||
TickStyleType,
|
||||
ColorbarProperties,
|
||||
} from '../types/Colorbar';
|
||||
import { ColorMapPreset } from '../types/Colormap';
|
||||
|
||||
const defaultPosition: ColorbarPositionType = 'right';
|
||||
const DefaultColormap = 'Grayscale';
|
||||
|
||||
export default {
|
||||
'cornerstone.colorbar': {
|
||||
width: '16px',
|
||||
colorbarTickPosition: 'left',
|
||||
colormaps,
|
||||
colorbarContainerPosition: 'right',
|
||||
colorbarInitialColormap: DefaultColormap,
|
||||
const positionStyles: PositionStylesMapType = {
|
||||
left: { left: '5%', width: '15px' },
|
||||
right: { right: '5%', width: '15px' },
|
||||
bottom: { bottom: '1%', height: '18px' },
|
||||
};
|
||||
|
||||
// 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 { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
|
||||
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
|
||||
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({
|
||||
viewportId,
|
||||
element,
|
||||
displaySets,
|
||||
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
|
||||
horizontalDirection: AllInOneMenu.HorizontalDirection.LeftToRight,
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
'ui.viewportActionCorner': ViewportActionCorners,
|
||||
'viewportActionMenu.topLeft': [
|
||||
{
|
||||
id: 'orientationMenu',
|
||||
enabled: true,
|
||||
id: MENU_IDS.ORIENTATION_MENU,
|
||||
component: createOrientationMenu,
|
||||
},
|
||||
{
|
||||
id: 'dataOverlay',
|
||||
enabled: true,
|
||||
id: MENU_IDS.DATA_OVERLAY_MENU,
|
||||
component: createDataOverlay,
|
||||
},
|
||||
{
|
||||
id: 'windowLevelActionMenu',
|
||||
enabled: true,
|
||||
id: MENU_IDS.WINDOW_LEVEL_MENU,
|
||||
component: createWindowLevelMenu,
|
||||
},
|
||||
],
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
export { default as useViewportActionCorners } from './useViewportActionCorners';
|
||||
export * from './useViewportActionCorners';
|
||||
export * from './useActiveViewportSegmentationRepresentations';
|
||||
export * from './useMeasurements';
|
||||
export * from './useSegmentations';
|
||||
export * from './useSegmentations';
|
||||
|
||||
@ -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 ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
||||
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
|
||||
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
import { findNearbyToolData } from './utils/findNearbyToolData';
|
||||
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
|
||||
@ -153,7 +152,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
preRegistration: async function (props: Types.Extensions.ExtensionParams): Promise<void> {
|
||||
const { servicesManager, serviceProvidersManager } = props;
|
||||
const { servicesManager } = props;
|
||||
servicesManager.registerService(CornerstoneViewportService.REGISTRATION);
|
||||
servicesManager.registerService(ToolGroupService.REGISTRATION);
|
||||
servicesManager.registerService(SyncGroupService.REGISTRATION);
|
||||
@ -162,11 +161,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
servicesManager.registerService(ViewportActionCornersService.REGISTRATION);
|
||||
servicesManager.registerService(ColorbarService.REGISTRATION);
|
||||
|
||||
serviceProvidersManager.registerProvider(
|
||||
ViewportActionCornersService.REGISTRATION.name,
|
||||
ViewportActionCornersProvider
|
||||
);
|
||||
|
||||
const { syncGroupService } = servicesManager.services;
|
||||
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 { StackViewport, VolumeViewport, getRenderingEngine } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
import { getRenderingEngine } from '@cornerstonejs/core';
|
||||
import { ColorbarOptions, ChangeTypes } from '../../types/Colorbar';
|
||||
const { ViewportColorbar } = utilities.voi.colorbar;
|
||||
|
||||
export default class ColorbarService extends PubSubService {
|
||||
static EVENTS = {
|
||||
STATE_CHANGED: 'event::ColorbarService:stateChanged',
|
||||
};
|
||||
|
||||
static defaultStyles = {
|
||||
position: 'absolute',
|
||||
boxSizing: 'border-box',
|
||||
border: 'solid 1px #555',
|
||||
cursor: 'initial',
|
||||
};
|
||||
|
||||
static positionStyles = {
|
||||
left: { left: '5%' },
|
||||
right: { right: '5%' },
|
||||
top: { top: '5%' },
|
||||
bottom: { bottom: '5%' },
|
||||
};
|
||||
|
||||
static defaultTickStyles = {
|
||||
position: 'left',
|
||||
style: {
|
||||
font: '12px Arial',
|
||||
color: '#fff',
|
||||
maxNumTicks: 8,
|
||||
tickSize: 5,
|
||||
tickWidth: 1,
|
||||
labelMargin: 3,
|
||||
},
|
||||
};
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'colorbarService',
|
||||
create: ({ servicesManager }: OhifTypes.Extensions.ExtensionParams) => {
|
||||
return new ColorbarService(servicesManager);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Structure of colorbars state:
|
||||
* {
|
||||
* [viewportId]: [
|
||||
* {
|
||||
* displaySetInstanceUID: string,
|
||||
* colorbar: {
|
||||
* activeColormapName: string,
|
||||
* colormaps: array,
|
||||
* volumeId: string (optional),
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
colorbars = {};
|
||||
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.
|
||||
* @param viewport - The viewport instance to search volumes in
|
||||
* @param searchId - The identifier to search for within volume IDs
|
||||
* @returns The matching volume ID if found, null otherwise
|
||||
* Gets the appropriate data ID for a viewport and display set
|
||||
* @param viewport - The viewport instance
|
||||
* @param displaySetInstanceUID - The display set instance UID to identify data
|
||||
* @returns The appropriate data ID for the viewport type (volumeId for volume viewports, undefined for stack)
|
||||
*/
|
||||
private getVolumeIdForIdentifier(viewport, searchId: string): string | null {
|
||||
const volumeIds = viewport.getAllVolumeIds?.() || [];
|
||||
return volumeIds.length > 0 ? volumeIds.find(id => id.includes(searchId)) || null : null;
|
||||
private getDataIdForViewport(viewport, displaySetInstanceUID: string): string | undefined {
|
||||
// For volume viewports, find the matching volumeId
|
||||
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`.
|
||||
* 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 displaySetInstanceUIDs An array of display set instance UIDs to associate with the colorbar.
|
||||
* @param options Configuration options for the colorbar, including position, colormaps, active colormap name, ticks, and width.
|
||||
*/
|
||||
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
|
||||
public addColorbar(
|
||||
viewportId: string,
|
||||
displaySetInstanceUIDs: string[],
|
||||
options = {} as ColorbarOptions
|
||||
) {
|
||||
const { displaySetService } = this.servicesManager.services;
|
||||
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
|
||||
const viewport = renderingEngine.getViewport(viewportId);
|
||||
@ -78,35 +78,24 @@ export default class ColorbarService extends PubSubService {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = viewport;
|
||||
const actorEntries = viewport.getActors();
|
||||
|
||||
if (!actorEntries || actorEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { position, width: thickness, activeColormapName, colormaps } = options;
|
||||
const { activeColormapName, colormaps } = options;
|
||||
|
||||
const numContainers = displaySetInstanceUIDs.length;
|
||||
|
||||
const containers = this.createContainers(
|
||||
numContainers,
|
||||
element,
|
||||
position,
|
||||
thickness,
|
||||
viewportId
|
||||
);
|
||||
|
||||
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
|
||||
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
|
||||
// don't show colorbar for overlay display sets (e.g. segmentation)
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (displaySet.isOverlayDisplaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID);
|
||||
const properties = viewport?.getProperties(volumeId);
|
||||
const dataId = this.getDataIdForViewport(viewport, displaySetInstanceUID);
|
||||
const properties = dataId ? viewport.getProperties(dataId) : viewport.getProperties();
|
||||
const colormap = properties?.colormap;
|
||||
|
||||
if (activeColormapName && !colormap) {
|
||||
this.setViewportColormap(
|
||||
viewportId,
|
||||
@ -116,38 +105,43 @@ export default class ColorbarService extends PubSubService {
|
||||
);
|
||||
}
|
||||
|
||||
const colorbarContainer = containers[index];
|
||||
|
||||
const colorbar = new ViewportColorbar({
|
||||
id: `ctColorbar-${viewportId}-${index}`,
|
||||
element,
|
||||
colormaps: options.colormaps || {},
|
||||
// if there's an existing colormap set, we use it, otherwise we use the activeColormapName, otherwise, grayscale
|
||||
// Prepare colorbar data for the React component
|
||||
const colorbarData = {
|
||||
activeColormapName: colormap?.name || options?.activeColormapName || 'Grayscale',
|
||||
container: colorbarContainer,
|
||||
ticks: {
|
||||
...ColorbarService.defaultTickStyles,
|
||||
...options.ticks,
|
||||
},
|
||||
volumeId: viewport instanceof VolumeViewport ? volumeId : undefined,
|
||||
});
|
||||
colormaps: options.colormaps ? Object.values(options.colormaps) : [],
|
||||
volumeId: dataId,
|
||||
dataId,
|
||||
};
|
||||
|
||||
// Store the colorbar data in the service state
|
||||
if (this.colorbars[viewportId]) {
|
||||
this.colorbars[viewportId].push({
|
||||
colorbar,
|
||||
container: colorbarContainer,
|
||||
displaySetInstanceUID,
|
||||
});
|
||||
// Check if there's already an entry for this displaySetInstanceUID
|
||||
const existingIndex = this.colorbars[viewportId].findIndex(
|
||||
item => item.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 {
|
||||
// Create new colorbar array for this viewport
|
||||
this.colorbars[viewportId] = [
|
||||
{
|
||||
colorbar,
|
||||
container: colorbarContainer,
|
||||
colorbar: colorbarData,
|
||||
displaySetInstanceUID,
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
// Notify listeners about the state change
|
||||
this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, {
|
||||
viewportId,
|
||||
changeType: ChangeTypes.Added,
|
||||
@ -175,10 +169,6 @@ export default class ColorbarService extends PubSubService {
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
// Remove only the specific colorbar container
|
||||
const { container } = colorbarInfo[index];
|
||||
container.parentNode.removeChild(container);
|
||||
|
||||
// Remove the colorbar from the array
|
||||
colorbarInfo.splice(index, 1);
|
||||
|
||||
@ -188,11 +178,6 @@ export default class ColorbarService extends PubSubService {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remove all colorbars for the viewport (original behavior)
|
||||
colorbarInfo.forEach(({ container }) => {
|
||||
container.parentNode.removeChild(container);
|
||||
});
|
||||
|
||||
delete this.colorbars[viewportId];
|
||||
}
|
||||
|
||||
@ -259,71 +244,15 @@ export default class ColorbarService extends PubSubService {
|
||||
if (!viewport || !actorEntries || actorEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
const setViewportProperties = (viewport, uid) => {
|
||||
const volumeId = this.getVolumeIdForIdentifier(viewport, uid);
|
||||
viewport.setProperties({ colormap }, volumeId);
|
||||
};
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
setViewportProperties(viewport, viewportId);
|
||||
}
|
||||
// Get the appropriate dataId for this viewport/displaySet combination
|
||||
const dataId = this.getDataIdForViewport(viewport, displaySetInstanceUID);
|
||||
|
||||
if (viewport instanceof VolumeViewport) {
|
||||
setViewportProperties(viewport, displaySetInstanceUID);
|
||||
}
|
||||
// Set properties with or without dataId based on what the viewport supports
|
||||
viewport.setProperties({ colormap }, dataId);
|
||||
|
||||
if (immediate) {
|
||||
viewport.render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the container elements for colorbars based on the specified parameters. This function dynamically
|
||||
* generates and styles DOM elements to host the colorbars, positioning them according to the specified options.
|
||||
*
|
||||
* @param numContainers The number of containers to create, typically corresponding to the number of colorbars.
|
||||
* @param element The DOM element within which the colorbar containers will be placed.
|
||||
* @param position The position of the colorbar containers (e.g., 'top', 'bottom', 'left', 'right').
|
||||
* @param thickness The thickness of the colorbar containers, affecting their width or height depending on their position.
|
||||
* @param viewportId The identifier of the viewport for which the containers are being created.
|
||||
* @returns An array of the created container DOM elements.
|
||||
*/
|
||||
private createContainers(numContainers, element, position, thickness, viewportId) {
|
||||
const containers = [];
|
||||
const dimensions = {
|
||||
1: 50,
|
||||
2: 33,
|
||||
};
|
||||
const dimension = dimensions[numContainers] || 50 / numContainers;
|
||||
|
||||
Array.from({ length: numContainers }).forEach((_, i) => {
|
||||
const colorbarContainer = document.createElement('div');
|
||||
colorbarContainer.id = `ctColorbarContainer-${viewportId}-${i + 1}`;
|
||||
|
||||
Object.assign(colorbarContainer.style, ColorbarService.defaultStyles);
|
||||
|
||||
if (['top', 'bottom'].includes(position)) {
|
||||
Object.assign(colorbarContainer.style, {
|
||||
width: `${dimension}%`,
|
||||
height: thickness || '2.5%',
|
||||
left: `${(i + 1) * dimension}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
...ColorbarService.positionStyles[position],
|
||||
});
|
||||
} else if (['left', 'right'].includes(position)) {
|
||||
Object.assign(colorbarContainer.style, {
|
||||
height: `${dimension}%`,
|
||||
width: thickness || '2.5%',
|
||||
top: `${(i + 1) * dimension}%`,
|
||||
transform: 'translateY(-50%)',
|
||||
...ColorbarService.positionStyles[position],
|
||||
});
|
||||
}
|
||||
|
||||
element.appendChild(colorbarContainer);
|
||||
containers.push(colorbarContainer);
|
||||
});
|
||||
|
||||
return containers;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,5 @@
|
||||
import { PubSubService } from '@ohif/core';
|
||||
import { ViewportActionCornersLocations } 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';
|
||||
};
|
||||
import { ViewportActionCornersLocations, Types } from '@ohif/ui-next';
|
||||
|
||||
class ViewportActionCornersService extends PubSubService {
|
||||
public static readonly EVENTS = {};
|
||||
@ -40,39 +26,121 @@ class ViewportActionCornersService extends PubSubService {
|
||||
getState: getStateImplementation,
|
||||
addComponent: addComponentImplementation,
|
||||
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 {
|
||||
if (getStateImplementation) {
|
||||
this.serviceImplementation._getState = getStateImplementation;
|
||||
}
|
||||
if (addComponentImplementation) {
|
||||
this.serviceImplementation._addComponent = addComponentImplementation;
|
||||
}
|
||||
if (addComponentsImplementation) {
|
||||
this.serviceImplementation._addComponents = addComponentsImplementation;
|
||||
}
|
||||
if (clearComponentsImplementation) {
|
||||
this.serviceImplementation._clear = clearComponentsImplementation;
|
||||
}
|
||||
this.serviceImplementation._getState =
|
||||
getStateImplementation ?? this.serviceImplementation._getState;
|
||||
this.serviceImplementation._addComponent =
|
||||
addComponentImplementation ?? this.serviceImplementation._addComponent;
|
||||
this.serviceImplementation._addComponents =
|
||||
addComponentsImplementation ?? this.serviceImplementation._addComponents;
|
||||
this.serviceImplementation._clear = clearImplementation ?? this.serviceImplementation._clear;
|
||||
this.serviceImplementation._lockItem =
|
||||
lockItemImplementation ?? this.serviceImplementation._lockItem;
|
||||
this.serviceImplementation._unlockItem =
|
||||
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() {
|
||||
return this.serviceImplementation._getState();
|
||||
}
|
||||
|
||||
public addComponent(component: ActionComponentInfo) {
|
||||
public addComponent(component: Types.ActionComponentInfo) {
|
||||
this.serviceImplementation._addComponent(component);
|
||||
}
|
||||
|
||||
public addComponents(components: Array<ActionComponentInfo>) {
|
||||
public addComponents(components: Array<Types.ActionComponentInfo>) {
|
||||
this.serviceImplementation._addComponents(components);
|
||||
}
|
||||
|
||||
public clear(viewportId: string) {
|
||||
public clear(viewportId: string): void {
|
||||
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) {
|
||||
case ViewportActionCornersLocations.topLeft:
|
||||
return { align: 'start', side: 'bottom' };
|
||||
|
||||
@ -1,27 +1,112 @@
|
||||
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 = {
|
||||
position: string;
|
||||
colormaps: Array<ColorMapPreset>;
|
||||
position: ColorbarPositionType;
|
||||
colormaps: Record<string, ColorMapPreset>;
|
||||
activeColormapName: string;
|
||||
ticks: object;
|
||||
ticks?: TickConfigType;
|
||||
width: string;
|
||||
};
|
||||
|
||||
// Props for the Colorbar component
|
||||
export type ColorbarProps = {
|
||||
viewportId: string;
|
||||
displaySets: Array<any>;
|
||||
colorbarProperties: ColorbarProperties;
|
||||
};
|
||||
|
||||
// Extended properties with styling options
|
||||
export type ColorbarProperties = {
|
||||
width: string;
|
||||
colorbarTickPosition: string;
|
||||
colorbarContainerPosition: string;
|
||||
colormaps: Array<ColorMapPreset>;
|
||||
colorbarTickPosition: TickPositionType;
|
||||
colorbarContainerPosition: ColorbarPositionType;
|
||||
colormaps: Record<string, ColorMapPreset>;
|
||||
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 {
|
||||
Removed = 'removed',
|
||||
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 loadingIndicatorTotalPercentCustomization from './customizations/loadingIndicatorTotalPercentCustomization';
|
||||
import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization';
|
||||
import viewportActionCornersCustomization from './customizations/viewportActionCornersCustomization';
|
||||
import labellingFlowCustomization from './customizations/labellingFlowCustomization';
|
||||
import viewportNotificationCustomization from './customizations/notificationCustomization';
|
||||
import aboutModalCustomization from './customizations/aboutModalCustomization';
|
||||
@ -62,7 +61,6 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
||||
...loadingIndicatorProgressCustomization,
|
||||
...loadingIndicatorTotalPercentCustomization,
|
||||
...progressLoadingBarCustomization,
|
||||
...viewportActionCornersCustomization,
|
||||
...labellingFlowCustomization,
|
||||
...contextMenuUICustomization,
|
||||
...viewportNotificationCustomization,
|
||||
|
||||
@ -26,6 +26,7 @@ import {
|
||||
ModalProvider,
|
||||
ViewportDialogProvider,
|
||||
UserAuthenticationProvider,
|
||||
ViewportActionCornersProvider,
|
||||
} from '@ohif/ui-next';
|
||||
// Viewer Project
|
||||
// TODO: Should this influence study list?
|
||||
@ -104,6 +105,7 @@ function App({
|
||||
userAuthenticationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const providers = [
|
||||
@ -119,6 +121,7 @@ function App({
|
||||
[TooltipProvider],
|
||||
[DialogProvider, { service: uiDialogService, dialog: ManagedDialog }],
|
||||
[ModalProvider, { service: uiModalService, modal: ModalNext }],
|
||||
[ViewportActionCornersProvider, { service: viewportActionCornersService }],
|
||||
[ShepherdJourneyProvider],
|
||||
];
|
||||
|
||||
|
||||
@ -4,11 +4,13 @@ import { ViewportGrid, ViewportPane } from '@ohif/ui-next';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import EmptyViewport from './EmptyViewport';
|
||||
import { useAppConfig } from '@state';
|
||||
import { useViewportActionCornersWithGrid } from '../hooks';
|
||||
|
||||
function ViewerViewportGrid(props: withAppTypes) {
|
||||
const { servicesManager, viewportComponents = [], dataSource, commandsManager } = props;
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const [appConfig] = useAppConfig();
|
||||
const { initializeViewportCorners } = useViewportActionCornersWithGrid();
|
||||
|
||||
const { layout, activeViewportId, viewports, isHangingProtocolLayout } = viewportGrid;
|
||||
const { numCols, numRows } = layout;
|
||||
@ -415,8 +417,14 @@ function ViewerViewportGrid(props: withAppTypes) {
|
||||
displaySetOptions={displaySetOptions}
|
||||
needsRerendering={displaySetsNeedsRerendering}
|
||||
isHangingProtocolLayout={isHangingProtocolLayout}
|
||||
onElementEnabled={() => {
|
||||
onElementEnabled={evt => {
|
||||
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>
|
||||
@ -425,7 +433,7 @@ function ViewerViewportGrid(props: withAppTypes) {
|
||||
}
|
||||
|
||||
return viewportPanes;
|
||||
}, [viewports, activeViewportId, viewportComponents, dataSource]);
|
||||
}, [viewports, activeViewportId, viewportComponents, dataSource, initializeViewportCorners]);
|
||||
|
||||
/**
|
||||
* 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 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 { utils } from '@ohif/core';
|
||||
import { ImageViewerProvider, DragAndDropProvider } from '@ohif/ui-next';
|
||||
import { useSearchParams } from '@hooks';
|
||||
import { useSearchParams } from '../../hooks';
|
||||
import { useAppConfig } from '@state';
|
||||
import ViewportGrid from '@components/ViewportGrid';
|
||||
import Compose from './Compose';
|
||||
|
||||
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
//
|
||||
import filtersMeta from './filtersMeta.js';
|
||||
import { useAppConfig } from '@state';
|
||||
import { useDebounce, useSearchParams } from '@hooks';
|
||||
import { useDebounce, useSearchParams } from '../../hooks';
|
||||
import { utils, Types as coreTypes } from '@ohif/core';
|
||||
|
||||
import {
|
||||
|
||||
@ -16,6 +16,8 @@ export type DisplaySet = {
|
||||
label?: string;
|
||||
/** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */
|
||||
isOverlayDisplaySet?: boolean;
|
||||
/** flag indicating if it supports window level */
|
||||
supportsWindowLevel?: boolean;
|
||||
|
||||
// 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',
|
||||
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.',
|
||||
image: segmentationOverlay,
|
||||
default: null,
|
||||
@ -946,7 +971,7 @@ window.config = {
|
||||
// rest of window config
|
||||
customizationService: [
|
||||
{
|
||||
'viewportActionMenu.dataOverlay': {
|
||||
'viewportActionMenu.dataOverlayMenu': {
|
||||
$merge: {
|
||||
enabled: true,
|
||||
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 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 {
|
||||
topLeft,
|
||||
topRight,
|
||||
@ -33,35 +35,102 @@ const locationClasses = {
|
||||
),
|
||||
};
|
||||
|
||||
function ViewportActionCorners({ cornerComponents }) {
|
||||
if (!cornerComponents) {
|
||||
return null;
|
||||
}
|
||||
const ViewportActionCornersContext = createContext<{
|
||||
registerCorner: (location: ViewportActionCornersLocations, children: ReactNode) => void;
|
||||
} | 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 (
|
||||
<div
|
||||
className="pointer-events-none absolute h-full w-full select-none"
|
||||
onDoubleClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{Object.entries(cornerComponents).map(([location, locationArray]) => (
|
||||
<div
|
||||
key={location}
|
||||
className={locationClasses[location]}
|
||||
>
|
||||
{locationArray.map(componentInfo => (
|
||||
<div key={componentInfo.id}>{componentInfo.component}</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ViewportActionCornersContext.Provider value={{ registerCorner }}>
|
||||
{children}
|
||||
<div
|
||||
className="pointer-events-none absolute h-full w-full select-none"
|
||||
onDoubleClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{Object.entries(corners).map(([location, children]) => {
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={location}
|
||||
className={locationClasses[location]}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ViewportActionCornersContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportActionCorners.propTypes = {
|
||||
cornerComponents: PropTypes.object.isRequired,
|
||||
};
|
||||
function Corner({
|
||||
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 DragAndDropProvider from './DragAndDropProvider';
|
||||
import CineProvider, { useCine } from './CineProvider';
|
||||
import {
|
||||
ViewportActionCornersProvider,
|
||||
useViewportActionCorners,
|
||||
} from './ViewportActionCornersProvider';
|
||||
|
||||
export { useNotification, NotificationProvider };
|
||||
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
|
||||
@ -20,3 +24,4 @@ export { UserAuthenticationProvider, useUserAuthentication };
|
||||
export { ImageViewerContext, ImageViewerProvider, useImageViewer };
|
||||
export { DragAndDropProvider };
|
||||
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 './ContextMenuItem';
|
||||
export * from './ViewportActionCornersTypes';
|
||||
export * from './ActionCorners';
|
||||
|
||||
/**
|
||||
* StringNumber often comes back from DICOMweb for integer valued items.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user