feat(data overlay): add foreground background control plus orientation change in the UI and add RTDOSE support (#5007)

This commit is contained in:
Alireza authored and GitHub committed 2025-05-05 16:35:05 -04:00
1 parent c1fced0573
commit cd4e6408bc
105 files changed
+3117 -788

No files matched your search

@@ -0,0 +1,478 @@
import React, { useState } from 'react';
import {
Button,
Icons,
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Switch,
} from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: string }>) {
const { commandsManager, servicesManager } = useSystem();
const [pendingForegrounds, setPendingForegrounds] = useState<string[]>([]);
const [pendingSegmentations, setPendingSegmentations] = useState<string[]>([]);
const [thresholdOpacityEnabled, setThresholdOpacityEnabled] = useState(false);
const { hangingProtocolService } = servicesManager.services;
const {
backgroundDisplaySet,
potentialOverlayDisplaySets,
potentialForegroundDisplaySets,
potentialBackgroundDisplaySets,
overlayDisplaySets,
foregroundDisplaySets,
} = useViewportDisplaySets(viewportId);
/**
* Change the background display set
*/
const handleBackgroundSelection = (newBackgroundDisplaySet: AppTypes.DisplaySet) => {
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportId,
newBackgroundDisplaySet.displaySetInstanceUID
);
commandsManager.run('setDisplaySetsForViewports', {
viewportsToUpdate: updatedViewports,
});
};
/**
* Handle threshold and opacity toggle
*/
const handleThresholdOpacityToggle = (checked: boolean) => {
setThresholdOpacityEnabled(checked);
if (foregroundDisplaySets.length > 0) {
// Example implementation of threshold/opacity adjustment
commandsManager.runCommand('setForegroundThresholdOpacity', {
viewportId,
enabled: checked,
});
}
};
/**
* Replace a display set layer with a new one
*/
const handleReplaceDisplaySetLayer = (
currentDisplaySetInstanceUID: string,
newDisplaySetInstanceUID: string
) => {
// Remove current display set
commandsManager.runCommand('removeDisplaySetLayer', {
viewportId,
displaySetInstanceUID: currentDisplaySetInstanceUID,
});
setTimeout(() => {
commandsManager.runCommand('addDisplaySetAsLayer', {
viewportId,
displaySetInstanceUID: newDisplaySetInstanceUID,
});
}, 0);
};
/**
* Remove a display set layer
*/
const handleRemoveDisplaySetLayer = (displaySetInstanceUID: string) => {
commandsManager.runCommand('removeDisplaySetLayer', {
viewportId,
displaySetInstanceUID,
});
};
/**
* Add a display set as a layer
*/
const handleAddDisplaySetAsLayer = (displaySetInstanceUID: string) => {
commandsManager.runCommand('addDisplaySetAsLayer', {
viewportId,
displaySetInstanceUID,
});
};
/**
* Handle overlay display set selection change
*/
const handleOverlaySelectionChange = (
currentDisplaySet: AppTypes.DisplaySet,
newDisplaySetInstanceUID: string
) => {
if (newDisplaySetInstanceUID === currentDisplaySet.displaySetInstanceUID) {
return;
}
// Find the selected display set
const selectedDisplaySet = potentialOverlayDisplaySets.find(
ds => ds.displaySetInstanceUID === newDisplaySetInstanceUID
);
if (selectedDisplaySet) {
handleReplaceDisplaySetLayer(
currentDisplaySet.displaySetInstanceUID,
selectedDisplaySet.displaySetInstanceUID
);
}
};
/**
* Handle foreground display set selection change
*/
const handleForegroundSelectionChange = (
currentDisplaySet: AppTypes.DisplaySet,
newDisplaySetInstanceUID: string
) => {
if (newDisplaySetInstanceUID === currentDisplaySet.displaySetInstanceUID) {
return;
}
// Find the selected display set
const selectedDisplaySet = potentialForegroundDisplaySets.find(
ds => ds.displaySetInstanceUID === newDisplaySetInstanceUID
);
if (selectedDisplaySet) {
handleReplaceDisplaySetLayer(
currentDisplaySet.displaySetInstanceUID,
selectedDisplaySet.displaySetInstanceUID
);
}
};
/**
* Handle pending segmentation selection
*/
const handlePendingSegmentationSelection = (pendingId: string, displaySetInstanceUID: string) => {
const selectedDisplaySet = potentialOverlayDisplaySets.find(
ds => ds.displaySetInstanceUID === displaySetInstanceUID
);
if (selectedDisplaySet) {
handleAddDisplaySetAsLayer(selectedDisplaySet.displaySetInstanceUID);
// Remove this pending segmentation from the list
setPendingSegmentations(pendingSegmentations.filter(id => id !== pendingId));
}
};
/**
* Handle pending foreground selection
*/
const handlePendingForegroundSelection = (pendingId: string, displaySetInstanceUID: string) => {
const selectedDisplaySet = potentialForegroundDisplaySets.find(
ds => ds.displaySetInstanceUID === displaySetInstanceUID
);
if (selectedDisplaySet) {
handleAddDisplaySetAsLayer(selectedDisplaySet.displaySetInstanceUID);
// Remove this pending foreground from the list
setPendingForegrounds(pendingForegrounds.filter(id => id !== pendingId));
}
};
return (
<div className="bg-popover flex h-full w-[275px] flex-col rounded rounded-md p-1.5">
{/* Top buttons row */}
<div className={`flex`}>
<Button
variant="ghost"
className="text-primary flex items-center p-1"
onClick={() => {
// Add a new pending foreground slot with a unique ID
setPendingForegrounds([...pendingForegrounds, `pending-${Date.now()}`]);
}}
disabled={potentialForegroundDisplaySets.length === 0}
>
<Icons.Plus className="h-4 w-4" />
Foreground
</Button>
<Button
variant="ghost"
className="text-primary ml-2 flex items-center"
disabled={potentialOverlayDisplaySets.length === 0}
onClick={() => {
setPendingSegmentations([...pendingSegmentations, `seg-${Date.now()}`]);
}}
>
<Icons.Plus className="h-4 w-4" />
Segmentation
</Button>
</div>
<div className="">
{/* Overlays Segmentation section */}
<div className="my-2 ml-1">
{overlayDisplaySets.map((displaySet, index) => (
<div
key={displaySet.displaySetInstanceUID}
className="mb-1 flex items-center"
>
<Icons.LayerSegmentation className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value={displaySet.displaySetInstanceUID}
onValueChange={value => handleOverlaySelectionChange(displaySet, value)}
>
<SelectTrigger className="flex-1">
<SelectValue>{displaySet.label?.toUpperCase()}</SelectValue>
</SelectTrigger>
<SelectContent>
{/* Include both potential overlays and the current overlay */}
<SelectItem
key={displaySet.displaySetInstanceUID}
value={displaySet.displaySetInstanceUID}
>
{displaySet.label}
</SelectItem>
{potentialOverlayDisplaySets.map(item => (
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-2 flex-shrink-0"
>
<Icons.More className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => handleRemoveDisplaySetLayer(displaySet.displaySetInstanceUID)}
>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
{pendingSegmentations.map(pendingId => (
<div
key={pendingId}
className="mb-2 flex items-center"
>
<Icons.LayerSegmentation className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value=""
onValueChange={value => handlePendingSegmentationSelection(pendingId, value)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="SELECT A SEGMENTATION" />
</SelectTrigger>
<SelectContent>
{potentialOverlayDisplaySets.map(item => (
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-2 flex-shrink-0"
>
<Icons.More className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => {
// Remove this pending segmentation
setPendingSegmentations(pendingSegmentations.filter(id => id !== pendingId));
}}
>
Cancel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</div>
{/* Foregrounds section */}
<div className="my-2 px-1">
{foregroundDisplaySets.map((displaySet, index) => (
<div
key={displaySet.displaySetInstanceUID}
className="mb-1 flex items-center"
>
<Icons.LayerForeground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value={displaySet.displaySetInstanceUID}
onValueChange={value => handleForegroundSelectionChange(displaySet, value)}
>
<SelectTrigger className="flex-1">
<SelectValue>{displaySet.label?.toUpperCase()}</SelectValue>
</SelectTrigger>
<SelectContent>
{/* Include both potential foregrounds and the current foreground */}
<SelectItem
key={displaySet.displaySetInstanceUID}
value={displaySet.displaySetInstanceUID}
>
{displaySet.label}
</SelectItem>
{potentialForegroundDisplaySets.map(item => (
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-2 flex-shrink-0"
>
<Icons.More className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => handleRemoveDisplaySetLayer(displaySet.displaySetInstanceUID)}
>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
{pendingForegrounds.map(pendingId => (
<div
key={pendingId}
className="mb-2 flex items-center"
>
<Icons.LayerForeground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value=""
onValueChange={value => handlePendingForegroundSelection(pendingId, value)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="SELECT A FOREGROUND" />
</SelectTrigger>
<SelectContent>
{potentialForegroundDisplaySets.map(item => (
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-2 flex-shrink-0"
>
<Icons.More className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => {
// Remove this pending foreground
setPendingForegrounds(pendingForegrounds.filter(id => id !== pendingId));
}}
>
Cancel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</div>
{/* Background section */}
<div className="mt-1 mb-1 flex items-center px-1">
<Icons.LayerBackground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value={backgroundDisplaySet.displaySetInstanceUID}
onValueChange={value => {
const selectedDisplaySet = potentialBackgroundDisplaySets.find(
ds => ds.displaySetInstanceUID === value
);
if (selectedDisplaySet) {
handleBackgroundSelection(selectedDisplaySet);
}
}}
>
<SelectTrigger className="flex-1">
<SelectValue>
{(
backgroundDisplaySet.SeriesDescription ||
backgroundDisplaySet.label ||
'background'
).toUpperCase()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{potentialBackgroundDisplaySets.map(displaySet => (
<SelectItem
key={displaySet.displaySetInstanceUID}
value={displaySet.displaySetInstanceUID}
>
{displaySet.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* {foregroundDisplaySets.length > 0 && (
<div className="mt-1 ml-7">
<div className="flex items-center">
<Switch
id="threshold-opacity-switch"
className="mr-2"
checked={thresholdOpacityEnabled}
onCheckedChange={handleThresholdOpacityToggle}
/>
<label
htmlFor="threshold-opacity-switch"
className="text-muted-foreground cursor-pointer text-sm"
onClick={() => setThresholdOpacityEnabled(!thresholdOpacityEnabled)}
>
Control threshold & opacity
</label>
</div>
</div>
)} */}
</div>
);
}
export default ViewportDataOverlayMenu;
@@ -0,0 +1,62 @@
import React, { ReactNode } from 'react';
import {
Button,
cn,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useViewportGrid,
} from '@ohif/ui-next';
import ViewportDataOverlayMenu from './ViewportDataOverlayMenu';
import classNames from 'classnames';
import { useSystem } from '@ohif/core';
export function ViewportDataOverlayMenuWrapper({
viewportId,
displaySets,
location,
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
}>): ReactNode {
const { servicesManager } = useSystem();
const { viewportActionCornersService } = servicesManager.services;
const [viewportGrid] = useViewportGrid();
const { align, side } = viewportActionCornersService.getAlignAndSide(location);
return (
<Popover>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<Button
variant="ghost"
size="icon"
className={cn(
viewportGrid.activeViewportId === viewportId
? 'visible'
: 'invisible group-hover/pane:visible'
)}
>
<Icons.ViewportViews className={classNames('text-highlight')} />
</Button>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<ViewportDataOverlayMenu
className="w-full"
viewportId={viewportId}
displaySets={displaySets}
/>
</PopoverContent>
</Popover>
);
}
@@ -1,136 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Button, Icons, Separator } from '@ohif/ui-next';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
function ViewportSegmentationMenu({
viewportId,
servicesManager,
}: withAppTypes<{ viewportId: string }>) {
const { segmentationService } = servicesManager.services;
const [activeSegmentations, setActiveSegmentations] = useState([]);
const [availableSegmentations, setAvailableSegmentations] = useState([]);
useEffect(() => {
const updateSegmentations = () => {
const active = segmentationService.getSegmentationRepresentations(viewportId);
setActiveSegmentations(active);
const all = segmentationService.getSegmentations();
const available = all.filter(
seg => !active.some(activeSeg => activeSeg.segmentationId === seg.segmentationId)
);
setAvailableSegmentations(available);
};
updateSegmentations();
const subscriptions = [
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
].map(event => segmentationService.subscribe(event, updateSegmentations));
return () => {
subscriptions.forEach(subscription => subscription.unsubscribe());
};
}, [segmentationService, viewportId]);
const toggleSegmentationRepresentationVisibility = (
segmentationId,
type = SegmentationRepresentations.Labelmap
) => {
segmentationService.toggleSegmentationRepresentationVisibility(viewportId, {
segmentationId,
type,
});
};
const addSegmentationToViewport = segmentationId => {
segmentationService.addSegmentationRepresentation(viewportId, { segmentationId });
};
const removeSegmentationFromViewport = segmentationId => {
segmentationService.removeSegmentationRepresentations(viewportId, {
segmentationId,
});
};
return (
<div className="bg-muted flex h-full w-[262px] flex-col rounded p-3">
<span className="text-muted-foreground mb-2 text-xs font-semibold">Current Viewport</span>
<ul className="space-y-1">
{activeSegmentations.map(segmentation => (
<li
key={segmentation.id}
className="flex items-center text-sm"
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground mr-2"
onClick={() => removeSegmentationFromViewport(segmentation.segmentationId)}
>
<Icons.Minus className="h-6 w-6" />
</Button>
<span className="text-foreground flex-grow">{segmentation.label}</span>
{segmentation.visible ? (
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={() =>
toggleSegmentationRepresentationVisibility(
segmentation.segmentationId,
segmentation.type
)
}
>
<Icons.Hide className="h-6 w-6" />
</Button>
) : (
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={() =>
toggleSegmentationRepresentationVisibility(
segmentation.segmentationId,
segmentation.type
)
}
>
<Icons.Show className="h-6 w-6" />
</Button>
)}
</li>
))}
</ul>
{availableSegmentations.length > 0 && (
<>
<Separator className="bg-input mb-3" />
<span className="text-muted-foreground mb-2 text-xs font-semibold">Available</span>
<ul className="space-y-1">
{availableSegmentations.map(({ segmentationId, label }) => (
<li
key={segmentationId}
className="flex items-center text-sm"
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground mr-2"
onClick={() => addSegmentationToViewport(segmentationId)}
>
<Icons.Plus className="h-6 w-6" />
</Button>
<span className="text-foreground/60">{label}</span>
</li>
))}
</ul>
</>
)}
</div>
);
}
export default ViewportSegmentationMenu;
@@ -1,83 +0,0 @@
import React, { ReactNode, useEffect, useState } from 'react';
import { Button, Icons, Popover, PopoverContent, PopoverTrigger } from '@ohif/ui-next';
import ViewportSegmentationMenu from './ViewportSegmentationMenu';
import classNames from 'classnames';
import { useSegmentations } from '../../hooks/useSegmentations';
export function ViewportSegmentationMenuWrapper({
viewportId,
displaySets,
servicesManager,
commandsManager,
location,
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
}>): ReactNode {
const { viewportActionCornersService, viewportGridService } = servicesManager.services;
const segmentations = useSegmentations({ servicesManager });
const activeViewportId = viewportGridService.getActiveViewportId();
const isActiveViewport = viewportId === activeViewportId;
const { align, side } = getAlignAndSide(viewportActionCornersService, location);
if (!segmentations?.length) {
return null;
}
return (
<Popover>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<Button
variant="ghost"
size="icon"
>
<Icons.ViewportViews
className={classNames(
'text-highlight',
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
)}
/>
</Button>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={-15}
sideOffset={5}
>
<ViewportSegmentationMenu
className="w-full"
viewportId={viewportId}
displaySets={displaySets}
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
</PopoverContent>
</Popover>
);
}
const getAlignAndSide = (viewportActionCornersService, location) => {
const ViewportActionCornersLocations = viewportActionCornersService.LOCATIONS;
switch (location) {
case ViewportActionCornersLocations.topLeft:
return { align: 'start', side: 'bottom' };
case ViewportActionCornersLocations.topRight:
return { align: 'end', side: 'bottom' };
case ViewportActionCornersLocations.bottomLeft:
return { align: 'start', side: 'top' };
case ViewportActionCornersLocations.bottomRight:
return { align: 'end', side: 'top' };
default:
console.debug('Unknown location, defaulting to bottom-start');
return { align: 'start', side: 'bottom' };
}
};
@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper';
import { ViewportDataOverlayMenuWrapper } from './ViewportDataOverlayMenuWrapper';
export function getViewportDataOverlaySettingsMenu(
props: withAppTypes<{
@@ -7,5 +7,5 @@ export function getViewportDataOverlaySettingsMenu(
element: HTMLElement;
}>
): ReactNode {
return <ViewportSegmentationMenuWrapper {...props} />;
return <ViewportDataOverlayMenuWrapper {...props} />;
}
@@ -0,0 +1,156 @@
import { utilities as csUtils } from '@cornerstonejs/core';
export const DEFAULT_COLORMAP = 'hsv';
export const DEFAULT_OPACITY = 0.5;
export const DEFAULT_OPACITY_PERCENT = DEFAULT_OPACITY * 100;
export const DERIVED_OVERLAY_MODALITIES = ['SEG', 'RTSTRUCT'];
/**
* Get modality-specific color and opacity settings from the customization service
*/
export function getModalityOverlayColormap(customizationService, modality) {
const modalityOverlayDefaultColorMaps = customizationService?.getCustomization(
'cornerstone.modalityOverlayDefaultColorMaps'
) || { defaultSettings: {} };
return (
modalityOverlayDefaultColorMaps.defaultSettings[modality] || {
colormap: DEFAULT_COLORMAP,
opacity: DEFAULT_OPACITY,
}
);
}
/**
* Identifies display sets that can be used as overlays for a specific viewport.
*
* "Enhanced" display sets are those that:
* 1. Are not already in the viewport
* 2. Are evaluated for their ability to be overlaid onto the background display set
* 3. Have an "isOverlayable" flag indicating if they're compatible with the viewport
*
* A display set is considered overlayable when:
* - The background display set is reconstructable
* - The display set is not unsupported
* - The Frame of Reference matches the background display set
* - For non-derived modalities: background can be a volume and display set is either multiframe or valid volume
*
* @returns {Object} Object containing:
* - viewportDisplaySets: Display sets already in the viewport
* - enhancedDisplaySets: Other display sets with overlayability assessment
*/
export function getEnhancedDisplaySets({ viewportId, services }) {
const { displaySetService, viewportGridService } = services;
const displaySetsUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const allDisplaySets = displaySetService.getActiveDisplaySets();
const otherDisplaySets = allDisplaySets.filter(
displaySet => !displaySetsUIDs.includes(displaySet.displaySetInstanceUID)
);
const viewportDisplaySets = displaySetsUIDs.map(displaySetUID =>
displaySetService.getDisplaySetByUID(displaySetUID)
);
const backgroundCanBeVolume = csUtils.isValidVolume(viewportDisplaySets[0].imageIds);
const backgroundDisplaySet = viewportDisplaySets[0];
const enhancedDisplaySets = otherDisplaySets.map(displaySet => {
if (!backgroundDisplaySet.isReconstructable) {
return {
...displaySet,
isOverlayable: false,
};
}
if (displaySet.unsupported) {
return {
...displaySet,
isOverlayable: false,
};
}
// Check if Frame of Reference matches
if (
displaySet.FrameOfReferenceUID &&
displaySet.FrameOfReferenceUID !== backgroundDisplaySet.FrameOfReferenceUID
) {
return {
...displaySet,
isOverlayable: false,
};
}
// Special handling for derived modalities
if (!DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality)) {
if (!backgroundCanBeVolume) {
return {
...displaySet,
isOverlayable: false,
};
}
const imageIds = displaySet.imageIds || displaySet.images?.map(image => image.imageId);
const isMultiframe = displaySet.isMultiFrame;
if (!isMultiframe && imageIds?.length > 0 && !csUtils.isValidVolume(imageIds)) {
return {
...displaySet,
isOverlayable: false,
};
}
}
return {
...displaySet,
isOverlayable: true,
};
});
return {
viewportDisplaySets,
enhancedDisplaySets,
};
}
/**
* Sort function: puts disabled items (isOverlayable: false) at the end
*/
export const sortByOverlayable = (a, b) => {
if (a.isOverlayable === b.isOverlayable) {
return 0;
}
return a.isOverlayable ? -1 : 1;
};
/**
* Create display set options based on modality and opacity settings
*/
export function createColormapOverlayDisplaySetOptions(displaySet, opacity, customizationService) {
if (displaySet.Modality === 'SEG') {
return {};
}
const modalitySettings = getModalityOverlayColormap(customizationService, displaySet.Modality);
return {
colormap: {
name: modalitySettings.colormap || DEFAULT_COLORMAP,
opacity: opacity / 100, // Convert from percentage to 0-1 range
},
};
}
/**
* Get segmentations that can be added as overlays to the viewport
*
* Note: This function is deprecated as we now use display sets for segmentations
*/
export function getAvailableSegmentations(segmentationService) {
const segmentations = segmentationService.getSegmentations() || [];
return segmentations.map(segmentation => ({
segmentationId: segmentation.segmentationId,
label: segmentation.label || 'Segmentation',
segments: segmentation.segments,
frameOfReferenceUID: segmentation.frameOfReferenceUID,
}));
}