feat(data overlay): add foreground background control plus orientation change in the UI and add RTDOSE support (#5007)
This commit is contained in:
1 parent
c1fced0573
commit
cd4e6408bc
105 files changed
+3117
-788
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
|
||||
export type OHIFViewportActionCornersProps = {
|
||||
viewportId: string;
|
||||
@@ -9,8 +10,13 @@ export type OHIFViewportActionCornersProps = {
|
||||
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
|
||||
const { servicesManager } = useSystem();
|
||||
const [viewportActionCornersState] = useViewportActionCornersContext();
|
||||
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const isActiveViewport = viewportGrid.activeViewportId === viewportId;
|
||||
|
||||
const ViewportActionCorners =
|
||||
servicesManager.services.customizationService.getCustomization('ui.viewportActionCorner');
|
||||
|
||||
if (!viewportActionCornersState[viewportId]) {
|
||||
return null;
|
||||
}
|
||||
@@ -18,7 +24,8 @@ function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProp
|
||||
return (
|
||||
<ViewportActionCorners
|
||||
cornerComponents={viewportActionCornersState[viewportId]}
|
||||
></ViewportActionCorners>
|
||||
isActiveViewport={isActiveViewport}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+478
@@ -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;
|
||||
+62
@@ -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>
|
||||
);
|
||||
}
|
||||
-136
@@ -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;
|
||||
-83
@@ -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,
|
||||
}));
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { Icons, useViewportGrid } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
Button,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string }>) {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const [viewportGridState, viewportGridService] = useViewportGrid();
|
||||
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
|
||||
|
||||
const viewportId = viewportGridState.activeViewportId;
|
||||
|
||||
const handleOrientationChange = (orientation: string) => {
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const currentViewportType = viewportInfo?.getViewportType();
|
||||
|
||||
// Get the displaySets in this viewport
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
const displaySets = displaySetUIDs
|
||||
.map(uid => displaySetService.getDisplaySetByUID(uid))
|
||||
.filter(Boolean);
|
||||
|
||||
if (!displaySets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if at least one displaySet is reconstructable
|
||||
const hasReconstructableDisplaySet = displaySets.some(ds => ds.isReconstructable);
|
||||
|
||||
if (!hasReconstructableDisplaySet) {
|
||||
console.warn('Cannot change orientation: No reconstructable display sets in viewport');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the orientation enum based on the selected orientation
|
||||
let orientationEnum;
|
||||
switch (orientation.toLowerCase()) {
|
||||
case 'axial':
|
||||
orientationEnum = Enums.OrientationAxis.AXIAL;
|
||||
break;
|
||||
case 'sagittal':
|
||||
orientationEnum = Enums.OrientationAxis.SAGITTAL;
|
||||
break;
|
||||
case 'coronal':
|
||||
orientationEnum = Enums.OrientationAxis.CORONAL;
|
||||
break;
|
||||
default:
|
||||
orientationEnum = Enums.OrientationAxis.ACQUISITION;
|
||||
}
|
||||
|
||||
// If viewport is not already a volume type, we need to convert it
|
||||
if (currentViewportType !== Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
// Configure the viewport to be a volume viewport with current display sets
|
||||
const updatedViewport = {
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: displaySetUIDs,
|
||||
viewportOptions: {
|
||||
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
||||
orientation: orientationEnum,
|
||||
},
|
||||
displaySetOptions: displaySetUIDs.map(() => ({})),
|
||||
};
|
||||
|
||||
// Update the viewport to be a volume viewport
|
||||
commandsManager.run('setDisplaySetsForViewports', {
|
||||
viewportsToUpdate: [updatedViewport],
|
||||
});
|
||||
} else {
|
||||
// Set the viewport orientation
|
||||
commandsManager.runCommand('setViewportOrientation', {
|
||||
viewportId,
|
||||
orientation: orientationEnum,
|
||||
});
|
||||
}
|
||||
};
|
||||
const { viewportActionCornersService } = servicesManager.services;
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
const displaySets = displaySetUIDs
|
||||
.map(uid => displaySetService.getDisplaySetByUID(uid))
|
||||
.filter(Boolean);
|
||||
|
||||
if (!displaySets.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasReconstructableDisplaySet = displaySets.some(ds => ds.isReconstructable);
|
||||
|
||||
// Get proper alignment and side based on the location
|
||||
let align = 'center';
|
||||
let side = 'bottom';
|
||||
|
||||
if (location !== undefined) {
|
||||
const positioning = viewportActionCornersService.getAlignAndSide(location);
|
||||
align = positioning.align;
|
||||
side = positioning.side;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-highlight"
|
||||
disabled={!hasReconstructableDisplaySet}
|
||||
>
|
||||
<Icons.OrientationSwitch />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-[160px]"
|
||||
align={align}
|
||||
side={side}
|
||||
sideOffset={5}
|
||||
>
|
||||
<DropdownMenuLabel className="-ml-1">Orientation</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => handleOrientationChange('axial')}>Axial</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleOrientationChange('sagittal')}>
|
||||
Sagittal
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleOrientationChange('coronal')}>
|
||||
Coronal
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleOrientationChange('acquisition')}>
|
||||
Acquisition
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewportOrientationMenu;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { cn } from '@ohif/ui-next';
|
||||
import ViewportOrientationMenu from './ViewportOrientationMenu';
|
||||
|
||||
export function ViewportOrientationMenuWrapper({
|
||||
viewportId,
|
||||
location,
|
||||
}: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
location: string;
|
||||
}>): ReactNode {
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const isActiveViewport = viewportId === viewportGrid.activeViewportId;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-end',
|
||||
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
|
||||
)}
|
||||
>
|
||||
<ViewportOrientationMenu location={location} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { ViewportOrientationMenuWrapper } from './ViewportOrientationMenuWrapper';
|
||||
|
||||
export function getViewportOrientationMenu(
|
||||
props: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
location: string;
|
||||
}>
|
||||
): ReactNode {
|
||||
return <ViewportOrientationMenuWrapper {...props} />;
|
||||
}
|
||||
|
||||
export * from './ViewportOrientationMenu';
|
||||
@@ -1,18 +1,18 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { Switch } from '@ohif/ui-next';
|
||||
import { StackViewport, VolumeViewport } from '@cornerstonejs/core';
|
||||
import { StackViewport } from '@cornerstonejs/core';
|
||||
import { ColorbarProps } from '../../types/Colorbar';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function setViewportColorbar(
|
||||
viewportId,
|
||||
displaySets,
|
||||
commandsManager,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
colorbarOptions
|
||||
) {
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const { cornerstoneViewportService, viewportGridService } = servicesManager.services;
|
||||
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const backgroundColor = viewportInfo.getViewportOptions().background;
|
||||
@@ -32,36 +32,18 @@ export function setViewportColorbar(
|
||||
};
|
||||
}
|
||||
|
||||
const displaySetInstanceUIDs = [];
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
displaySetInstanceUIDs.push(viewportId);
|
||||
}
|
||||
|
||||
if (viewport instanceof VolumeViewport) {
|
||||
displaySets.forEach(ds => {
|
||||
displaySetInstanceUIDs.push(ds.displaySetInstanceUID);
|
||||
});
|
||||
}
|
||||
|
||||
commandsManager.run({
|
||||
commandName: 'toggleViewportColorbar',
|
||||
commandOptions: {
|
||||
viewportId,
|
||||
options: colorbarOptions,
|
||||
displaySetInstanceUIDs,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
commandsManager.run('toggleViewportColorbar', {
|
||||
viewportId,
|
||||
options: colorbarOptions,
|
||||
displaySetInstanceUIDs,
|
||||
});
|
||||
}
|
||||
|
||||
export function Colorbar({
|
||||
viewportId,
|
||||
displaySets,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
colorbarProperties,
|
||||
}: withAppTypes<ColorbarProps>): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { colorbarService } = servicesManager.services;
|
||||
const {
|
||||
width: colorbarWidth,
|
||||
@@ -73,7 +55,7 @@ export function Colorbar({
|
||||
const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId));
|
||||
|
||||
const onSetColorbar = useCallback(() => {
|
||||
setViewportColorbar(viewportId, displaySets, commandsManager, servicesManager, {
|
||||
setViewportColorbar(viewportId, commandsManager, servicesManager, {
|
||||
viewportId,
|
||||
colormaps,
|
||||
ticks: {
|
||||
|
||||
@@ -3,14 +3,10 @@ import { AllInOneMenu, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next
|
||||
|
||||
import { StackViewport, Types } from '@cornerstonejs/core';
|
||||
import { ColormapProps } from '../../types/Colormap';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function Colormap({
|
||||
colormaps,
|
||||
viewportId,
|
||||
displaySets,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
}: ColormapProps): ReactElement {
|
||||
export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]);
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import React, { ReactElement, useState, useEffect, useCallback } from 'react';
|
||||
import { VolumeLightingProps } from '../../types/ViewportPresets';
|
||||
import { Numeric } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function VolumeLighting({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
viewportId,
|
||||
hasShade,
|
||||
}: VolumeLightingProps): ReactElement {
|
||||
export function VolumeLighting({ viewportId, hasShade }: VolumeLightingProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [lightingValues, setLightingValues] = useState({
|
||||
ambient: null,
|
||||
|
||||
+2
-14
@@ -5,43 +5,31 @@ import { VolumeRenderingQuality } from './VolumeRenderingQuality';
|
||||
import { VolumeShift } from './VolumeShift';
|
||||
import { VolumeLighting } from './VolumeLighting';
|
||||
import { VolumeShade } from './VolumeShade';
|
||||
|
||||
export function VolumeRenderingOptions({
|
||||
viewportId,
|
||||
commandsManager,
|
||||
volumeRenderingQualityRange,
|
||||
servicesManager,
|
||||
}: VolumeRenderingOptionsProps): ReactElement {
|
||||
const [hasShade, setShade] = useState(false);
|
||||
return (
|
||||
<AllInOneMenu.ItemPanel>
|
||||
<VolumeRenderingQuality
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||
/>
|
||||
|
||||
<VolumeShift
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
<VolumeShift viewportId={viewportId} />
|
||||
<div className="mt-2 flex h-8 !h-[20px] w-full flex-shrink-0 items-center justify-start px-2 text-base">
|
||||
<div className="text-muted-foreground text-sm">Lighting</div>
|
||||
</div>
|
||||
<div className="bg-background mt-1 mb-1 h-px w-full"></div>
|
||||
<div className="hover:bg-accent flex h-8 w-full flex-shrink-0 items-center px-2 text-base hover:rounded">
|
||||
<VolumeShade
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
viewportId={viewportId}
|
||||
onClickShade={setShade}
|
||||
/>
|
||||
</div>
|
||||
<VolumeLighting
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
hasShade={hasShade}
|
||||
/>
|
||||
</AllInOneMenu.ItemPanel>
|
||||
|
||||
+2
-3
@@ -3,13 +3,13 @@ import { Icons } from '@ohif/ui-next';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets';
|
||||
import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function VolumeRenderingPresets({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
volumeRenderingPresets,
|
||||
}: VolumeRenderingPresetsProps): ReactElement {
|
||||
const { servicesManager } = useSystem();
|
||||
const { uiDialogService } = servicesManager.services;
|
||||
|
||||
const onClickPresets = () => {
|
||||
@@ -21,7 +21,6 @@ export function VolumeRenderingPresets({
|
||||
contentProps: {
|
||||
presets: volumeRenderingPresets,
|
||||
viewportId,
|
||||
commandsManager,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
import React, { ReactElement, useCallback, useState, useEffect } from 'react';
|
||||
import { VolumeRenderingQualityProps } from '../../types/ViewportPresets';
|
||||
import { Numeric } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function VolumeRenderingQuality({
|
||||
volumeRenderingQualityRange,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
viewportId,
|
||||
}: VolumeRenderingQualityProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const { min, max, step } = volumeRenderingQualityRange;
|
||||
const [quality, setQuality] = useState(null);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { Switch } from '@ohif/ui-next';
|
||||
import { VolumeShadeProps } from '../../types/ViewportPresets';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function VolumeShade({
|
||||
commandsManager,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
onClickShade = bool => {},
|
||||
}: VolumeShadeProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [shade, setShade] = useState(true);
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { VolumeShiftProps } from '../../types/ViewportPresets';
|
||||
import { Numeric } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function VolumeShift({
|
||||
viewportId,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
}: VolumeShiftProps): ReactElement {
|
||||
export function VolumeShift({ viewportId }: VolumeShiftProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [minShift, setMinShift] = useState<number | null>(null);
|
||||
const [maxShift, setMaxShift] = useState<number | null>(null);
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import React, { ReactElement, useCallback } from 'react';
|
||||
import { AllInOneMenu } from '@ohif/ui-next';
|
||||
import { WindowLevelPreset } from '../../types/WindowLevel';
|
||||
import { CommandsManager } from '@ohif/core';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type WindowLevelProps = {
|
||||
viewportId: string;
|
||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
||||
commandsManager: CommandsManager;
|
||||
};
|
||||
|
||||
export function WindowLevel({
|
||||
viewportId,
|
||||
commandsManager,
|
||||
presets,
|
||||
}: WindowLevelProps): ReactElement {
|
||||
export function WindowLevel({ viewportId, presets }: WindowLevelProps): ReactElement {
|
||||
const { commandsManager } = useSystem();
|
||||
const { t } = useTranslation('WindowLevelActionMenu');
|
||||
|
||||
const onSetWindowLevel = useCallback(
|
||||
|
||||
+6
-17
@@ -15,8 +15,7 @@ import { VolumeRenderingOptions } from './VolumeRenderingOptions';
|
||||
import { ViewportPreset } from '../../types/ViewportPresets';
|
||||
import { VolumeViewport3D } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
|
||||
export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export type WindowLevelActionMenuProps = {
|
||||
viewportId: string;
|
||||
@@ -34,13 +33,12 @@ export function WindowLevelActionMenu({
|
||||
presets,
|
||||
verticalDirection,
|
||||
horizontalDirection,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
colorbarProperties,
|
||||
displaySets,
|
||||
volumeRenderingPresets,
|
||||
volumeRenderingQualityRange,
|
||||
}: withAppTypes<WindowLevelActionMenuProps>): ReactElement {
|
||||
const { commandsManager, servicesManager } = useSystem();
|
||||
const {
|
||||
colormaps,
|
||||
colorbarContainerPosition,
|
||||
@@ -48,6 +46,7 @@ export function WindowLevelActionMenu({
|
||||
colorbarTickPosition,
|
||||
width: colorbarWidth,
|
||||
} = colorbarProperties;
|
||||
|
||||
const { colorbarService, cornerstoneViewportService } = servicesManager.services;
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
@@ -64,7 +63,7 @@ export function WindowLevelActionMenu({
|
||||
const [is3DVolume, setIs3DVolume] = useState(false);
|
||||
|
||||
const onSetColorbar = useCallback(() => {
|
||||
setViewportColorbar(viewportId, displaySets, commandsManager, servicesManager, {
|
||||
setViewportColorbar(viewportId, commandsManager, servicesManager, {
|
||||
colormaps,
|
||||
ticks: {
|
||||
position: colorbarTickPosition,
|
||||
@@ -117,7 +116,6 @@ export function WindowLevelActionMenu({
|
||||
verticalDirection={verticalDirection}
|
||||
horizontalDirection={horizontalDirection}
|
||||
iconClassName={classNames(
|
||||
// Visible on hover and for the active viewport
|
||||
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'
|
||||
@@ -132,9 +130,7 @@ export function WindowLevelActionMenu({
|
||||
{!is3DVolume && (
|
||||
<Colorbar
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
|
||||
colorbarProperties={colorbarProperties}
|
||||
/>
|
||||
)}
|
||||
@@ -150,9 +146,7 @@ export function WindowLevelActionMenu({
|
||||
className="flex h-full w-full flex-col"
|
||||
colormaps={colormaps}
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
|
||||
/>
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
@@ -165,7 +159,6 @@ export function WindowLevelActionMenu({
|
||||
>
|
||||
<WindowLevel
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
presets={presets}
|
||||
/>
|
||||
</AllInOneMenu.SubMenu>
|
||||
@@ -173,9 +166,7 @@ export function WindowLevelActionMenu({
|
||||
|
||||
{volumeRenderingPresets && is3DVolume && (
|
||||
<VolumeRenderingPresets
|
||||
servicesManager={servicesManager}
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
volumeRenderingPresets={volumeRenderingPresets}
|
||||
/>
|
||||
)}
|
||||
@@ -184,9 +175,7 @@ export function WindowLevelActionMenu({
|
||||
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
|
||||
<VolumeRenderingOptions
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
|
||||
+10
-14
@@ -1,12 +1,12 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { nonWLModalities } from './WindowLevelActionMenu';
|
||||
import { WindowLevelActionMenuProps } from './WindowLevelActionMenu';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { WindowLevelActionMenu as WindowLevelActionMenuComponent } from './WindowLevelActionMenu';
|
||||
|
||||
export function getWindowLevelActionMenu({
|
||||
function WindowLevelActionMenu({
|
||||
viewportId,
|
||||
element,
|
||||
displaySets,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
verticalDirection,
|
||||
horizontalDirection,
|
||||
}: withAppTypes<{
|
||||
@@ -14,31 +14,25 @@ export function getWindowLevelActionMenu({
|
||||
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 WindowLevelActionMenu = customizationService.getCustomization(
|
||||
'viewportActionMenu.windowLevelActionMenu'
|
||||
);
|
||||
const displaySetPresets = displaySets
|
||||
.filter(displaySet => presets[displaySet.Modality])
|
||||
.map(displaySet => {
|
||||
return { [displaySet.Modality]: presets[displaySet.Modality] };
|
||||
});
|
||||
|
||||
const modalities = displaySets
|
||||
.map(displaySet => displaySet.Modality)
|
||||
.filter(modality => !nonWLModalities.includes(modality));
|
||||
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
|
||||
|
||||
if (modalities.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const WindowLevelActionMenuComponent = WindowLevelActionMenu?.component;
|
||||
|
||||
return (
|
||||
<WindowLevelActionMenuComponent
|
||||
viewportId={viewportId}
|
||||
@@ -46,8 +40,6 @@ export function getWindowLevelActionMenu({
|
||||
presets={displaySetPresets}
|
||||
verticalDirection={verticalDirection}
|
||||
horizontalDirection={horizontalDirection}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
colorbarProperties={colorbarProperties}
|
||||
displaySets={displaySets}
|
||||
volumeRenderingPresets={volumeRenderingPresets}
|
||||
@@ -55,3 +47,7 @@ export function getWindowLevelActionMenu({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function getWindowLevelActionMenu(props: WindowLevelActionMenuProps) {
|
||||
return <WindowLevelActionMenu {...props} />;
|
||||
}
|
||||
Reference in new issue
Block a user