ui(components): New Dialog and Modal components for ui-next (#4772)

Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
Dan Rukasandsedghi authored and GitHub committed 2025-02-28 15:17:55 -05:00
1 parent 62b4fa3e89
commit a3b51aa8db
90 files changed
+3459 -1357

No files matched your search

@@ -146,6 +146,11 @@ const OHIFCornerstoneViewport = React.memo(
const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (!viewportInfo) {
return;
}
setEnabledElement(viewportId, element);
setEnabledVPElement(element);
@@ -323,10 +328,10 @@ const OHIFCornerstoneViewport = React.memo(
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
const windowLevelActionMenu = customizationService.getCustomization(
'viewportActionMenu.windowLevelActionMenu'
'viewportActionMenu.windowLevelActionMenu'
);
const segmentationOverlay = customizationService.getCustomization(
'viewportActionMenu.segmentationOverlay'
'viewportActionMenu.segmentationOverlay'
);
if (windowLevelActionMenu?.enabled) {
@@ -362,13 +367,7 @@ const OHIFCornerstoneViewport = React.memo(
location: segmentationOverlay.location,
});
}
}, [
displaySets,
viewportId,
viewportActionCornersService,
servicesManager,
commandsManager,
]);
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
const { ref: resizeRef } = useResizeDetector({
onResize,
+77 -75
View File
@@ -18,21 +18,19 @@ import * as cornerstoneTools from '@cornerstonejs/tools';
import { Types as OhifTypes, utils } from '@ohif/core';
import i18n from '@ohif/i18n';
import {
callLabelAutocompleteDialog,
showLabelAnnotationPopup,
callInputDialogAutoComplete,
createReportAsync,
callInputDialog,
colorPickerDialog,
callInputDialog,
} from '@ohif/extension-default';
import { vec3, mat4 } from 'gl-matrix';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
import { toolNames } from './initCornerstoneTools';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
@@ -200,26 +198,55 @@ function commandsModule({
}
},
/**
* Show the measurement labelling input dialog and update the label
* on the measurement with a response if not cancelled.
* Common logic for handling measurement label updates through dialog
* @param uid - measurement uid
* @returns Promise that resolves when the label is updated
*/
setMeasurementLabel: ({ uid }) => {
_handleMeasurementLabelDialog: async uid => {
const labelConfig = customizationService.getCustomization('measurementLabels');
const renderContent = customizationService.getCustomization('ui.labellingComponent');
const measurement = measurementService.getMeasurement(uid);
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, renderContent).then(
(val: Map<any, any>) => {
measurementService.update(
uid,
{
...val,
},
true
);
}
);
},
if (!measurement) {
console.debug('No measurement found for label editing');
return;
}
if (!labelConfig) {
const label = await callInputDialog({
uiDialogService,
title: 'Edit Measurement Label',
placeholder: measurement.label || 'Enter new label',
defaultValue: measurement.label,
});
if (label !== undefined && label !== null) {
measurementService.update(uid, { ...measurement, label }, true);
}
return;
}
const val = await callInputDialogAutoComplete({
measurement,
uiDialogService,
labelConfig,
renderContent,
});
if (val !== undefined && val !== null) {
measurementService.update(uid, { ...val }, true);
}
},
/**
* Show the measurement labelling input dialog and update the label
* on the measurement with a response if not cancelled.
*/
setMeasurementLabel: async ({ uid }) => {
await actions._handleMeasurementLabelDialog(uid);
},
renameMeasurement: async ({ uid }) => {
await actions._handleMeasurementLabelDialog(uid);
},
/**
*
* @param props - containing the updates to apply
@@ -321,23 +348,6 @@ function commandsModule({
measurementService.remove(uid);
},
renameMeasurement: ({ uid }) => {
const labelConfig = customizationService.getCustomization('measurementLabels');
const renderContent = customizationService.getCustomization('ui.labellingComponent');
const measurement = measurementService.getMeasurement(uid);
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, renderContent).then(
val => {
measurementService.update(
uid,
{
...val,
},
true
);
}
);
},
toggleLockMeasurement: ({ uid }) => {
measurementService.toggleLockMeasurement(uid);
},
@@ -375,10 +385,10 @@ function commandsModule({
viewportGridService.setActiveViewportId(viewportId);
},
arrowTextCallback: ({ callback, data, uid }) => {
arrowTextCallback: ({ callback }) => {
const labelConfig = customizationService.getCustomization('measurementLabels');
const renderContent = customizationService.getCustomization('ui.labellingComponent');
callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig, renderContent);
callInputDialogAutoComplete(uiDialogService, callback, {}, labelConfig, renderContent);
},
toggleCine: () => {
const { viewports } = viewportGridService.getState();
@@ -568,6 +578,7 @@ function commandsModule({
],
});
},
// capture viewport
showDownloadViewportModal: () => {
const { activeViewportId } = viewportGridService.getState();
@@ -589,10 +600,9 @@ function commandsModule({
title: 'Download High Quality Image',
contentProps: {
activeViewportId,
onClose: uiModalService.hide,
cornerstoneViewportService,
},
containerDimensions: 'w-[70%] max-w-[900px]',
containerClassName: 'max-w-4xl p-4',
});
}
},
@@ -1138,14 +1148,12 @@ function commandsModule({
*/
storeSegmentationCommand: async ({ segmentationId }) => {
const { segmentationService, viewportGridService } = servicesManager.services;
const datasources = extensionManager.getActiveDataSource();
const displaySetInstanceUIDs = await createReportAsync({
servicesManager,
getReport: () =>
commandsManager.runCommand('storeSegmentation', {
segmentationId,
dataSource: datasources[0],
}),
reportType: 'Segmentation',
});
@@ -1271,7 +1279,7 @@ function commandsModule({
segmentationService.setStyle({ type }, { fillAlphaInactive: value });
},
editSegmentLabel: ({ segmentationId, segmentIndex }) => {
editSegmentLabel: async ({ segmentationId, segmentIndex }) => {
const { segmentationService, uiDialogService } = servicesManager.services;
const segmentation = segmentationService.getSegmentation(segmentationId);
@@ -1280,19 +1288,14 @@ function commandsModule({
}
const segment = segmentation.segments[segmentIndex];
const { label } = segment;
const callback = (label, actionId) => {
if (label === '') {
return;
}
callInputDialog({
uiDialogService,
title: 'Edit Segment Label',
placeholder: 'Enter new label',
defaultValue: segment.label,
}).then(label => {
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
};
callInputDialog(uiDialogService, label, callback, false, {
dialogTitle: 'Edit Segment Label',
inputLabel: 'Enter new label',
});
},
@@ -1306,17 +1309,13 @@ function commandsModule({
const { label } = segmentation;
const callback = (label, actionId) => {
if (label === '') {
return;
}
callInputDialog({
uiDialogService,
title: 'Edit Segmentation Label',
placeholder: 'Enter new label',
defaultValue: label,
}).then(label => {
segmentationService.addOrUpdateSegmentation({ segmentationId, label });
};
callInputDialog(uiDialogService, label, callback, false, {
dialogTitle: 'Edit Segmentation Label',
inputLabel: 'Enter new label',
});
},
@@ -1333,13 +1332,16 @@ function commandsModule({
a: color[3] / 255.0,
};
colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
if (actionId === 'cancel') {
return;
}
const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
uiDialogService.show({
content: colorPickerDialog,
title: 'Segment Color',
contentProps: {
value: rgbaColor,
onSave: newRgbaColor => {
const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
},
},
});
},
@@ -1395,6 +1397,9 @@ function commandsModule({
setMeasurementLabel: {
commandFn: actions.setMeasurementLabel,
},
renameMeasurement: {
commandFn: actions.renameMeasurement,
},
updateMeasurement: {
commandFn: actions.updateMeasurement,
},
@@ -1407,9 +1412,6 @@ function commandsModule({
removeMeasurement: {
commandFn: actions.removeMeasurement,
},
renameMeasurement: {
commandFn: actions.renameMeasurement,
},
toggleLockMeasurement: {
commandFn: actions.toggleLockMeasurement,
},
@@ -10,21 +10,19 @@ export function VolumeRenderingPresets({
commandsManager,
volumeRenderingPresets,
}: VolumeRenderingPresetsProps): ReactElement {
const { uiModalService } = servicesManager.services;
const { uiDialogService } = servicesManager.services;
const onClickPresets = () => {
uiModalService.show({
uiDialogService.show({
id: 'volume-rendering-presets',
content: VolumeRenderingPresetsContent,
title: 'Rendering Presets',
movable: true,
isDraggable: true,
contentProps: {
onClose: uiModalService.hide,
presets: volumeRenderingPresets,
viewportId,
commandsManager,
},
containerDimensions: 'h-[543px] w-[460px]',
contentDimensions: 'h-[493px] w-[460px] pl-[12px] pr-[12px]',
});
};
@@ -1,29 +1,24 @@
import { ButtonEnums } from '@ohif/ui';
import { Icons } from '@ohif/ui-next';
import { Icons, FooterAction } from '@ohif/ui-next';
import React, { ReactElement, useState, useCallback } from 'react';
import { Button, InputFilterText } from '@ohif/ui';
import { PresetDialog } from '@ohif/ui-next';
import { ViewportPreset, VolumeRenderingPresetsContentProps } from '../../types/ViewportPresets';
interface Props extends VolumeRenderingPresetsContentProps {
hide: () => void;
}
export function VolumeRenderingPresetsContent({
presets,
viewportId,
commandsManager,
onClose,
}: VolumeRenderingPresetsContentProps): ReactElement {
const [filteredPresets, setFilteredPresets] = useState(presets);
hide,
}: Props): ReactElement {
const [searchValue, setSearchValue] = useState('');
const [selectedPreset, setSelectedPreset] = useState<ViewportPreset | null>(null);
const handleSearchChange = useCallback(
(value: string) => {
setSearchValue(value);
const filtered = value
? presets.filter(preset => preset.name.toLowerCase().includes(value.toLowerCase()))
: presets;
setFilteredPresets(filtered);
},
[presets]
);
const handleSearchChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(event.target.value);
}, []);
const handleApply = useCallback(
props => {
@@ -34,62 +29,54 @@ export function VolumeRenderingPresetsContent({
[commandsManager]
);
const filteredPresets = searchValue
? presets.filter(preset => preset.name.toLowerCase().includes(searchValue.toLowerCase()))
: presets;
const formatLabel = (label: string, maxChars: number) => {
return label.length > maxChars ? `${label.slice(0, maxChars)}...` : label;
};
return (
<div className="flex min-h-full w-full flex-col justify-between">
<div className="border-secondary-light h-[433px] w-full overflow-hidden rounded border bg-black px-2.5">
<div className="flex h-[46px] w-full items-center justify-start">
<div className="h-[26px] w-[200px]">
<InputFilterText
value={searchValue}
onDebounceChange={handleSearchChange}
placeholder={'Search all'}
/>
</div>
</div>
<div className="ohif-scrollbar overflow h-[385px] w-full overflow-y-auto">
<div className="grid grid-cols-4 gap-3 pt-2 pr-3">
{filteredPresets.map((preset, index) => (
<div
key={index}
className="flex cursor-pointer flex-col items-start"
onClick={() => {
setSelectedPreset(preset);
handleApply({ preset: preset.name, viewportId });
}}
>
<Icons.ByName
name={preset.name}
className={
selectedPreset?.name === preset.name
? 'border-primary-light h-[75px] w-[95px] max-w-none rounded border-2'
: 'hover:border-primary-light h-[75px] w-[95px] max-w-none rounded border-2 border-black'
}
/>
<label className="text-aqua-pale mt-2 text-left text-xs">
{formatLabel(preset.name, 11)}
</label>
</div>
))}
</div>
</div>
</div>
<footer className="flex h-[60px] w-full items-center justify-end">
<div className="flex">
<Button
name="Cancel"
size={ButtonEnums.size.medium}
type={ButtonEnums.type.secondary}
onClick={onClose}
>
{' '}
Cancel{' '}
</Button>
</div>
</footer>
</div>
<PresetDialog className="h-[500px]">
<PresetDialog.PresetBody>
<PresetDialog.PresetFilter>
<PresetDialog.PresetSearch
value={searchValue}
onChange={handleSearchChange}
placeholder="Search all"
/>
</PresetDialog.PresetFilter>
<PresetDialog.PresetGrid>
{filteredPresets.map((preset, index) => (
<div
key={index}
className="flex cursor-pointer flex-col items-start"
onClick={() => {
setSelectedPreset(preset);
handleApply({ preset: preset.name, viewportId });
}}
>
<Icons.ByName
name={preset.name}
className={
selectedPreset?.name === preset.name
? 'border-highlight h-[75px] w-[95px] max-w-none rounded border-2'
: 'hover:border-highlight h-[75px] w-[95px] max-w-none rounded border-2 border-black'
}
/>
<label className="text-muted-foreground mt-1 text-left text-xs">
{formatLabel(preset.name, 11)}
</label>
</div>
))}
</PresetDialog.PresetGrid>
</PresetDialog.PresetBody>
<FooterAction className="mt-4 flex-shrink-0">
<FooterAction.Right>
<FooterAction.Secondary onClick={hide}>Cancel</FooterAction.Secondary>
</FooterAction.Right>
</FooterAction>
</PresetDialog>
);
}
@@ -21,8 +21,9 @@ export function getWindowLevelActionMenu({
const { volumeRenderingPresets, volumeRenderingQualityRange } =
customizationService.getCustomization('cornerstone.3dVolumeRendering');
const WindowLevelActionMenu = customizationService.getCustomization(
'cornerstone.windowLevelActionMenu'
'viewportActionMenu.windowLevelActionMenu'
);
console.debug('🚀 ~ WindowLevelActionMenu:', WindowLevelActionMenu);
const displaySetPresets = displaySets
.filter(displaySet => presets[displaySet.Modality])
.map(displaySet => {
@@ -37,8 +38,10 @@ export function getWindowLevelActionMenu({
return null;
}
const WindowLevelActionMenuComponent = WindowLevelActionMenu?.component;
return (
<WindowLevelActionMenu
<WindowLevelActionMenuComponent
viewportId={viewportId}
element={element}
presets={displaySetPresets}
@@ -0,0 +1,152 @@
import React, { useState, useEffect } from 'react';
import { ImageModal, FooterAction } from '@ohif/ui-next';
const MAX_TEXTURE_SIZE = 10000;
const DEFAULT_FILENAME = 'image';
interface ViewportDownloadFormNewProps {
onClose: () => void;
defaultSize: number;
fileTypeOptions: Array<{ value: string; label: string }>;
viewportId: string;
showAnnotations: boolean;
onAnnotationsChange: (show: boolean) => void;
dimensions: { width: number; height: number };
onDimensionsChange: (dimensions: { width: number; height: number }) => void;
onEnableViewport: (element: HTMLElement) => void;
onDisableViewport: () => void;
onDownload: (filename: string, fileType: string) => void;
warningState: { enabled: boolean; value: string };
}
function ViewportDownloadFormNew({
onClose,
defaultSize,
fileTypeOptions,
viewportId,
showAnnotations,
onAnnotationsChange,
dimensions,
warningState,
onDimensionsChange,
onEnableViewport,
onDisableViewport,
onDownload,
}: ViewportDownloadFormNewProps) {
const [viewportElement, setViewportElement] = useState<HTMLElement | null>(null);
const [showWarningMessage, setShowWarningMessage] = useState(true);
const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg');
useEffect(() => {
if (!viewportElement) {
return;
}
onEnableViewport(viewportElement);
return () => {
onDisableViewport();
};
}, [onDisableViewport, onEnableViewport, viewportElement]);
return (
<ImageModal>
<ImageModal.Body>
<ImageModal.ImageVisual>
<div
style={{
height: dimensions.height,
width: dimensions.width,
position: 'relative',
}}
data-viewport-uid={viewportId}
ref={setViewportElement}
>
{warningState.enabled && showWarningMessage && (
<div
className="text-foreground absolute left-1/2 bottom-[5px] z-[1000] -translate-x-1/2 whitespace-nowrap rounded bg-black p-3 text-xs font-bold"
style={{
fontSize: '12px',
}}
>
{warningState.value}
</div>
)}
</div>
</ImageModal.ImageVisual>
<ImageModal.ImageOptions>
<div className="flex items-end space-x-2">
<ImageModal.Filename
value={filename}
onChange={e => setFilename(e.target.value)}
>
File name
</ImageModal.Filename>
<ImageModal.Filetype
selected={fileType}
onSelect={setFileType}
options={fileTypeOptions}
/>
</div>
<ImageModal.ImageSize
width={dimensions.width.toString()}
height={dimensions.height.toString()}
onWidthChange={e => {
onDimensionsChange({
...dimensions,
width: parseInt(e.target.value) || defaultSize,
});
}}
onHeightChange={e => {
onDimensionsChange({
...dimensions,
height: parseInt(e.target.value) || defaultSize,
});
}}
maxWidth={MAX_TEXTURE_SIZE.toString()}
maxHeight={MAX_TEXTURE_SIZE.toString()}
>
Image size <span className="text-muted-foreground">px</span>
</ImageModal.ImageSize>
<ImageModal.SwitchOption
defaultChecked={showAnnotations}
checked={showAnnotations}
onCheckedChange={onAnnotationsChange}
>
Include annotations
</ImageModal.SwitchOption>
{warningState.enabled && (
<ImageModal.SwitchOption
defaultChecked={showWarningMessage}
checked={showWarningMessage}
onCheckedChange={setShowWarningMessage}
>
Include warning message
</ImageModal.SwitchOption>
)}
<FooterAction className="mt-2">
<FooterAction.Right>
<FooterAction.Secondary onClick={onClose}>Cancel</FooterAction.Secondary>
<FooterAction.Primary
onClick={() => {
onDownload(filename || DEFAULT_FILENAME, fileType);
onClose();
}}
>
Save
</FooterAction.Primary>
</FooterAction.Right>
</FooterAction>
</ImageModal.ImageOptions>
</ImageModal.Body>
</ImageModal>
);
}
export default {
'ohif.captureViewportModal': ViewportDownloadFormNew,
};
@@ -1,9 +1,11 @@
import viewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService';
import { WindowLevelActionMenu } from '../components/WindowLevelActionMenu/WindowLevelActionMenu';
export default {
'viewportActionMenu.windowLevelActionMenu': {
enabled: true,
location: viewportActionCornersService.LOCATIONS.topRight,
component: WindowLevelActionMenu,
},
'viewportActionMenu.segmentationOverlay': {
enabled: true,
@@ -0,0 +1,6 @@
export default {
'viewportDownload.warningMessage': {
enabled: true,
value: 'Not For Diagnostic Use',
},
};
@@ -1,5 +0,0 @@
import { WindowLevelActionMenu } from '../components/WindowLevelActionMenu/WindowLevelActionMenu';
export default {
'cornerstone.windowLevelActionMenu': WindowLevelActionMenu,
};
@@ -8,7 +8,8 @@ import volumeRenderingCustomization from './customizations/volumeRenderingCustom
import colorbarCustomization from './customizations/colorbarCustomization';
import windowLevelPresetsCustomization from './customizations/windowLevelPresetsCustomization';
import miscCustomization from './customizations/miscCustomization';
import windowLevelActionMenuCustomization from './customizations/windowLevelActionMenuCustomization';
import captureViewportModalCustomization from './customizations/captureViewportModalCustomization';
import viewportDownloadWarningCustomization from './customizations/viewportDownloadWarningCustomization';
import viewportActionMenuCustomizations from './customizations/viewportActionMenuCustomizations';
function getCustomizationModule({ commandsManager, servicesManager }) {
@@ -26,7 +27,8 @@ function getCustomizationModule({ commandsManager, servicesManager }) {
...colorbarCustomization,
...windowLevelPresetsCustomization,
...miscCustomization,
...windowLevelActionMenuCustomization,
...captureViewportModalCustomization,
...viewportDownloadWarningCustomization,
...viewportActionMenuCustomizations,
},
},
+2
View File
@@ -54,6 +54,7 @@ import PanelMeasurement from './panels/PanelMeasurement';
import DicomUpload from './components/DicomUpload/DicomUpload';
import { useSegmentations } from './hooks/useSegmentations';
import { StudySummaryFromMetadata } from './components/StudySummaryFromMetadata';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import utils from './utils';
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
@@ -255,6 +256,7 @@ export {
PanelMeasurement,
DicomUpload,
StudySummaryFromMetadata,
CornerstoneViewportDownloadForm,
utils,
};
export default cornerstoneExtension;
@@ -84,35 +84,14 @@ export function onCompletedCalibrationLine(
return;
}
callInputDialog(
callInputDialog({
uiDialogService,
{
text: '',
label: `${length}`,
},
(value, id) => {
if (id === 'save') {
adjustCalibration(Number.parseFloat(value));
resolve(true);
} else {
reject('cancel');
}
},
false,
{
dialogTitle: 'Calibration',
inputLabel: 'Actual Physical distance (mm)',
// the input value must be a number
validateFunc: val => {
try {
const v = Number.parseFloat(val);
return !isNaN(v) && v !== 0.0;
} catch {
return false;
}
},
}
);
title: 'Calibration',
placeholder: 'Actual Physical distance (mm)',
defaultValue: `${length}`,
}).then(newValue => {
adjustCalibration(Number.parseFloat(newValue));
resolve(true);
});
});
}
@@ -1,53 +1,66 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import html2canvas from 'html2canvas';
import {
Enums,
getEnabledElement,
getOrCreateCanvas,
StackViewport,
BaseVolumeViewport,
} from '@cornerstonejs/core';
import { getEnabledElement, StackViewport, BaseVolumeViewport } from '@cornerstonejs/core';
import { ToolGroupManager } from '@cornerstonejs/tools';
import { ViewportDownloadForm } from '@ohif/ui';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
import { useSystem } from '@ohif/core/src';
const MINIMUM_SIZE = 100;
const DEFAULT_SIZE = 512;
const MAX_TEXTURE_SIZE = 10000;
const VIEWPORT_ID = 'cornerstone-viewport-download-form';
const FILE_TYPE_OPTIONS = [
{
value: 'jpg',
label: 'JPG',
},
{
value: 'png',
label: 'PNG',
},
];
type ViewportDownloadFormProps = {
hide: () => void;
activeViewportId: string;
};
const CornerstoneViewportDownloadForm = ({
onClose,
hide,
activeViewportId: activeViewportIdProp,
cornerstoneViewportService,
}: withAppTypes) => {
const enabledElement = OHIFgetEnabledElement(activeViewportIdProp);
const activeViewportElement = enabledElement?.element;
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
}: ViewportDownloadFormProps) => {
const { servicesManager } = useSystem();
const { customizationService, cornerstoneViewportService } = servicesManager.services;
const [showAnnotations, setShowAnnotations] = useState(true);
const [viewportDimensions, setViewportDimensions] = useState({
width: DEFAULT_SIZE,
height: DEFAULT_SIZE,
});
const {
viewportId: activeViewportId,
renderingEngineId,
viewport: activeViewport,
} = activeViewportEnabledElement;
const warningState = customizationService.getCustomization('viewportDownload.warningMessage') as {
enabled: boolean;
value: string;
};
const refViewportEnabledElementOHIF = OHIFgetEnabledElement(activeViewportIdProp);
const activeViewportElement = refViewportEnabledElementOHIF?.element;
const { viewportId: activeViewportId, renderingEngineId } =
getEnabledElement(activeViewportElement);
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
const toolModeAndBindings = Object.keys(toolGroup.toolOptions).reduce((acc, toolName) => {
const tool = toolGroup.toolOptions[toolName];
const { mode, bindings } = tool;
return {
...acc,
[toolName]: {
mode,
bindings,
},
};
}, {});
useEffect(() => {
const toolModeAndBindings = Object.keys(toolGroup.toolOptions).reduce((acc, toolName) => {
const tool = toolGroup.toolOptions[toolName];
const { mode, bindings } = tool;
return {
...acc,
[toolName]: { mode, bindings },
};
}, {});
return () => {
Object.keys(toolModeAndBindings).forEach(toolName => {
const { mode, bindings } = toolModeAndBindings[toolName];
@@ -56,157 +69,92 @@ const CornerstoneViewportDownloadForm = ({
};
}, []);
const enableViewport = viewportElement => {
if (viewportElement) {
const { renderingEngine, viewport } = getEnabledElement(activeViewportElement);
const viewportInput = {
viewportId: VIEWPORT_ID,
element: viewportElement,
type: viewport.type,
defaultOptions: {
background: viewport.defaultOptions.background,
orientation: viewport.defaultOptions.orientation,
},
};
renderingEngine.enableElement(viewportInput);
}
};
const disableViewport = viewportElement => {
if (viewportElement) {
const { renderingEngine } = getEnabledElement(viewportElement);
return new Promise(resolve => {
renderingEngine.disableElement(VIEWPORT_ID);
});
}
};
const updateViewportPreview = (downloadViewportElement, internalCanvas, fileType) =>
new Promise(resolve => {
const enabledElement = getEnabledElement(downloadViewportElement);
const { viewport: downloadViewport, renderingEngine } = enabledElement;
// Note: Since any trigger of dimensions will update the viewport,
// we need to resize the offScreenCanvas to accommodate for the new
// dimensions, this is due to the reason that we are using the GPU offScreenCanvas
// to render the viewport for the downloadViewport.
renderingEngine.resize();
// Trigger the render on the viewport to update the on screen
// downloadViewport.resetCamera();
downloadViewport.render();
downloadViewportElement.addEventListener(
Enums.Events.IMAGE_RENDERED,
function updateViewport(event) {
const enabledElement = getEnabledElement(event.target);
const { viewport } = enabledElement;
const { element } = viewport;
const downloadCanvas = getOrCreateCanvas(element);
const type = 'image/' + fileType;
const dataUrl = downloadCanvas.toDataURL(type, 1);
let newWidth = element.offsetHeight;
let newHeight = element.offsetWidth;
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
newHeight *= multiplier;
newWidth *= multiplier;
}
resolve({ dataUrl, width: newWidth, height: newHeight });
downloadViewportElement.removeEventListener(Enums.Events.IMAGE_RENDERED, updateViewport);
// for some reason we need a reset camera here, and I don't know why
downloadViewport.resetCamera();
const presentation = activeViewport.getViewPresentation();
if (downloadViewport.setView) {
downloadViewport.setView(activeViewport.getViewReference(), presentation);
}
downloadViewport.render();
}
);
});
const loadImage = (activeViewportElement, viewportElement, width, height) =>
new Promise(resolve => {
if (activeViewportElement && viewportElement) {
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
if (!activeViewportEnabledElement) {
return;
}
const { viewport } = activeViewportEnabledElement;
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
if (downloadViewport instanceof StackViewport) {
const imageId = viewport.getCurrentImageId();
const properties = viewport.getProperties();
downloadViewport.setStack([imageId]).then(() => {
try {
downloadViewport.setProperties(properties);
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
resolve({ width: newWidth, height: newHeight });
} catch (e) {
// Happens on clicking the cancel button
console.warn('Unable to set properties', e);
}
});
} else if (downloadViewport instanceof BaseVolumeViewport) {
const actors = viewport.getActors();
// downloadViewport.setActors(actors);
actors.forEach(actor => {
downloadViewport.addActor(actor);
});
downloadViewport.render();
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
resolve({ width: newWidth, height: newHeight });
}
}
});
const toggleAnnotations = (toggle, viewportElement, activeViewportElement) => {
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
const downloadViewportElement = getEnabledElement(viewportElement);
const { viewportId: activeViewportId, renderingEngineId } = activeViewportEnabledElement;
const { viewportId: downloadViewportId } = downloadViewportElement;
if (!activeViewportEnabledElement || !downloadViewportElement) {
const handleEnableViewport = (viewportElement: HTMLElement) => {
if (!viewportElement) {
return;
}
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
const { viewport } = getEnabledElement(activeViewportElement);
// add the viewport to the toolGroup
const viewportInput = {
viewportId: VIEWPORT_ID,
element: viewportElement,
type: viewport.type,
defaultOptions: {
background: viewport.defaultOptions.background,
orientation: viewport.defaultOptions.orientation,
},
};
renderingEngine.enableElement(viewportInput);
};
const handleDisableViewport = async () => {
renderingEngine.disableElement(VIEWPORT_ID);
};
const handleLoadImage = async (width: number, height: number) => {
if (!activeViewportElement) {
return;
}
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
if (!activeViewportEnabledElement) {
return;
}
const { viewport } = activeViewportEnabledElement;
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
try {
if (downloadViewport instanceof StackViewport) {
const imageId = viewport.getCurrentImageId();
const properties = viewport.getProperties();
await downloadViewport.setStack([imageId]);
downloadViewport.setProperties(properties);
return {
width: Math.min(width || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
height: Math.min(height || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
};
} else if (downloadViewport instanceof BaseVolumeViewport) {
const volumeIds = viewport.getAllVolumeIds();
downloadViewport.setVolumes([{ volumeId: volumeIds[0] }]);
return {
width: Math.min(width || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
height: Math.min(height || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
};
}
} catch (error) {
console.error('Error loading image:', error);
}
};
const handleToggleAnnotations = (show: boolean) => {
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
if (!activeViewportEnabledElement) {
return;
}
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
if (!downloadViewport) {
return;
}
const { viewportId: activeViewportId, renderingEngineId } = activeViewportEnabledElement;
const { id: downloadViewportId } = downloadViewport;
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
toolGroup.addViewport(downloadViewportId, renderingEngineId);
Object.keys(toolGroup.getToolInstances()).forEach(toolName => {
// make all tools Enabled so that they can not be interacted with
// in the download viewport
if (toggle && toolName !== 'Crosshairs') {
if (show && toolName !== 'Crosshairs') {
try {
toolGroup.setToolEnabled(toolName);
} catch (e) {
console.log(e);
} catch (error) {
console.debug('Error enabling tool:', error);
}
} else {
toolGroup.setToolDisabled(toolName);
@@ -214,33 +162,50 @@ const CornerstoneViewportDownloadForm = ({
});
};
const downloadBlob = (filename, fileType) => {
const file = `${filename}.${fileType}`;
useEffect(() => {
if (viewportDimensions.width && viewportDimensions.height) {
setTimeout(() => {
handleLoadImage(viewportDimensions.width, viewportDimensions.height);
handleToggleAnnotations(showAnnotations);
}, 100);
}
}, [viewportDimensions, showAnnotations]);
const handleDownload = async (filename: string, fileType: string) => {
const divForDownloadViewport = document.querySelector(
`div[data-viewport-uid="${VIEWPORT_ID}"]`
);
html2canvas(divForDownloadViewport).then(canvas => {
const link = document.createElement('a');
link.download = file;
link.href = canvas.toDataURL(fileType, 1.0);
link.click();
});
if (!divForDownloadViewport) {
console.debug('No viewport found for download');
return;
}
const canvas = await html2canvas(divForDownloadViewport as HTMLElement);
const link = document.createElement('a');
link.download = `${filename}.${fileType}`;
link.href = canvas.toDataURL(`image/${fileType}`, 1.0);
link.click();
};
const ViewportDownloadFormNew = customizationService.getCustomization(
'ohif.captureViewportModal'
);
return (
<ViewportDownloadForm
onClose={onClose}
minimumSize={MINIMUM_SIZE}
maximumSize={MAX_TEXTURE_SIZE}
<ViewportDownloadFormNew
onClose={hide}
defaultSize={DEFAULT_SIZE}
activeViewportElement={activeViewportElement}
enableViewport={enableViewport}
disableViewport={disableViewport}
updateViewportPreview={updateViewportPreview}
loadImage={loadImage}
toggleAnnotations={toggleAnnotations}
downloadBlob={downloadBlob}
fileTypeOptions={FILE_TYPE_OPTIONS}
viewportId={VIEWPORT_ID}
showAnnotations={showAnnotations}
onAnnotationsChange={setShowAnnotations}
dimensions={viewportDimensions}
onDimensionsChange={setViewportDimensions}
onEnableViewport={handleEnableViewport}
onDisableViewport={handleDisableViewport}
onDownload={handleDownload}
warningState={warningState}
/>
);
};