feat(actions): simplify action corner api through the toolbarService (#5033)
This commit is contained in:
1 parent
f71b88e639
commit
7c15bb8901
246 files changed
+7930
-4270
No files matched your search
@@ -1,103 +1,27 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import React, { ReactElement, useCallback } from 'react';
|
||||
import { Switch } from '@ohif/ui-next';
|
||||
import { ColorbarProps, ColorbarOptions } from '../../types/Colorbar';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||
|
||||
export function setViewportColorbar(
|
||||
viewportId: string,
|
||||
commandsManager: any,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
colorbarOptions: Partial<ColorbarOptions>
|
||||
) {
|
||||
const { cornerstoneViewportService, viewportGridService } = servicesManager.services;
|
||||
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
export function Colorbar({ viewportId }: { viewportId?: string } = {}): ReactElement {
|
||||
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const backgroundColor = viewportInfo.getViewportOptions().background;
|
||||
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
|
||||
|
||||
if (isLight) {
|
||||
colorbarOptions.ticks = {
|
||||
position: 'left',
|
||||
style: {
|
||||
font: '13px Inter',
|
||||
color: '#000000',
|
||||
maxNumTicks: 8,
|
||||
tickSize: 5,
|
||||
tickWidth: 1,
|
||||
labelMargin: 3,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
commandsManager.run('toggleViewportColorbar', {
|
||||
viewportId,
|
||||
options: colorbarOptions,
|
||||
displaySetInstanceUIDs,
|
||||
});
|
||||
}
|
||||
|
||||
export function Colorbar({
|
||||
viewportId,
|
||||
colorbarProperties,
|
||||
}: withAppTypes<ColorbarProps>): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { colorbarService } = servicesManager.services;
|
||||
const {
|
||||
width: colorbarWidth,
|
||||
colorbarTickPosition,
|
||||
colorbarContainerPosition,
|
||||
colormaps,
|
||||
colorbarInitialColormap,
|
||||
} = colorbarProperties;
|
||||
const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId));
|
||||
|
||||
const onSetColorbar = useCallback(() => {
|
||||
setViewportColorbar(viewportId, commandsManager, servicesManager, {
|
||||
viewportId,
|
||||
colormaps,
|
||||
ticks: {
|
||||
position: colorbarTickPosition,
|
||||
},
|
||||
width: colorbarWidth,
|
||||
position: colorbarContainerPosition,
|
||||
activeColormapName: colorbarInitialColormap,
|
||||
});
|
||||
}, [commandsManager]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateColorbarState = () => {
|
||||
setShowColorbar(colorbarService.hasColorbar(viewportId));
|
||||
};
|
||||
|
||||
const { unsubscribe } = colorbarService.subscribe(
|
||||
colorbarService.EVENTS.STATE_CHANGED,
|
||||
updateColorbarState
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewportId]);
|
||||
const handleToggle = useCallback(() => {
|
||||
toggleColorbar();
|
||||
}, [toggleColorbar]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSetColorbar();
|
||||
}}
|
||||
>
|
||||
<div className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded">
|
||||
<div className="flex w-7 flex-shrink-0 items-center justify-center"></div>
|
||||
<span className="flex-grow">Display Color bar</span>
|
||||
<span
|
||||
className="flex-grow"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
Display Color bar
|
||||
</span>
|
||||
<Switch
|
||||
className="ml-2 flex-shrink-0"
|
||||
checked={showColorbar}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSetColorbar();
|
||||
}}
|
||||
checked={!!hasColorbar}
|
||||
onCheckedChange={handleToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,91 +1,66 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { AllInOneMenu, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
|
||||
import React, { ReactElement, useEffect, useRef, useState } from 'react';
|
||||
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
|
||||
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||
|
||||
import { StackViewport, Types } from '@cornerstonejs/core';
|
||||
import { ColormapProps } from '../../types/Colormap';
|
||||
import { useSystem } from '@ohif/core';
|
||||
export function Colormap({ viewportId }: { viewportId?: string } = {}): ReactElement {
|
||||
const { viewportDisplaySets } = useViewportRendering(viewportId);
|
||||
|
||||
export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
|
||||
viewportDisplaySets?.[0]?.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]);
|
||||
// Use the hook with the active display set
|
||||
const { colorbarProperties, setColormap } = useViewportRendering(viewportId, {
|
||||
displaySetInstanceUID: activeDisplaySetUID,
|
||||
});
|
||||
|
||||
const { colormaps } = colorbarProperties;
|
||||
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [prePreviewColormap, setPrePreviewColormap] = useState(null);
|
||||
const [currentColormap, setCurrentColormap] = useState(null);
|
||||
|
||||
const showPreviewRef = useRef(showPreview);
|
||||
showPreviewRef.current = showPreview;
|
||||
const prePreviewColormapRef = useRef(prePreviewColormap);
|
||||
prePreviewColormapRef.current = prePreviewColormap;
|
||||
const activeDisplaySetRef = useRef(activeDisplaySet);
|
||||
activeDisplaySetRef.current = activeDisplaySet;
|
||||
|
||||
const onSetColorLUT = useCallback(
|
||||
props => {
|
||||
// TODO: Better way to check if it's a fusion
|
||||
const oneOpacityColormaps = ['Grayscale', 'X Ray'];
|
||||
const opacity =
|
||||
displaySets.length > 1 && !oneOpacityColormaps.includes(props.colormap.name) ? 0.5 : 1;
|
||||
commandsManager.run({
|
||||
commandName: 'setViewportColormap',
|
||||
commandOptions: {
|
||||
...props,
|
||||
opacity,
|
||||
immediate: true,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
});
|
||||
},
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
const getViewportColormap = (viewportId, displaySet) => {
|
||||
const { displaySetInstanceUID } = displaySet;
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
if (viewport instanceof StackViewport) {
|
||||
const { colormap } = viewport.getProperties();
|
||||
if (!colormap) {
|
||||
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
|
||||
}
|
||||
return colormap;
|
||||
}
|
||||
const actorEntries = viewport.getActors();
|
||||
const actorEntry = actorEntries?.find(entry =>
|
||||
entry.referencedId.includes(displaySetInstanceUID)
|
||||
);
|
||||
const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId);
|
||||
if (!colormap) {
|
||||
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
|
||||
}
|
||||
return colormap;
|
||||
};
|
||||
|
||||
const activeIndex = displaySets.findIndex(
|
||||
ds => ds.displaySetInstanceUID === activeDisplaySetRef.current.displaySetInstanceUID
|
||||
);
|
||||
const currentColormapRef = useRef(currentColormap);
|
||||
currentColormapRef.current = currentColormap;
|
||||
|
||||
useEffect(() => {
|
||||
setActiveDisplaySet(displaySets[displaySets.length - 1]);
|
||||
}, [displaySets]);
|
||||
setCurrentColormap(null);
|
||||
setPrePreviewColormap(null);
|
||||
}, [activeDisplaySetUID]);
|
||||
|
||||
const handleSetColorLUT = (colormap, immediate = true) => {
|
||||
// Check if it's a fusion viewport
|
||||
const oneOpacityColormaps = ['Grayscale', 'X Ray'];
|
||||
const opacity =
|
||||
viewportDisplaySets.length > 1 && !oneOpacityColormaps.includes(colormap.name) ? 0.5 : 1;
|
||||
|
||||
setColormap({
|
||||
colormap,
|
||||
opacity,
|
||||
immediate,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{displaySets.length > 1 && (
|
||||
{viewportDisplaySets && viewportDisplaySets.length > 1 && (
|
||||
<div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
|
||||
<Tabs
|
||||
value={String(activeIndex)}
|
||||
onValueChange={val => {
|
||||
const index = parseInt(val, 10);
|
||||
setActiveDisplaySet(displaySets[index]);
|
||||
value={activeDisplaySetUID}
|
||||
onValueChange={displaySetUID => {
|
||||
setActiveDisplaySetUID(displaySetUID);
|
||||
setPrePreviewColormap(null);
|
||||
}}
|
||||
>
|
||||
<TabsList>
|
||||
{displaySets.map((ds, i) => (
|
||||
{viewportDisplaySets.map(ds => (
|
||||
<TabsTrigger
|
||||
key={i}
|
||||
value={String(i)}
|
||||
key={ds.displaySetInstanceUID}
|
||||
value={ds.displaySetInstanceUID}
|
||||
>
|
||||
{ds.Modality}
|
||||
</TabsTrigger>
|
||||
@@ -94,6 +69,7 @@ export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps):
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
@@ -104,49 +80,47 @@ export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps):
|
||||
checked={showPreview}
|
||||
onCheckedChange={checked => {
|
||||
setShowPreview(checked);
|
||||
|
||||
if (!checked && currentColormapRef.current) {
|
||||
handleSetColorLUT(currentColormapRef.current);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AllInOneMenu.DividerItem />
|
||||
<AllInOneMenu.ItemPanel
|
||||
maxHeight="calc(100vh - 250px)"
|
||||
className="min-h-[200px] flex-grow"
|
||||
>
|
||||
{colormaps.map((colormap, index) => (
|
||||
<AllInOneMenu.Item
|
||||
key={index}
|
||||
label={colormap.description}
|
||||
useIconSpace={false}
|
||||
onClick={() => {
|
||||
onSetColorLUT({
|
||||
viewportId,
|
||||
colormap,
|
||||
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
|
||||
});
|
||||
setPrePreviewColormap(null);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (showPreviewRef.current) {
|
||||
setPrePreviewColormap(getViewportColormap(viewportId, activeDisplaySetRef.current));
|
||||
onSetColorLUT({
|
||||
viewportId,
|
||||
colormap,
|
||||
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (showPreviewRef.current && prePreviewColormapRef.current) {
|
||||
onSetColorLUT({
|
||||
viewportId,
|
||||
colormap: prePreviewColormapRef.current,
|
||||
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
}}
|
||||
></AllInOneMenu.Item>
|
||||
))}
|
||||
</AllInOneMenu.ItemPanel>
|
||||
|
||||
<div className="h-[300px] flex-grow">
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="p-1">
|
||||
{colormaps.map((colormap, index) => (
|
||||
<AllInOneMenu.Item
|
||||
key={index}
|
||||
label={colormap.description}
|
||||
useIconSpace={false}
|
||||
onClick={() => {
|
||||
setCurrentColormap(colormap);
|
||||
handleSetColorLUT(colormap);
|
||||
setPrePreviewColormap(null);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (showPreviewRef.current) {
|
||||
if (!prePreviewColormapRef.current) {
|
||||
setPrePreviewColormap(colormap);
|
||||
}
|
||||
handleSetColorLUT(colormap);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (showPreviewRef.current && prePreviewColormapRef.current) {
|
||||
handleSetColorLUT(prePreviewColormapRef.current);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+3
-5
@@ -1,15 +1,13 @@
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
import { AllInOneMenu } from '@ohif/ui-next';
|
||||
import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets';
|
||||
import { VolumeRenderingQuality } from './VolumeRenderingQuality';
|
||||
import { VolumeShift } from './VolumeShift';
|
||||
import { VolumeLighting } from './VolumeLighting';
|
||||
import { VolumeShade } from './VolumeShade';
|
||||
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||
|
||||
export function VolumeRenderingOptions({
|
||||
viewportId,
|
||||
volumeRenderingQualityRange,
|
||||
}: VolumeRenderingOptionsProps): ReactElement {
|
||||
export function VolumeRenderingOptions({ viewportId }: { viewportId?: string } = {}): ReactElement {
|
||||
const { volumeRenderingQualityRange } = useViewportRendering(viewportId);
|
||||
const [hasShade, setShade] = useState(false);
|
||||
return (
|
||||
<AllInOneMenu.ItemPanel>
|
||||
|
||||
+3
-5
@@ -1,14 +1,12 @@
|
||||
import { AllInOneMenu } from '@ohif/ui-next';
|
||||
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';
|
||||
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||
|
||||
export function VolumeRenderingPresets({
|
||||
viewportId,
|
||||
volumeRenderingPresets,
|
||||
}: VolumeRenderingPresetsProps): ReactElement {
|
||||
export function VolumeRenderingPresets({ viewportId }: { viewportId?: string } = {}): ReactElement {
|
||||
const { volumeRenderingPresets } = useViewportRendering(viewportId);
|
||||
const { servicesManager } = useSystem();
|
||||
const { uiDialogService } = servicesManager.services;
|
||||
|
||||
|
||||
@@ -1,54 +1,122 @@
|
||||
import React, { ReactElement, useCallback } from 'react';
|
||||
import { AllInOneMenu } from '@ohif/ui-next';
|
||||
import React, { ReactElement, useEffect, useRef, useState } from 'react';
|
||||
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
|
||||
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||
import { WindowLevelPreset } from '../../types/WindowLevel';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type WindowLevelProps = {
|
||||
viewportId: string;
|
||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
||||
};
|
||||
|
||||
export function WindowLevel({ viewportId, presets }: WindowLevelProps): ReactElement {
|
||||
const { commandsManager } = useSystem();
|
||||
const { t } = useTranslation('WindowLevelActionMenu');
|
||||
|
||||
const onSetWindowLevel = useCallback(
|
||||
props => {
|
||||
commandsManager.run({
|
||||
commandName: 'setViewportWindowLevel',
|
||||
commandOptions: {
|
||||
...props,
|
||||
viewportId,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
});
|
||||
},
|
||||
[commandsManager, viewportId]
|
||||
export function WindowLevel({ viewportId }: { viewportId?: string } = {}): ReactElement {
|
||||
const { viewportDisplaySets } = useViewportRendering(viewportId);
|
||||
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
|
||||
viewportDisplaySets?.[0]?.displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Use the hook with the active display set
|
||||
const { windowLevelPresets, setWindowLevel } = useViewportRendering(viewportId, {
|
||||
displaySetInstanceUID: activeDisplaySetUID,
|
||||
});
|
||||
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [prePreviewPreset, setPrePreviewPreset] = useState<WindowLevelPreset | null>(null);
|
||||
const [currentPreset, setCurrentPreset] = useState<WindowLevelPreset | null>(null);
|
||||
|
||||
const showPreviewRef = useRef(showPreview);
|
||||
showPreviewRef.current = showPreview;
|
||||
const prePreviewPresetRef = useRef(prePreviewPreset);
|
||||
prePreviewPresetRef.current = prePreviewPreset;
|
||||
const currentPresetRef = useRef(currentPreset);
|
||||
currentPresetRef.current = currentPreset;
|
||||
|
||||
// Reset presets when active display set changes
|
||||
useEffect(() => {
|
||||
setCurrentPreset(null);
|
||||
setPrePreviewPreset(null);
|
||||
}, [activeDisplaySetUID]);
|
||||
|
||||
// Handle applying window level preset
|
||||
const handleSetWindowLevel = (preset: WindowLevelPreset, immediate = false) => {
|
||||
setWindowLevel({
|
||||
windowWidth: Number(preset.window),
|
||||
windowCenter: Number(preset.level),
|
||||
immediate,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AllInOneMenu.ItemPanel>
|
||||
{presets.map((modalityPresets, modalityIndex) => (
|
||||
<React.Fragment key={modalityIndex}>
|
||||
{Object.entries(modalityPresets).map(([modality, presetsArray]) => (
|
||||
<React.Fragment key={modality}>
|
||||
<AllInOneMenu.HeaderItem>
|
||||
{t('Modality Presets', { modality })}
|
||||
</AllInOneMenu.HeaderItem>
|
||||
{presetsArray.map((preset, index) => (
|
||||
<AllInOneMenu.Item
|
||||
key={`${modality}-${index}`}
|
||||
label={preset.description}
|
||||
secondaryLabel={`${preset.window} / ${preset.level}`}
|
||||
useIconSpace={false}
|
||||
onClick={() => onSetWindowLevel(preset)}
|
||||
/>
|
||||
<>
|
||||
{viewportDisplaySets && viewportDisplaySets.length > 1 && (
|
||||
<div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
|
||||
<Tabs
|
||||
value={activeDisplaySetUID}
|
||||
onValueChange={displaySetUID => {
|
||||
setActiveDisplaySetUID(displaySetUID);
|
||||
}}
|
||||
>
|
||||
<TabsList>
|
||||
{viewportDisplaySets.map(ds => (
|
||||
<TabsTrigger
|
||||
key={ds.displaySetInstanceUID}
|
||||
value={ds.displaySetInstanceUID}
|
||||
>
|
||||
{ds.Modality}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</AllInOneMenu.ItemPanel>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
>
|
||||
<span className="flex-shrink-0">Preview in viewport</span>
|
||||
<Switch
|
||||
className="ml-auto flex-shrink-0"
|
||||
checked={showPreview}
|
||||
onCheckedChange={checked => {
|
||||
setShowPreview(checked);
|
||||
|
||||
// When turning off preview, restore the current preset if one exists
|
||||
if (!checked && currentPresetRef.current) {
|
||||
handleSetWindowLevel(currentPresetRef.current, true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AllInOneMenu.DividerItem />
|
||||
|
||||
<div className="h-[175px] flex-grow">
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="p-1">
|
||||
{windowLevelPresets.map((preset, index) => (
|
||||
<AllInOneMenu.Item
|
||||
key={index}
|
||||
label={preset.description}
|
||||
secondaryLabel={`${preset.window} / ${preset.level}`}
|
||||
useIconSpace={false}
|
||||
onClick={() => {
|
||||
setCurrentPreset(preset);
|
||||
handleSetWindowLevel(preset, true);
|
||||
setPrePreviewPreset(null);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (showPreviewRef.current) {
|
||||
if (!prePreviewPresetRef.current) {
|
||||
setPrePreviewPreset(currentPresetRef.current || preset);
|
||||
}
|
||||
handleSetWindowLevel(preset, true);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (showPreviewRef.current && prePreviewPresetRef.current) {
|
||||
handleSetWindowLevel(prePreviewPresetRef.current, true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+43
-124
@@ -1,114 +1,55 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import React, { ReactElement, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AllInOneMenu, cn } from '@ohif/ui-next';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { AllInOneMenu } from '@ohif/ui-next';
|
||||
import { Colormap } from './Colormap';
|
||||
import { Colorbar } from './Colorbar';
|
||||
import { setViewportColorbar } from './Colorbar';
|
||||
import { WindowLevelPreset } from '../../types/WindowLevel';
|
||||
import { ColorbarProperties } from '../../types/Colorbar';
|
||||
import { VolumeRenderingQualityRange } from '../../types/ViewportPresets';
|
||||
import { WindowLevel } from './WindowLevel';
|
||||
import { VolumeRenderingPresets } from './VolumeRenderingPresets';
|
||||
import { VolumeRenderingOptions } from './VolumeRenderingOptions';
|
||||
import { ViewportPreset } from '../../types/ViewportPresets';
|
||||
import { VolumeViewport3D } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||
|
||||
export type WindowLevelActionMenuProps = {
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
||||
colorbarProperties: ColorbarProperties;
|
||||
displaySets: Array<any>;
|
||||
volumeRenderingPresets: Array<ViewportPreset>;
|
||||
volumeRenderingQualityRange: VolumeRenderingQualityRange;
|
||||
location: string;
|
||||
element?: HTMLElement;
|
||||
align?: 'start' | 'end' | 'center';
|
||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||
};
|
||||
|
||||
export function WindowLevelActionMenu({
|
||||
viewportId,
|
||||
element,
|
||||
presets,
|
||||
verticalDirection,
|
||||
horizontalDirection,
|
||||
colorbarProperties,
|
||||
displaySets,
|
||||
volumeRenderingPresets,
|
||||
volumeRenderingQualityRange,
|
||||
}: withAppTypes<WindowLevelActionMenuProps>): ReactElement {
|
||||
const { commandsManager, servicesManager } = useSystem();
|
||||
const {
|
||||
colormaps,
|
||||
colorbarContainerPosition,
|
||||
colorbarInitialColormap,
|
||||
colorbarTickPosition,
|
||||
width: colorbarWidth,
|
||||
} = colorbarProperties;
|
||||
|
||||
const { colorbarService, cornerstoneViewportService } = servicesManager.services;
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const backgroundColor = viewportInfo?.getViewportOptions().background;
|
||||
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
|
||||
align,
|
||||
side,
|
||||
}: WindowLevelActionMenuProps): ReactElement {
|
||||
return (
|
||||
<WindowLevelActionMenuContent
|
||||
viewportId={viewportId}
|
||||
align={align}
|
||||
side={side}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function WindowLevelActionMenuContent({
|
||||
viewportId,
|
||||
align,
|
||||
side,
|
||||
}: {
|
||||
viewportId: string;
|
||||
align?: string;
|
||||
side?: string;
|
||||
}): ReactElement {
|
||||
const { t } = useTranslation('WindowLevelActionMenu');
|
||||
// Use a stable key for the menu to avoid infinite re-renders
|
||||
const menuKey = useMemo(() => `${viewportId}`, [viewportId]);
|
||||
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const { activeViewportId } = viewportGrid;
|
||||
|
||||
const [vpHeight, setVpHeight] = useState(element?.clientHeight);
|
||||
const [menuKey, setMenuKey] = useState(0);
|
||||
const [is3DVolume, setIs3DVolume] = useState(false);
|
||||
|
||||
const onSetColorbar = useCallback(() => {
|
||||
setViewportColorbar(viewportId, commandsManager, servicesManager, {
|
||||
colormaps,
|
||||
ticks: {
|
||||
position: colorbarTickPosition,
|
||||
},
|
||||
width: colorbarWidth,
|
||||
position: colorbarContainerPosition,
|
||||
activeColormapName: colorbarInitialColormap,
|
||||
});
|
||||
}, [commandsManager]);
|
||||
|
||||
useEffect(() => {
|
||||
const newVpHeight = element?.clientHeight;
|
||||
if (vpHeight !== newVpHeight) {
|
||||
setVpHeight(newVpHeight);
|
||||
}
|
||||
}, [element, vpHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!colorbarService.hasColorbar(viewportId)) {
|
||||
return;
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
colorbarService.removeColorbar(viewportId);
|
||||
onSetColorbar();
|
||||
}, 0);
|
||||
}, [viewportId, displaySets, viewport]);
|
||||
|
||||
useEffect(() => {
|
||||
setMenuKey(menuKey + 1);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
if (viewport instanceof VolumeViewport3D) {
|
||||
setIs3DVolume(true);
|
||||
} else {
|
||||
setIs3DVolume(false);
|
||||
}
|
||||
}, [
|
||||
displaySets,
|
||||
viewportId,
|
||||
presets,
|
||||
volumeRenderingQualityRange,
|
||||
volumeRenderingPresets,
|
||||
const {
|
||||
is3DVolume,
|
||||
colorbarProperties,
|
||||
activeViewportId,
|
||||
viewportGrid,
|
||||
]);
|
||||
windowLevelPresets,
|
||||
volumeRenderingPresets,
|
||||
volumeRenderingQualityRange,
|
||||
} = useViewportRendering(viewportId);
|
||||
|
||||
return (
|
||||
<AllInOneMenu.Menu
|
||||
@@ -116,60 +57,38 @@ export function WindowLevelActionMenu({
|
||||
key={menuKey}
|
||||
// the visibility is handled by the parent component
|
||||
isVisible={true}
|
||||
horizontalDirection={horizontalDirection}
|
||||
verticalDirection={verticalDirection}
|
||||
align={align}
|
||||
side={side}
|
||||
>
|
||||
<AllInOneMenu.ItemPanel>
|
||||
{!is3DVolume && (
|
||||
<Colorbar
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
|
||||
colorbarProperties={colorbarProperties}
|
||||
/>
|
||||
)}
|
||||
{!is3DVolume && <Colorbar viewportId={viewportId} />}
|
||||
|
||||
{colormaps && !is3DVolume && (
|
||||
{colorbarProperties?.colormaps && !is3DVolume && (
|
||||
<AllInOneMenu.SubMenu
|
||||
key="colorLUTPresets"
|
||||
itemLabel="Color LUT"
|
||||
itemIcon="icon-color-lut"
|
||||
className="flex h-[calc(100%-32px)] flex-col"
|
||||
>
|
||||
<Colormap
|
||||
className="flex h-full w-full flex-col"
|
||||
colormaps={colormaps}
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
|
||||
/>
|
||||
<Colormap viewportId={viewportId} />
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
|
||||
{presets && presets.length > 0 && !is3DVolume && (
|
||||
{windowLevelPresets?.length > 0 && !is3DVolume && (
|
||||
<AllInOneMenu.SubMenu
|
||||
key="windowLevelPresets"
|
||||
itemLabel={t('Modality Window Presets')}
|
||||
itemIcon="viewport-window-level"
|
||||
>
|
||||
<WindowLevel
|
||||
viewportId={viewportId}
|
||||
presets={presets}
|
||||
/>
|
||||
<WindowLevel viewportId={viewportId} />
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
|
||||
{volumeRenderingPresets && is3DVolume && (
|
||||
<VolumeRenderingPresets
|
||||
viewportId={viewportId}
|
||||
volumeRenderingPresets={volumeRenderingPresets}
|
||||
/>
|
||||
)}
|
||||
{volumeRenderingPresets && is3DVolume && <VolumeRenderingPresets viewportId={viewportId} />}
|
||||
|
||||
{volumeRenderingQualityRange && is3DVolume && (
|
||||
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
|
||||
<VolumeRenderingOptions
|
||||
viewportId={viewportId}
|
||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||
/>
|
||||
<VolumeRenderingOptions viewportId={viewportId} />
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
</AllInOneMenu.ItemPanel>
|
||||
|
||||
+82
-87
@@ -2,63 +2,75 @@ import React, { ReactNode } from 'react';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Icons,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
useViewportGrid,
|
||||
useViewportActionCorners,
|
||||
AllInOneMenu,
|
||||
useIconPresentation,
|
||||
} from '@ohif/ui-next';
|
||||
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
|
||||
import { MENU_IDS } from '../menus/menu-ids';
|
||||
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
|
||||
import { useViewportRendering } from '../../hooks';
|
||||
|
||||
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;
|
||||
export function WindowLevelActionMenuWrapper(
|
||||
props: withAppTypes<{
|
||||
viewportId: string;
|
||||
element?: HTMLElement;
|
||||
location?: number;
|
||||
isOpen?: boolean;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
displaySets?: Array<AppTypes.DisplaySet>;
|
||||
disabled?: boolean;
|
||||
isEmbedded?: boolean;
|
||||
}>
|
||||
): ReactNode {
|
||||
const {
|
||||
viewportId,
|
||||
element,
|
||||
location,
|
||||
isOpen = false,
|
||||
onOpen,
|
||||
onClose,
|
||||
disabled,
|
||||
isEmbedded = false,
|
||||
onInteraction: onInteractionProps,
|
||||
hasEmbeddedVariantToUse,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [gridState] = useViewportGrid();
|
||||
const viewportIdToUse = viewportId || gridState.activeViewportId;
|
||||
|
||||
const { viewportDisplaySets: displaySets } = useViewportDisplaySets(viewportIdToUse);
|
||||
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 { toolbarService } = servicesManager.services;
|
||||
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
|
||||
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
|
||||
|
||||
const handleOpenChange = (openState: boolean) => {
|
||||
const shouldToggleColorbar = hasColorbar && !isEmbedded;
|
||||
|
||||
if (isOpen && shouldToggleColorbar && openState) {
|
||||
toggleColorbar();
|
||||
onClose?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOpen && openState && shouldToggleColorbar) {
|
||||
toggleColorbar();
|
||||
return;
|
||||
}
|
||||
|
||||
if (openState) {
|
||||
viewportActionCornersAPI.openItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
|
||||
onOpen?.();
|
||||
} else {
|
||||
viewportActionCornersAPI.closeItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
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 { align, side } = toolbarService.getAlignAndSide(location);
|
||||
|
||||
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
|
||||
|
||||
@@ -66,25 +78,41 @@ export function WindowLevelActionMenuWrapper({
|
||||
return null;
|
||||
}
|
||||
|
||||
const Icon =
|
||||
hasColorbar && !isEmbedded && hasEmbeddedVariantToUse ? (
|
||||
<Icons.Close className={iconClassName} />
|
||||
) : (
|
||||
<Icons.ViewportWindowLevel className={iconClassName} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={isMenuOpen}
|
||||
open={isOpen}
|
||||
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'
|
||||
<div>
|
||||
{IconContainer ? (
|
||||
<IconContainer
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
{...containerProps}
|
||||
>
|
||||
{Icon}
|
||||
</IconContainer>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={disabled}
|
||||
>
|
||||
{Icon}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
<Icons.ByName name="viewport-window-level" />
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="border-none bg-transparent p-0 shadow-none"
|
||||
@@ -94,45 +122,12 @@ export function WindowLevelActionMenuWrapper({
|
||||
sideOffset={5}
|
||||
>
|
||||
<WindowLevelActionMenu
|
||||
viewportId={viewportId}
|
||||
viewportId={viewportIdToUse}
|
||||
element={element}
|
||||
presets={displaySetPresets}
|
||||
horizontalDirection={horizontalDirection}
|
||||
verticalDirection={verticalDirection}
|
||||
colorbarProperties={colorbarProperties}
|
||||
displaySets={displaySets}
|
||||
volumeRenderingPresets={volumeRenderingPresets}
|
||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||
align={align}
|
||||
side={side}
|
||||
/>
|
||||
</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 };
|
||||
};
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import { WindowLevelActionMenuWrapper } from './WindowLevelActionMenuWrapper';
|
||||
|
||||
export function getWindowLevelActionMenu(
|
||||
props: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
location: string;
|
||||
displaySets: Array<any>;
|
||||
}>
|
||||
) {
|
||||
return <WindowLevelActionMenuWrapper {...props} />;
|
||||
}
|
||||
Reference in new issue
Block a user