import { getEnabledElement, StackViewport, VolumeViewport, utilities as csUtils, Enums as CoreEnums, Types as CoreTypes, BaseVolumeViewport, getRenderingEngines, } from '@cornerstonejs/core'; import { ToolGroupManager, Enums, utilities as cstUtils, annotation, Types as ToolTypes, } from '@cornerstonejs/tools'; import * as cornerstoneTools from '@cornerstonejs/tools'; import * as labelmapInterpolation from '@cornerstonejs/labelmap-interpolation'; import { ONNXSegmentationController } from '@cornerstonejs/ai'; import { Types as OhifTypes, utils } from '@ohif/core'; import i18n from '@ohif/i18n'; import { callInputDialogAutoComplete, createReportAsync, colorPickerDialog, callInputDialog, } from '@ohif/extension-default'; import { vec3, mat4 } from 'gl-matrix'; import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync'; import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection'; import { getViewportEnabledElement } from './utils/getViewportEnabledElement'; import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement'; import toggleVOISliceSync from './utils/toggleVOISliceSync'; import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores'; import { toolNames } from './initCornerstoneTools'; import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats'; import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport'; import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils'; import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; const { DefaultHistoryMemo } = csUtils.HistoryMemo; const toggleSyncFunctions = { imageSlice: toggleImageSliceSync, voi: toggleVOISliceSync, }; const { segmentation: segmentationUtils } = cstUtils; const getLabelmapTools = ({ toolGroupService }) => { const labelmapTools = []; const toolGroupIds = toolGroupService.getToolGroupIds(); toolGroupIds.forEach(toolGroupId => { const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId); const tools = toolGroup.getToolInstances(); // tools is an object with toolName as the key and tool as the value Object.keys(tools).forEach(toolName => { const tool = tools[toolName]; if ( tool instanceof cornerstoneTools.LabelmapBaseTool && tool.shouldResolvePreviewRequests() ) { labelmapTools.push(tool); } }); }); return labelmapTools; }; const getPreviewTools = ({ toolGroupService }) => { const labelmapTools = getLabelmapTools({ toolGroupService }); const previewTools = labelmapTools.filter(tool => tool.acceptPreview || tool.rejectPreview); return previewTools; }; const segmentAI = new ONNXSegmentationController({ autoSegmentMode: true, models: { sam_b: [ { name: 'sam-b-encoder', url: 'https://huggingface.co/schmuell/sam-b-fp16/resolve/main/sam_vit_b_01ec64.encoder-fp16.onnx', size: 180, key: 'encoder', }, { name: 'sam-b-decoder', url: 'https://huggingface.co/schmuell/sam-b-fp16/resolve/main/sam_vit_b_01ec64.decoder.onnx', size: 17, key: 'decoder', }, ], }, modelName: 'sam_b', }); let segmentAIEnabled = false; function commandsModule({ servicesManager, commandsManager, }: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule { const { viewportGridService, toolGroupService, cineService, uiDialogService, cornerstoneViewportService, uiNotificationService, measurementService, customizationService, colorbarService, hangingProtocolService, syncGroupService, segmentationService, displaySetService, } = servicesManager.services as AppTypes.Services; function _getActiveViewportEnabledElement() { return getActiveViewportEnabledElement(viewportGridService); } function _getViewportEnabledElement(viewportId: string) { return getViewportEnabledElement(viewportId); } function _getActiveViewportToolGroupId() { const viewport = _getActiveViewportEnabledElement(); return toolGroupService.getToolGroupForViewport(viewport.id); } function _getActiveSegmentationInfo() { const viewportId = viewportGridService.getActiveViewportId(); const activeSegmentation = segmentationService.getActiveSegmentation(viewportId); const segmentationId = activeSegmentation?.segmentationId; const activeSegmentIndex = segmentationService.getActiveSegment(viewportId).segmentIndex; return { segmentationId, segmentIndex: activeSegmentIndex, }; } const actions = { hydrateSecondaryDisplaySet: async ({ displaySet, viewportId }) => { if (!displaySet) { return; } if (displaySet.isOverlayDisplaySet) { // update the previously stored segmentationPresentation with the new viewportId // presentation so that when we put the referencedDisplaySet back in the viewport // it will have the correct segmentation representation hydrated commandsManager.runCommand('updateStoredSegmentationPresentation', { displaySet, type: displaySet.Modality === 'SEG' ? SegmentationRepresentations.Labelmap : SegmentationRepresentations.Contour, }); } const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID; const storePositionPresentation = refDisplaySet => { // update the previously stored positionPresentation with the new viewportId // presentation so that when we put the referencedDisplaySet back in the viewport // it will be in the correct position zoom and pan commandsManager.runCommand('updateStoredPositionPresentation', { viewportId, displaySetInstanceUIDs: [refDisplaySet.displaySetInstanceUID], }); }; if (displaySet.Modality === 'SEG' || displaySet.Modality === 'RTSTRUCT') { const referencedDisplaySet = displaySetService.getDisplaySetByUID( referencedDisplaySetInstanceUID ); storePositionPresentation(referencedDisplaySet); return commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', { viewportId, displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID], }); } else if (displaySet.Modality === 'SR') { const results = commandsManager.runCommand('hydrateStructuredReport', { displaySetInstanceUID: displaySet.displaySetInstanceUID, }); const { SeriesInstanceUIDs } = results; const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( SeriesInstanceUIDs[0] ); referencedDisplaySets.forEach(storePositionPresentation); if (referencedDisplaySets.length) { actions.setDisplaySetsForViewports({ viewportsToUpdate: [ { viewportId: viewportGridService.getActiveViewportId(), displaySetInstanceUIDs: [referencedDisplaySets[0].displaySetInstanceUID], }, ], }); } return results; } }, runSegmentBidirectional: async ({ segmentationId, segmentIndex } = {}) => { // Get active segmentation if not specified const targetSegmentation = segmentationId && segmentIndex ? { segmentationId, segmentIndex } : _getActiveSegmentationInfo(); const { segmentationId: targetId, segmentIndex: targetIndex } = targetSegmentation; // Get bidirectional measurement data const bidirectionalData = await cstUtils.segmentation.getSegmentLargestBidirectional({ segmentationId: targetId, segmentIndices: [targetIndex], }); const activeViewportId = viewportGridService.getActiveViewportId(); // Process each bidirectional measurement bidirectionalData.forEach(measurement => { const { segmentIndex, majorAxis, minorAxis } = measurement; // Create annotation const annotation = cornerstoneTools.SegmentBidirectionalTool.hydrate( activeViewportId, [majorAxis, minorAxis], { segmentIndex, segmentationId: targetId, } ); measurement.annotationUID = annotation.annotationUID; // Update segmentation stats const updatedSegmentation = updateSegmentBidirectionalStats({ segmentationId: targetId, segmentIndex: targetIndex, bidirectionalData: measurement, segmentationService, annotation, }); // Save changes if needed if (updatedSegmentation) { segmentationService.addOrUpdateSegmentation({ segmentationId: targetId, segments: updatedSegmentation.segments, }); } }); // get the active segmentIndex bidirectional annotation and jump to it const activeBidirectional = bidirectionalData.find( measurement => measurement.segmentIndex === targetIndex ); commandsManager.run('jumpToMeasurement', { uid: activeBidirectional.annotationUID, }); }, interpolateLabelmap: () => { const { segmentationId, segmentIndex } = _getActiveSegmentationInfo(); labelmapInterpolation.interpolate({ segmentationId, segmentIndex, }); }, /** * Generates the selector props for the context menu, specific to * the cornerstone viewport, and then runs the context menu. */ showCornerstoneContextMenu: options => { const element = _getActiveViewportEnabledElement()?.viewport?.element; const optionsToUse = { ...options, element }; const { useSelectedAnnotation, nearbyToolData, event } = optionsToUse; // This code is used to invoke the context menu via keyboard shortcuts if (useSelectedAnnotation && !nearbyToolData) { const firstAnnotationSelected = getFirstAnnotationSelected(element); // filter by allowed selected tools from config property (if there is any) const isToolAllowed = !optionsToUse.allowedSelectedTools || optionsToUse.allowedSelectedTools.includes(firstAnnotationSelected?.metadata?.toolName); if (isToolAllowed) { optionsToUse.nearbyToolData = firstAnnotationSelected; } else { return; } } optionsToUse.defaultPointsPosition = []; // if (optionsToUse.nearbyToolData) { // optionsToUse.defaultPointsPosition = commandsManager.runCommand( // 'getToolDataActiveCanvasPoints', // { toolData: optionsToUse.nearbyToolData } // ); // } // TODO - make the selectorProps richer by including the study metadata and display set. optionsToUse.selectorProps = { toolName: optionsToUse.nearbyToolData?.metadata?.toolName, value: optionsToUse.nearbyToolData, uid: optionsToUse.nearbyToolData?.annotationUID, nearbyToolData: optionsToUse.nearbyToolData, event, ...optionsToUse.selectorProps, }; commandsManager.run(options, optionsToUse); }, updateStoredSegmentationPresentation: ({ displaySet, type }) => { const { addSegmentationPresentationItem } = useSegmentationPresentationStore.getState(); const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID; addSegmentationPresentationItem(referencedDisplaySetInstanceUID, { segmentationId: displaySet.displaySetInstanceUID, hydrated: true, type, }); }, updateStoredPositionPresentation: ({ viewportId, displaySetInstanceUIDs, referencedImageId, options, }) => { const presentations = cornerstoneViewportService.getPresentations(viewportId); const { positionPresentationStore, setPositionPresentation, getPositionPresentationId } = usePositionPresentationStore.getState(); // Look inside positionPresentationStore and find the key that includes ALL the displaySetInstanceUIDs // and the value has viewportId as activeViewportId. let previousReferencedDisplaySetStoreKey; if ( displaySetInstanceUIDs && Array.isArray(displaySetInstanceUIDs) && displaySetInstanceUIDs.length > 0 ) { previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find( ([key, value]) => { return ( displaySetInstanceUIDs.every(uid => key.includes(uid)) && value.viewportId === viewportId ); } )?.[0]; } // Create presentation data with referencedImageId and options if provided const presentationData = referencedImageId ? { ...presentations.positionPresentation, viewReference: { referencedImageId, ...options, }, } : presentations.positionPresentation; if (previousReferencedDisplaySetStoreKey) { setPositionPresentation(previousReferencedDisplaySetStoreKey, presentationData); return; } // if not found means we have not visited that referencedDisplaySetInstanceUID before // so we need to grab the positionPresentationId directly from the store, // Todo: this is really hacky, we should have a better way for this const positionPresentationId = getPositionPresentationId({ displaySetInstanceUIDs, viewportId, }); setPositionPresentation(positionPresentationId, presentationData); }, getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) { return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates); }, getNearbyAnnotation({ element, canvasCoordinates }) { const nearbyToolData = actions.getNearbyToolData({ nearbyToolData: null, element, canvasCoordinates, }); const isAnnotation = toolName => { const enabledElement = getEnabledElement(element); if (!enabledElement) { return; } const { renderingEngineId, viewportId } = enabledElement; const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId, renderingEngineId); const toolInstance = toolGroup.getToolInstance(toolName); return toolInstance?.constructor?.isAnnotation ?? true; }; return nearbyToolData?.metadata?.toolName && isAnnotation(nearbyToolData.metadata.toolName) ? nearbyToolData : null; }, /** * Common logic for handling measurement label updates through dialog * @param uid - measurement uid * @returns Promise that resolves when the label is updated */ _handleMeasurementLabelDialog: async uid => { const labelConfig = customizationService.getCustomization('measurementLabels'); const renderContent = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(uid); 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, { ...measurement, label: 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 * @param props.measurementKey - chooses the measurement key to apply the * code to. This will typically be finding or site to apply a * finding code or a findingSites code. * @param props.code - A coding scheme value from DICOM, including: * * CodeValue - the language independent code, for example '1234' * * CodingSchemeDesignator - the issue of the code value * * CodeMeaning - the text value shown to the user * * ref - a string reference in the form `:` * * type - defaulting to 'finding'. Will replace other codes of same type * * style - a styling object to use * * Other fields * Note it is a valid option to remove the finding or site values by * supplying null for the code. * @param props.uid - the measurement UID to find it with * @param props.label - the text value for the code. Has NOTHING to do with * the measurement label, which can be set with textLabel * @param props.textLabel is the measurement label to apply. Set to null to * delete. * * If the measurementKey is `site`, then the code will also be added/replace * the 0 element of findingSites. This behaviour is expected to be enhanced * in the future with ability to set other site information. */ updateMeasurement: props => { const { code, uid, textLabel, label } = props; let { style } = props; const measurement = measurementService.getMeasurement(uid); if (!measurement) { console.warn('No measurement found to update', uid); return; } const updatedMeasurement = { ...measurement, }; // Call it textLabel as the label value // TODO - remove the label setting when direct rendering of findingSites is enabled if (textLabel !== undefined) { updatedMeasurement.label = textLabel; } if (code !== undefined) { const measurementKey = code.type || 'finding'; if (code.ref && !code.CodeValue) { const split = code.ref.indexOf(':'); code.CodeValue = code.ref.substring(split + 1); code.CodeMeaning = code.text || label; code.CodingSchemeDesignator = code.ref.substring(0, split); } updatedMeasurement[measurementKey] = code; if (measurementKey !== 'finding') { if (updatedMeasurement.findingSites) { updatedMeasurement.findingSites = updatedMeasurement.findingSites.filter( it => it.type !== measurementKey ); updatedMeasurement.findingSites.push(code); } else { updatedMeasurement.findingSites = [code]; } } } style ||= updatedMeasurement.finding?.style; style ||= updatedMeasurement.findingSites?.find(site => site?.style)?.style; if (style) { // Reset the selected values to preserve appearance on selection style.lineDashSelected ||= style.lineDash; annotation.config.style.setAnnotationStyles(measurement.uid, style); // this is a bit ugly, but given the underlying behavior, this is how it needs to work. switch (measurement.toolName) { case toolNames.PlanarFreehandROI: { const targetAnnotation = annotation.state.getAnnotation(measurement.uid); targetAnnotation.data.isOpenUShapeContour = !!style.isOpenUShapeContour; break; } default: break; } } measurementService.update(updatedMeasurement.uid, updatedMeasurement, true); }, /** * Jumps to the specified (by uid) measurement in the active viewport. * Also marks any provided display measurements isActive value */ jumpToMeasurement: ({ uid, displayMeasurements = [] }) => { measurementService.jumpToMeasurement(viewportGridService.getActiveViewportId(), uid); for (const measurement of displayMeasurements) { measurement.isActive = measurement.uid === uid; } }, removeMeasurement: ({ uid }) => { if (Array.isArray(uid)) { measurementService.removeMany(uid); } else { measurementService.remove(uid); } }, toggleLockMeasurement: ({ uid }) => { measurementService.toggleLockMeasurement(uid); }, toggleVisibilityMeasurement: ({ uid, items, visibility }) => { if (visibility === undefined && items?.length) { visibility = !items[0].isVisible; } if (Array.isArray(uid)) { measurementService.toggleVisibilityMeasurementMany(uid, visibility); } else { measurementService.toggleVisibilityMeasurement(uid, visibility); } }, /** * Download the CSV report for the measurements. */ downloadCSVMeasurementsReport: ({ measurementFilter }) => { utils.downloadCSVReport(measurementService.getMeasurements(measurementFilter)); }, downloadCSVSegmentationReport: ({ segmentationId }) => { const segmentation = segmentationService.getSegmentation(segmentationId); const { representationData } = segmentation; const { Labelmap } = representationData; const { referencedImageIds } = Labelmap; const firstImageId = referencedImageIds[0]; // find displaySet for firstImageId const displaySet = displaySetService .getActiveDisplaySets() .find(ds => ds.imageIds?.some(i => i === firstImageId)); const { SeriesNumber, SeriesInstanceUID, StudyInstanceUID, SeriesDate, SeriesTime, SeriesDescription, } = displaySet; const additionalInfo = { reference: { SeriesNumber, SeriesInstanceUID, StudyInstanceUID, SeriesDate, SeriesTime, SeriesDescription, }, }; generateSegmentationCSVReport(segmentation, additionalInfo); }, // Retrieve value commands getActiveViewportEnabledElement: _getActiveViewportEnabledElement, setViewportActive: ({ viewportId }) => { const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); if (!viewportInfo) { console.warn('No viewport found for viewportId:', viewportId); return; } viewportGridService.setActiveViewportId(viewportId); }, arrowTextCallback: async ({ callback }) => { const labelConfig = customizationService.getCustomization('measurementLabels'); const renderContent = customizationService.getCustomization('ui.labellingComponent'); const value = await callInputDialogAutoComplete({ uiDialogService, labelConfig, renderContent, }); callback?.(value); }, toggleCine: () => { const { viewports } = viewportGridService.getState(); const { isCineEnabled } = cineService.getState(); cineService.setIsCineEnabled(!isCineEnabled); viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false })); }, setViewportWindowLevel({ viewportId, windowWidth, windowCenter, displaySetInstanceUID, }: { viewportId: string; windowWidth: number; windowCenter: number; displaySetInstanceUID?: string; }) { // convert to numbers const windowWidthNum = Number(windowWidth); const windowCenterNum = Number(windowCenter); // get actor from the viewport const renderingEngine = cornerstoneViewportService.getRenderingEngine(); const viewport = renderingEngine.getViewport(viewportId); const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum); if (viewport instanceof BaseVolumeViewport) { const volumeId = actions.getVolumeIdForDisplaySet({ viewportId, displaySetInstanceUID, }); viewport.setProperties( { voiRange: { upper, lower, }, }, volumeId ); } else { viewport.setProperties({ voiRange: { upper, lower, }, }); } viewport.render(); }, toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => { const hasColorbar = colorbarService.hasColorbar(viewportId); if (hasColorbar) { colorbarService.removeColorbar(viewportId); return; } colorbarService.addColorbar(viewportId, displaySetInstanceUIDs, options); }, setWindowLevel(props) { const { toolGroupId } = props; const { viewportId } = _getActiveViewportEnabledElement(); const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId); if (toolGroupId && toolGroupId !== viewportToolGroupId) { return; } actions.setViewportWindowLevel({ ...props, viewportId }); }, setWindowLevelPreset: ({ presetName, presetIndex }) => { const windowLevelPresets = customizationService.getCustomization( 'cornerstone.windowLevelPresets' ); const activeViewport = viewportGridService.getActiveViewportId(); const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewport); const metadata = viewport.getImageData().metadata; const modality = metadata.Modality; if (!modality) { return; } const windowLevelPresetForModality = windowLevelPresets[modality]; if (!windowLevelPresetForModality) { return; } const windowLevelPreset = windowLevelPresetForModality[presetName] ?? Object.values(windowLevelPresetForModality)[presetIndex]; actions.setViewportWindowLevel({ viewportId: activeViewport, windowWidth: windowLevelPreset.window, windowCenter: windowLevelPreset.level, }); }, getVolumeIdForDisplaySet: ({ viewportId, displaySetInstanceUID }) => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); if (viewport instanceof BaseVolumeViewport) { const volumeIds = viewport.getAllVolumeIds(); const volumeId = volumeIds.find(id => id.includes(displaySetInstanceUID)); return volumeId; } return null; }, setToolEnabled: ({ toolName, toggle, toolGroupId }) => { const { viewports } = viewportGridService.getState(); if (!viewports.size) { return; } const toolGroup = toolGroupService.getToolGroup(toolGroupId ?? null); if (!toolGroup || !toolGroup.hasTool(toolName)) { return; } const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled; // Toggle the tool's state only if the toggle is true if (toggle) { toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName); } else { toolGroup.setToolEnabled(toolName); } const renderingEngine = cornerstoneViewportService.getRenderingEngine(); renderingEngine.render(); }, toggleEnabledDisabledToolbar({ value, itemId, toolGroupId }) { const toolName = itemId || value; toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId(); const toolGroup = toolGroupService.getToolGroup(toolGroupId); if (!toolGroup || !toolGroup.hasTool(toolName)) { return; } const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled; toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName); }, toggleActiveDisabledToolbar({ value, itemId, toolGroupId }) { const toolName = itemId || value; toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId(); const toolGroup = toolGroupService.getToolGroup(toolGroupId); if (!toolGroup || !toolGroup.hasTool(toolName)) { return; } const toolIsActive = [ Enums.ToolModes.Active, Enums.ToolModes.Enabled, Enums.ToolModes.Passive, ].includes(toolGroup.getToolOptions(toolName).mode); toolIsActive ? toolGroup.setToolDisabled(toolName) : actions.setToolActive({ toolName, toolGroupId }); // we should set the previously active tool to active after we set the // current tool disabled if (toolIsActive) { const prevToolName = toolGroup.getPrevActivePrimaryToolName(); if (prevToolName !== toolName) { actions.setToolActive({ toolName: prevToolName, toolGroupId }); } } }, setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [] }) => { // Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons) toolName = toolName || itemId || value; toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds(); toolGroupIds.forEach(toolGroupId => { actions.setToolActive({ toolName, toolGroupId }); }); }, setToolActive: ({ toolName, toolGroupId = null }) => { const { viewports } = viewportGridService.getState(); if (!viewports.size) { return; } const toolGroup = toolGroupService.getToolGroup(toolGroupId); if (!toolGroup) { return; } if (!toolGroup?.hasTool(toolName)) { return; } const activeToolName = toolGroup.getActivePrimaryMouseButtonTool(); if (activeToolName) { const activeToolOptions = toolGroup.getToolConfiguration(activeToolName); activeToolOptions?.disableOnPassive ? toolGroup.setToolDisabled(activeToolName) : toolGroup.setToolPassive(activeToolName); } // Set the new toolName to be active toolGroup.setToolActive(toolName, { bindings: [ { mouseButton: Enums.MouseBindings.Primary, }, ], }); }, // capture viewport showDownloadViewportModal: () => { const { activeViewportId } = viewportGridService.getState(); if (!cornerstoneViewportService.getCornerstoneViewport(activeViewportId)) { // Cannot download a non-cornerstone viewport (image). uiNotificationService.show({ title: 'Download Image', message: 'Image cannot be downloaded', type: 'error', }); return; } const { uiModalService } = servicesManager.services; if (uiModalService) { uiModalService.show({ content: CornerstoneViewportDownloadForm, title: 'Download High Quality Image', contentProps: { activeViewportId, cornerstoneViewportService, }, containerClassName: 'max-w-4xl p-4', }); } }, /** * Rotates the viewport by `rotation` relative to its current rotation. */ rotateViewportBy: ({ rotation, viewportId }: { rotation: number; viewportId?: string }) => { actions._rotateViewport({ rotation, viewportId, rotationMode: 'apply' }); }, /** * Sets the viewport rotation to an absolute value `rotation`. */ setViewportRotation: ({ rotation, viewportId }: { rotation: number; viewportId?: string }) => { actions._rotateViewport({ rotation, viewportId, rotationMode: 'set' }); }, flipViewportHorizontal: ({ viewportId, newValue = 'toggle', }: { viewportId?: string; newValue?: 'toggle' | boolean; }) => { const enabledElement = viewportId ? _getViewportEnabledElement(viewportId) : _getActiveViewportEnabledElement(); if (!enabledElement) { return; } const { viewport } = enabledElement; let flipHorizontal: boolean; if (newValue === 'toggle') { const { flipHorizontal: currentHorizontalFlip } = viewport.getCamera(); flipHorizontal = !currentHorizontalFlip; } else { flipHorizontal = newValue; } viewport.setCamera({ flipHorizontal }); viewport.render(); }, flipViewportVertical: ({ viewportId, newValue = 'toggle', }: { viewportId?: string; newValue?: 'toggle' | boolean; }) => { const enabledElement = viewportId ? _getViewportEnabledElement(viewportId) : _getActiveViewportEnabledElement(); if (!enabledElement) { return; } const { viewport } = enabledElement; let flipVertical: boolean; if (newValue === 'toggle') { const { flipVertical: currentVerticalFlip } = viewport.getCamera(); flipVertical = !currentVerticalFlip; } else { flipVertical = newValue; } viewport.setCamera({ flipVertical }); viewport.render(); }, invertViewport: ({ element }) => { let enabledElement; if (element === undefined) { enabledElement = _getActiveViewportEnabledElement(); } else { enabledElement = element; } if (!enabledElement) { return; } const { viewport } = enabledElement; const { invert } = viewport.getProperties(); viewport.setProperties({ invert: !invert }); viewport.render(); }, resetViewport: () => { const enabledElement = _getActiveViewportEnabledElement(); if (!enabledElement) { return; } const { viewport } = enabledElement; viewport.resetProperties?.(); viewport.resetCamera(); viewport.render(); }, scaleViewport: ({ direction }) => { const enabledElement = _getActiveViewportEnabledElement(); const scaleFactor = direction > 0 ? 0.9 : 1.1; if (!enabledElement) { return; } const { viewport } = enabledElement; if (viewport instanceof StackViewport) { if (direction) { const { parallelScale } = viewport.getCamera(); viewport.setCamera({ parallelScale: parallelScale * scaleFactor }); viewport.render(); } else { viewport.resetCamera(); viewport.render(); } } }, /** Jumps the active viewport or the specified one to the given slice index */ jumpToImage: ({ imageIndex, viewport: gridViewport }): void => { // Get current active viewport (return if none active) let viewport; if (!gridViewport) { const enabledElement = _getActiveViewportEnabledElement(); if (!enabledElement) { return; } viewport = enabledElement.viewport; } else { viewport = cornerstoneViewportService.getCornerstoneViewport(gridViewport.id); } // Get number of slices // -> Copied from cornerstone3D jumpToSlice\_getImageSliceData() let numberOfSlices = 0; if (viewport instanceof StackViewport) { numberOfSlices = viewport.getImageIds().length; } else if (viewport instanceof VolumeViewport) { numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport).numberOfSlices; } else { throw new Error('Unsupported viewport type'); } const jumpIndex = imageIndex < 0 ? numberOfSlices + imageIndex : imageIndex; if (jumpIndex >= numberOfSlices || jumpIndex < 0) { throw new Error(`Can't jump to ${imageIndex}`); } // Set slice to last slice const options = { imageIndex: jumpIndex }; csUtils.jumpToSlice(viewport.element, options); }, scroll: (options: ToolTypes.ScrollOptions) => { const enabledElement = _getActiveViewportEnabledElement(); // Allow either or direction for consistency in scroll implementation options.delta ??= options.direction || 1; options.direction ??= options.delta; if (!enabledElement) { return; } const { viewport } = enabledElement; csUtils.scroll(viewport, options); }, setViewportColormap: ({ viewportId, displaySetInstanceUID, colormap, opacity = 1, immediate = false, }) => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); let hpOpacity; // Retrieve active protocol's viewport match details const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol(); // Get display set options for the specified viewport ID const displaySetsInfo = viewportMatchDetails.get(viewportId)?.displaySetsInfo; if (displaySetsInfo) { // Find the display set that matches the given UID const matchingDisplaySet = displaySetsInfo.find( displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID ); // If a matching display set is found, update the opacity with its value hpOpacity = matchingDisplaySet?.displaySetOptions?.options?.colormap?.opacity; } // HP takes priority over the default opacity colormap = { ...colormap, opacity: hpOpacity || opacity }; if (viewport instanceof StackViewport) { viewport.setProperties({ colormap }); } if (viewport instanceof VolumeViewport) { if (!displaySetInstanceUID) { const { viewports } = viewportGridService.getState(); displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0]; } // ToDo: Find a better way of obtaining the volumeId that corresponds to the displaySetInstanceUID const volumeId = viewport .getAllVolumeIds() .find((_volumeId: string) => _volumeId.includes(displaySetInstanceUID)) ?? viewport.getVolumeId(); viewport.setProperties({ colormap }, volumeId); } if (immediate) { viewport.render(); } }, changeActiveViewport: ({ direction = 1 }) => { const { activeViewportId, viewports } = viewportGridService.getState(); const viewportIds = Array.from(viewports.keys()); const currentIndex = viewportIds.indexOf(activeViewportId); const nextViewportIndex = (currentIndex + direction + viewportIds.length) % viewportIds.length; viewportGridService.setActiveViewportId(viewportIds[nextViewportIndex] as string); }, /** * If the syncId is given and a synchronizer with that ID already exists, it will * toggle it on/off for the provided viewports. If not, it will attempt to create * a new synchronizer using the given syncId and type for the specified viewports. * If no viewports are provided, you may notice some default behavior. * - 'voi' type, we will aim to synchronize all viewports with the same modality * -'imageSlice' type, we will aim to synchronize all viewports with the same orientation. * * @param options * @param options.viewports - The viewports to synchronize * @param options.syncId - The synchronization group ID * @param options.type - The type of synchronization to perform */ toggleSynchronizer: ({ type, viewports, syncId }) => { const synchronizer = syncGroupService.getSynchronizer(syncId); if (synchronizer) { synchronizer.isDisabled() ? synchronizer.setEnabled(true) : synchronizer.setEnabled(false); return; } const fn = toggleSyncFunctions[type]; if (fn) { fn({ servicesManager, viewports, syncId, }); } }, setViewportForToolConfiguration: ({ viewportId, toolName }) => { if (!viewportId) { const { activeViewportId } = viewportGridService.getState(); viewportId = activeViewportId ?? 'default'; } const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); if (!toolGroup?.hasTool(toolName)) { return; } const prevConfig = toolGroup?.getToolConfiguration(toolName); toolGroup?.setToolConfiguration( toolName, { ...prevConfig, sourceViewportId: viewportId, }, true // overwrite ); const renderingEngine = cornerstoneViewportService.getRenderingEngine(); renderingEngine.render(); }, storePresentation: ({ viewportId }) => { cornerstoneViewportService.storePresentation({ viewportId }); }, updateVolumeData: ({ volume }) => { // update vtkOpenGLTexture and imageData of computed volume const { imageData, vtkOpenGLTexture } = volume; const numSlices = imageData.getDimensions()[2]; const slicesToUpdate = [...Array(numSlices).keys()]; slicesToUpdate.forEach(i => { vtkOpenGLTexture.setUpdatedFrame(i); }); imageData.modified(); }, attachProtocolViewportDataListener: ({ protocol, stageIndex }) => { const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED; const command = protocol.callbacks.onViewportDataInitialized; const numPanes = protocol.stages?.[stageIndex]?.viewports.length ?? 1; let numPanesWithData = 0; const { unsubscribe } = cornerstoneViewportService.subscribe(EVENT, evt => { numPanesWithData++; if (numPanesWithData === numPanes) { commandsManager.run(...command); // Unsubscribe from the event unsubscribe(EVENT); } }); }, setViewportPreset: ({ viewportId, preset }) => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); if (!viewport) { return; } viewport.setProperties({ preset, }); viewport.render(); }, /** * Sets the volume quality for a given viewport. * @param {string} viewportId - The ID of the viewport to set the volume quality. * @param {number} volumeQuality - The desired quality level of the volume rendering. */ setVolumeRenderingQulaity: ({ viewportId, volumeQuality }) => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const { actor } = viewport.getActors()[0]; const mapper = actor.getMapper(); const image = mapper.getInputData(); const dims = image.getDimensions(); const spacing = image.getSpacing(); const spatialDiagonal = vec3.length( vec3.fromValues(dims[0] * spacing[0], dims[1] * spacing[1], dims[2] * spacing[2]) ); let sampleDistance = spacing.reduce((a, b) => a + b) / 3.0; sampleDistance /= volumeQuality > 1 ? 0.5 * volumeQuality ** 2 : 1.0; const samplesPerRay = spatialDiagonal / sampleDistance + 1; mapper.setMaximumSamplesPerRay(samplesPerRay); mapper.setSampleDistance(sampleDistance); viewport.render(); }, /** * Shifts opacity points for a given viewport id. * @param {string} viewportId - The ID of the viewport to set the mapping range. * @param {number} shift - The shift value to shift the points by. */ shiftVolumeOpacityPoints: ({ viewportId, shift }) => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const { actor } = viewport.getActors()[0]; const ofun = actor.getProperty().getScalarOpacity(0); const opacityPointValues = []; // Array to hold values // Gather Existing Values const size = ofun.getSize(); for (let pointIdx = 0; pointIdx < size; pointIdx++) { const opacityPointValue = [0, 0, 0, 0]; ofun.getNodeValue(pointIdx, opacityPointValue); // opacityPointValue now holds [xLocation, opacity, midpoint, sharpness] opacityPointValues.push(opacityPointValue); } // Add offset opacityPointValues.forEach(opacityPointValue => { opacityPointValue[0] += shift; // Change the location value }); // Set new values ofun.removeAllPoints(); opacityPointValues.forEach(opacityPointValue => { ofun.addPoint(...opacityPointValue); }); viewport.render(); }, /** * Sets the volume lighting settings for a given viewport. * @param {string} viewportId - The ID of the viewport to set the lighting settings. * @param {Object} options - The lighting settings to be set. * @param {boolean} options.shade - The shade setting for the lighting. * @param {number} options.ambient - The ambient setting for the lighting. * @param {number} options.diffuse - The diffuse setting for the lighting. * @param {number} options.specular - The specular setting for the lighting. **/ setVolumeLighting: ({ viewportId, options }) => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const { actor } = viewport.getActors()[0]; const property = actor.getProperty(); if (options.shade !== undefined) { property.setShade(options.shade); } if (options.ambient !== undefined) { property.setAmbient(options.ambient); } if (options.diffuse !== undefined) { property.setDiffuse(options.diffuse); } if (options.specular !== undefined) { property.setSpecular(options.specular); } viewport.render(); }, resetCrosshairs: ({ viewportId }) => { const crosshairInstances = []; const getCrosshairInstances = toolGroupId => { const toolGroup = toolGroupService.getToolGroup(toolGroupId); crosshairInstances.push(toolGroup.getToolInstance('Crosshairs')); }; if (!viewportId) { const toolGroupIds = toolGroupService.getToolGroupIds(); toolGroupIds.forEach(getCrosshairInstances); } else { const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); getCrosshairInstances(toolGroup.id); } crosshairInstances.forEach(ins => { ins?.computeToolCenter(); }); }, /** * Creates a labelmap for the active viewport * * The created labelmap will be registered as a display set and also added * as a segmentation representation to the viewport. */ createLabelmapForViewport: async ({ viewportId, options = {} }) => { const { viewportGridService, displaySetService, segmentationService } = servicesManager.services; const { viewports } = viewportGridService.getState(); const targetViewportId = viewportId; const viewport = viewports.get(targetViewportId); // Todo: add support for multiple display sets const displaySetInstanceUID = options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0]; const segs = segmentationService.getSegmentations(); const label = options.label || `Segmentation ${segs.length + 1}`; const segmentationId = options.segmentationId || `${csUtils.uuidv4()}`; const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); // This will create the segmentation and register it as a display set const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet( displaySet, { label, segmentationId, segments: options.createInitialSegment ? { 1: { label: `${i18n.t('Segment')} 1`, active: true, }, } : {}, } ); // Also add the segmentation representation to the viewport await segmentationService.addSegmentationRepresentation(viewportId, { segmentationId, type: Enums.SegmentationRepresentations.Labelmap, }); return generatedSegmentationId; }, /** * Sets the active segmentation for a viewport * @param props.segmentationId - The ID of the segmentation to set as active */ setActiveSegmentation: ({ segmentationId }) => { const { viewportGridService, segmentationService } = servicesManager.services; segmentationService.setActiveSegmentation( viewportGridService.getActiveViewportId(), segmentationId ); }, /** * Adds a new segment to a segmentation * @param props.segmentationId - The ID of the segmentation to add the segment to */ addSegmentCommand: ({ segmentationId }) => { const { segmentationService } = servicesManager.services; segmentationService.addSegment(segmentationId); }, /** * Sets the active segment and jumps to its center * @param props.segmentationId - The ID of the segmentation * @param props.segmentIndex - The index of the segment to activate */ setActiveSegmentAndCenterCommand: ({ segmentationId, segmentIndex }) => { const { segmentationService, viewportGridService } = servicesManager.services; // set both active segmentation and active segment segmentationService.setActiveSegmentation( viewportGridService.getActiveViewportId(), segmentationId ); segmentationService.setActiveSegment(segmentationId, segmentIndex); segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex); }, /** * Toggles the visibility of a segment * @param props.segmentationId - The ID of the segmentation * @param props.segmentIndex - The index of the segment * @param props.type - The type of visibility to toggle */ toggleSegmentVisibilityCommand: ({ segmentationId, segmentIndex, type }) => { const { segmentationService, viewportGridService } = servicesManager.services; segmentationService.toggleSegmentVisibility( viewportGridService.getActiveViewportId(), segmentationId, segmentIndex, type ); }, /** * Toggles the lock state of a segment * @param props.segmentationId - The ID of the segmentation * @param props.segmentIndex - The index of the segment */ toggleSegmentLockCommand: ({ segmentationId, segmentIndex }) => { const { segmentationService } = servicesManager.services; segmentationService.toggleSegmentLocked(segmentationId, segmentIndex); }, /** * Toggles the visibility of a segmentation representation * @param props.segmentationId - The ID of the segmentation * @param props.type - The type of representation */ toggleSegmentationVisibilityCommand: ({ segmentationId, type }) => { const { segmentationService, viewportGridService } = servicesManager.services; segmentationService.toggleSegmentationRepresentationVisibility( viewportGridService.getActiveViewportId(), { segmentationId, type } ); }, /** * Downloads a segmentation * @param props.segmentationId - The ID of the segmentation to download */ downloadSegmentationCommand: ({ segmentationId }) => { const { segmentationService } = servicesManager.services; segmentationService.downloadSegmentation(segmentationId); }, /** * Stores a segmentation and shows it in the viewport * @param props.segmentationId - The ID of the segmentation to store */ storeSegmentationCommand: async ({ segmentationId }) => { const { segmentationService, viewportGridService } = servicesManager.services; const displaySetInstanceUIDs = await createReportAsync({ servicesManager, getReport: () => commandsManager.runCommand('storeSegmentation', { segmentationId, }), reportType: 'Segmentation', }); if (displaySetInstanceUIDs) { segmentationService.remove(segmentationId); viewportGridService.setDisplaySetsForViewport({ viewportId: viewportGridService.getActiveViewportId(), displaySetInstanceUIDs, }); } }, /** * Downloads a segmentation as RTSS * @param props.segmentationId - The ID of the segmentation */ downloadRTSSCommand: ({ segmentationId }) => { const { segmentationService } = servicesManager.services; segmentationService.downloadRTSS(segmentationId); }, /** * Sets the style for a segmentation * @param props.segmentationId - The ID of the segmentation * @param props.type - The type of style * @param props.key - The style key to set * @param props.value - The style value */ setSegmentationStyleCommand: ({ type, key, value }) => { const { segmentationService } = servicesManager.services; segmentationService.setStyle({ type }, { [key]: value }); }, /** * Deletes a segment from a segmentation * @param props.segmentationId - The ID of the segmentation * @param props.segmentIndex - The index of the segment to delete */ deleteSegmentCommand: ({ segmentationId, segmentIndex }) => { const { segmentationService } = servicesManager.services; segmentationService.removeSegment(segmentationId, segmentIndex); }, /** * Deletes an entire segmentation * @param props.segmentationId - The ID of the segmentation to delete */ deleteSegmentationCommand: ({ segmentationId }) => { const { segmentationService } = servicesManager.services; segmentationService.remove(segmentationId); }, /** * Removes a segmentation from the viewport * @param props.segmentationId - The ID of the segmentation to remove */ removeSegmentationFromViewportCommand: ({ segmentationId }) => { const { segmentationService, viewportGridService } = servicesManager.services; segmentationService.removeSegmentationRepresentations( viewportGridService.getActiveViewportId(), { segmentationId } ); }, /** * Toggles rendering of inactive segmentations */ toggleRenderInactiveSegmentationsCommand: () => { const { segmentationService, viewportGridService } = servicesManager.services; const viewportId = viewportGridService.getActiveViewportId(); const renderInactive = segmentationService.getRenderInactiveSegmentations(viewportId); segmentationService.setRenderInactiveSegmentations(viewportId, !renderInactive); }, /** * Sets the fill alpha value for a segmentation type * @param props.type - The type of segmentation * @param props.value - The alpha value to set */ setFillAlphaCommand: ({ type, value }) => { const { segmentationService } = servicesManager.services; segmentationService.setStyle({ type }, { fillAlpha: value }); }, /** * Sets the outline width for a segmentation type * @param props.type - The type of segmentation * @param props.value - The width value to set */ setOutlineWidthCommand: ({ type, value }) => { const { segmentationService } = servicesManager.services; segmentationService.setStyle({ type }, { outlineWidth: value }); }, /** * Sets whether to render fill for a segmentation type * @param props.type - The type of segmentation * @param props.value - Whether to render fill */ setRenderFillCommand: ({ type, value }) => { const { segmentationService } = servicesManager.services; segmentationService.setStyle({ type }, { renderFill: value }); }, /** * Sets whether to render outline for a segmentation type * @param props.type - The type of segmentation * @param props.value - Whether to render outline */ setRenderOutlineCommand: ({ type, value }) => { const { segmentationService } = servicesManager.services; segmentationService.setStyle({ type }, { renderOutline: value }); }, /** * Sets the fill alpha for inactive segmentations * @param props.type - The type of segmentation * @param props.value - The alpha value to set */ setFillAlphaInactiveCommand: ({ type, value }) => { const { segmentationService } = servicesManager.services; segmentationService.setStyle({ type }, { fillAlphaInactive: value }); }, editSegmentLabel: async ({ segmentationId, segmentIndex }) => { const { segmentationService, uiDialogService } = servicesManager.services; const segmentation = segmentationService.getSegmentation(segmentationId); if (!segmentation) { return; } const segment = segmentation.segments[segmentIndex]; callInputDialog({ uiDialogService, title: 'Edit Segment Label', placeholder: 'Enter new label', defaultValue: segment.label, }).then(label => { segmentationService.setSegmentLabel(segmentationId, segmentIndex, label); }); }, editSegmentationLabel: ({ segmentationId }) => { const { segmentationService, uiDialogService } = servicesManager.services; const segmentation = segmentationService.getSegmentation(segmentationId); if (!segmentation) { return; } const { label } = segmentation; callInputDialog({ uiDialogService, title: 'Edit Segmentation Label', placeholder: 'Enter new label', defaultValue: label, }).then(label => { segmentationService.addOrUpdateSegmentation({ segmentationId, label }); }); }, editSegmentColor: ({ segmentationId, segmentIndex }) => { const { segmentationService, uiDialogService, viewportGridService } = servicesManager.services; const viewportId = viewportGridService.getActiveViewportId(); const color = segmentationService.getSegmentColor(viewportId, segmentationId, segmentIndex); const rgbaColor = { r: color[0], g: color[1], b: color[2], a: color[3] / 255.0, }; 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); }, }, }); }, getRenderInactiveSegmentations: () => { const { segmentationService, viewportGridService } = servicesManager.services; return segmentationService.getRenderInactiveSegmentations( viewportGridService.getActiveViewportId() ); }, deleteActiveAnnotation: () => { const activeAnnotationsUID = cornerstoneTools.annotation.selection.getAnnotationsSelected(); activeAnnotationsUID.forEach(activeAnnotationUID => { measurementService.remove(activeAnnotationUID); }); }, setDisplaySetsForViewports: ({ viewportsToUpdate }) => { const { cineService, viewportGridService } = servicesManager.services; // Stopping the cine of modified viewports before changing the viewports to // avoid inconsistent state and lost references viewportsToUpdate.forEach(viewport => { const state = cineService.getState(); const currentCineState = state.cines?.[viewport.viewportId]; cineService.setCine({ id: viewport.viewportId, frameRate: currentCineState?.frameRate ?? state.default?.frameRate ?? 24, isPlaying: false, }); }); viewportGridService.setDisplaySetsForViewports(viewportsToUpdate); }, undo: () => { DefaultHistoryMemo.undo(); }, redo: () => { DefaultHistoryMemo.redo(); }, toggleSegmentPreviewEdit: ({ toggle }) => { let labelmapTools = getLabelmapTools({ toolGroupService }); labelmapTools = labelmapTools.filter(tool => !tool.toolName.includes('Eraser')); labelmapTools.forEach(tool => { tool.configuration = { ...tool.configuration, preview: { ...tool.configuration.preview, enabled: toggle, }, }; }); }, toggleSegmentSelect: ({ toggle }) => { const toolGroupIds = toolGroupService.getToolGroupIds(); toolGroupIds.forEach(toolGroupId => { const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId); if (toggle) { toolGroup.setToolActive(cornerstoneTools.SegmentSelectTool.toolName); } else { toolGroup.setToolDisabled(cornerstoneTools.SegmentSelectTool.toolName); } }); }, toggleSegmentLabel: ({ toggle }) => { const toolGroupIds = toolGroupService.getToolGroupIds(); toolGroupIds.forEach(toolGroupId => { const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId); if (toggle) { toolGroup.setToolActive(cornerstoneTools.SegmentLabelTool.toolName); } else { toolGroup.setToolDisabled(cornerstoneTools.SegmentLabelTool.toolName); } }); }, toggleUseCenterSegmentIndex: ({ toggle }) => { let labelmapTools = getLabelmapTools({ toolGroupService }); labelmapTools = labelmapTools.filter(tool => !tool.toolName.includes('Eraser')); labelmapTools.forEach(tool => { tool.configuration = { ...tool.configuration, useCenterSegmentIndex: toggle, }; }); }, _handlePreviewAction: action => { const { viewport } = _getActiveViewportEnabledElement(); const previewTools = getPreviewTools({ toolGroupService }); previewTools.forEach(tool => { try { tool[`${action}Preview`](); } catch (error) { console.debug('Error accepting preview for tool', tool.toolName); } }); if (segmentAI.enabled) { segmentAI[`${action}Preview`](viewport.element); } }, acceptPreview: () => { actions._handlePreviewAction('accept'); }, rejectPreview: () => { actions._handlePreviewAction('reject'); }, clearMarkersForMarkerLabelmap: () => { const { viewport } = _getActiveViewportEnabledElement(); const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(viewport.id); const toolInstance = toolGroup.getToolInstance('MarkerLabelmap'); if (!toolInstance) { return; } toolInstance.clearMarkers(viewport); }, interpolateScrollForMarkerLabelmap: () => { const { viewport } = _getActiveViewportEnabledElement(); const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(viewport.id); const toolInstance = toolGroup.getToolInstance('MarkerLabelmap'); if (!toolInstance) { return; } toolInstance.interpolateScroll(viewport, 1); }, toggleLabelmapAssist: async () => { const { viewport } = _getActiveViewportEnabledElement(); const newState = !segmentAI.enabled; segmentAI.enabled = newState; if (!segmentAIEnabled) { await segmentAI.initModel(); segmentAIEnabled = true; } // set the brush tool to active const toolGroupIds = toolGroupService.getToolGroupIds(); if (newState) { actions.setToolActiveToolbar({ toolName: 'CircularBrushForAutoSegmentAI', toolGroupIds: toolGroupIds, }); } else { toolGroupIds.forEach(toolGroupId => { const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId); toolGroup.setToolPassive('CircularBrushForAutoSegmentAI'); }); } if (segmentAI.enabled) { segmentAI.initViewport(viewport); } }, setBrushSize: ({ value, toolNames }) => { const brushSize = Number(value); toolGroupService.getToolGroupIds()?.forEach(toolGroupId => { if (toolNames?.length === 0) { segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize); } else { toolNames?.forEach(toolName => { segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName); }); } }); }, setThresholdRange: ({ value, toolNames = [ 'ThresholdCircularBrush', 'ThresholdSphereBrush', 'ThresholdCircularBrushDynamic', 'ThresholdSphereBrushDynamic', ], }) => { const toolGroupIds = toolGroupService.getToolGroupIds(); if (!toolGroupIds?.length) { return; } for (const toolGroupId of toolGroupIds) { const toolGroup = toolGroupService.getToolGroup(toolGroupId); toolNames?.forEach(toolName => { toolGroup.setToolConfiguration(toolName, { threshold: { range: value, }, }); }); } }, increaseBrushSize: () => { const toolGroupIds = toolGroupService.getToolGroupIds(); if (!toolGroupIds?.length) { return; } for (const toolGroupId of toolGroupIds) { const brushSize = segmentationUtils.getBrushSizeForToolGroup(toolGroupId); segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize + 3); } }, decreaseBrushSize: () => { const toolGroupIds = toolGroupService.getToolGroupIds(); if (!toolGroupIds?.length) { return; } for (const toolGroupId of toolGroupIds) { const brushSize = segmentationUtils.getBrushSizeForToolGroup(toolGroupId); segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize - 3); } }, addNewSegment: () => { const { segmentationService } = servicesManager.services; const { activeViewportId } = viewportGridService.getState(); const activeSegmentation = segmentationService.getActiveSegmentation(activeViewportId); if (!activeSegmentation) { return; } segmentationService.addSegment(activeSegmentation.segmentationId); }, loadSegmentationDisplaySetsForViewport: ({ viewportId, displaySetInstanceUIDs }) => { const updatedViewports = getUpdatedViewportsForSegmentation({ viewportId, servicesManager, displaySetInstanceUIDs, }); actions.setDisplaySetsForViewports({ viewportsToUpdate: updatedViewports.map(viewport => ({ viewportId: viewport.viewportId, displaySetInstanceUIDs: viewport.displaySetInstanceUIDs, })), }); }, setViewportOrientation: ({ viewportId, orientation }) => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); if (!viewport || viewport.type !== CoreEnums.ViewportType.ORTHOGRAPHIC) { console.warn('Orientation can only be set on volume viewports'); return; } // Get display sets for this viewport to verify at least one is reconstructable const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); const displaySets = displaySetUIDs.map(uid => displaySetService.getDisplaySetByUID(uid)); if (!displaySets.some(ds => ds.isReconstructable)) { console.warn('Cannot change orientation: No reconstructable display sets in viewport'); return; } viewport.setOrientation(orientation); viewport.render(); // update the orientation in the viewport info const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); viewportInfo.setOrientation(orientation); }, /** * Toggles the horizontal flip state of the viewport. */ toggleViewportHorizontalFlip: ({ viewportId }: { viewportId?: string } = {}) => { actions.flipViewportHorizontal({ viewportId, newValue: 'toggle' }); }, /** * Explicitly sets the horizontal flip state of the viewport. */ setViewportHorizontalFlip: ({ flipped, viewportId, }: { flipped: boolean; viewportId?: string; }) => { actions.flipViewportHorizontal({ viewportId, newValue: flipped }); }, /** * Toggles the vertical flip state of the viewport. */ toggleViewportVerticalFlip: ({ viewportId }: { viewportId?: string } = {}) => { actions.flipViewportVertical({ viewportId, newValue: 'toggle' }); }, /** * Explicitly sets the vertical flip state of the viewport. */ setViewportVerticalFlip: ({ flipped, viewportId, }: { flipped: boolean; viewportId?: string; }) => { actions.flipViewportVertical({ viewportId, newValue: flipped }); }, /** * Internal helper to rotate or set absolute rotation for a viewport. */ _rotateViewport: ({ rotation, viewportId, rotationMode = 'apply', }: { rotation: number; viewportId?: string; rotationMode?: 'apply' | 'set'; }) => { const enabledElement = viewportId ? _getViewportEnabledElement(viewportId) : _getActiveViewportEnabledElement(); if (!enabledElement) { return; } const { viewport } = enabledElement; if (viewport instanceof BaseVolumeViewport) { const camera = viewport.getCamera(); const rotAngle = (rotation * Math.PI) / 180; const rotMat = mat4.identity(new Float32Array(16)); mat4.rotate(rotMat, rotMat, rotAngle, camera.viewPlaneNormal); const rotatedViewUp = vec3.transformMat4(vec3.create(), camera.viewUp, rotMat); viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 }); viewport.render(); return; } if (viewport.getRotation !== undefined) { const { rotation: currentRotation } = viewport.getViewPresentation(); const newRotation = rotationMode === 'apply' ? (currentRotation + rotation + 360) % 360 : (() => { // In 'set' mode, account for the effect horizontal/vertical flips // have on the perceived rotation direction. A single flip mirrors // the image and inverses rotation direction, while two flips // restore the original parity. We therefore invert the rotation // angle when an odd number of flips are applied so that the // requested absolute rotation matches the user expectation. const { flipHorizontal = false, flipVertical = false } = viewport.getViewPresentation(); const flipsParity = (flipHorizontal ? 1 : 0) + (flipVertical ? 1 : 0); const effectiveRotation = flipsParity % 2 === 1 ? -rotation : rotation; return (effectiveRotation + 360) % 360; })(); viewport.setViewPresentation({ rotation: newRotation }); viewport.render(); } }, startRecordingForAnnotationGroup: () => { cornerstoneTools.AnnotationTool.startGroupRecording(); }, endRecordingForAnnotationGroup: () => { cornerstoneTools.AnnotationTool.endGroupRecording(); }, triggerCreateAnnotationMemo: ({ annotation, FrameOfReferenceUID, options, }: { annotation: ToolTypes.Annotation; FrameOfReferenceUID: string; options: { newAnnotation?: boolean; deleting?: boolean }; }): void => { const { newAnnotation, deleting } = options; const renderingEngines = getRenderingEngines(); const viewports = renderingEngines.flatMap(re => re.getViewports()); const validViewport = viewports.find( vp => vp.getFrameOfReferenceUID() === FrameOfReferenceUID ); if (!validViewport) { return; } cornerstoneTools.AnnotationTool.createAnnotationMemo(validViewport.element, annotation, { newAnnotation, deleting, }); }, }; const definitions = { // The command here is to show the viewer context menu, as being the // context menu showCornerstoneContextMenu: { commandFn: actions.showCornerstoneContextMenu, options: { menuCustomizationId: 'measurementsContextMenu', commands: [ { commandName: 'showContextMenu', }, ], }, }, getNearbyToolData: { commandFn: actions.getNearbyToolData, }, getNearbyAnnotation: { commandFn: actions.getNearbyAnnotation, storeContexts: [], options: {}, }, toggleViewportColorbar: { commandFn: actions.toggleViewportColorbar, }, setMeasurementLabel: { commandFn: actions.setMeasurementLabel, }, renameMeasurement: { commandFn: actions.renameMeasurement, }, updateMeasurement: { commandFn: actions.updateMeasurement, }, jumpToMeasurement: { commandFn: actions.jumpToMeasurement, }, removeMeasurement: { commandFn: actions.removeMeasurement, }, toggleLockMeasurement: { commandFn: actions.toggleLockMeasurement, }, toggleVisibilityMeasurement: { commandFn: actions.toggleVisibilityMeasurement, }, downloadCSVMeasurementsReport: { commandFn: actions.downloadCSVMeasurementsReport, }, setViewportWindowLevel: { commandFn: actions.setViewportWindowLevel, }, setWindowLevel: { commandFn: actions.setWindowLevel, }, setWindowLevelPreset: { commandFn: actions.setWindowLevelPreset, }, setToolActive: { commandFn: actions.setToolActive, }, setToolActiveToolbar: { commandFn: actions.setToolActiveToolbar, }, setToolEnabled: { commandFn: actions.setToolEnabled, }, rotateViewportCW: { commandFn: actions.rotateViewportBy, options: { rotation: 90 }, }, rotateViewportCCW: { commandFn: actions.rotateViewportBy, options: { rotation: -90 }, }, rotateViewportCWSet: { commandFn: actions.setViewportRotation, options: { rotation: 90 }, }, incrementActiveViewport: { commandFn: actions.changeActiveViewport, }, decrementActiveViewport: { commandFn: actions.changeActiveViewport, options: { direction: -1 }, }, flipViewportHorizontal: { commandFn: actions.toggleViewportHorizontalFlip, }, flipViewportVertical: { commandFn: actions.toggleViewportVerticalFlip, }, setViewportHorizontalFlip: { commandFn: actions.setViewportHorizontalFlip, options: { flipped: true }, }, setViewportVerticalFlip: { commandFn: actions.setViewportVerticalFlip, options: { flipped: true }, }, invertViewport: { commandFn: actions.invertViewport, }, resetViewport: { commandFn: actions.resetViewport, }, scaleUpViewport: { commandFn: actions.scaleViewport, options: { direction: 1 }, }, scaleDownViewport: { commandFn: actions.scaleViewport, options: { direction: -1 }, }, fitViewportToWindow: { commandFn: actions.scaleViewport, options: { direction: 0 }, }, nextImage: { commandFn: actions.scroll, options: { direction: 1 }, }, previousImage: { commandFn: actions.scroll, options: { direction: -1 }, }, firstImage: { commandFn: actions.jumpToImage, options: { imageIndex: 0 }, }, lastImage: { commandFn: actions.jumpToImage, options: { imageIndex: -1 }, }, jumpToImage: { commandFn: actions.jumpToImage, }, showDownloadViewportModal: { commandFn: actions.showDownloadViewportModal, }, toggleCine: { commandFn: actions.toggleCine, }, arrowTextCallback: { commandFn: actions.arrowTextCallback, }, setViewportActive: { commandFn: actions.setViewportActive, }, setViewportColormap: { commandFn: actions.setViewportColormap, }, setViewportForToolConfiguration: { commandFn: actions.setViewportForToolConfiguration, }, storePresentation: { commandFn: actions.storePresentation, }, attachProtocolViewportDataListener: { commandFn: actions.attachProtocolViewportDataListener, }, setViewportPreset: { commandFn: actions.setViewportPreset, }, setVolumeRenderingQulaity: { commandFn: actions.setVolumeRenderingQulaity, }, shiftVolumeOpacityPoints: { commandFn: actions.shiftVolumeOpacityPoints, }, setVolumeLighting: { commandFn: actions.setVolumeLighting, }, resetCrosshairs: { commandFn: actions.resetCrosshairs, }, toggleSynchronizer: { commandFn: actions.toggleSynchronizer, }, updateVolumeData: { commandFn: actions.updateVolumeData, }, toggleEnabledDisabledToolbar: { commandFn: actions.toggleEnabledDisabledToolbar, }, toggleActiveDisabledToolbar: { commandFn: actions.toggleActiveDisabledToolbar, }, updateStoredPositionPresentation: { commandFn: actions.updateStoredPositionPresentation, }, updateStoredSegmentationPresentation: { commandFn: actions.updateStoredSegmentationPresentation, }, createLabelmapForViewport: { commandFn: actions.createLabelmapForViewport, }, setActiveSegmentation: { commandFn: actions.setActiveSegmentation, }, addSegment: { commandFn: actions.addSegmentCommand, }, setActiveSegmentAndCenter: { commandFn: actions.setActiveSegmentAndCenterCommand, }, toggleSegmentVisibility: { commandFn: actions.toggleSegmentVisibilityCommand, }, toggleSegmentLock: { commandFn: actions.toggleSegmentLockCommand, }, toggleSegmentationVisibility: { commandFn: actions.toggleSegmentationVisibilityCommand, }, downloadSegmentation: { commandFn: actions.downloadSegmentationCommand, }, storeSegmentation: { commandFn: actions.storeSegmentationCommand, }, downloadRTSS: { commandFn: actions.downloadRTSSCommand, }, setSegmentationStyle: { commandFn: actions.setSegmentationStyleCommand, }, deleteSegment: { commandFn: actions.deleteSegmentCommand, }, deleteSegmentation: { commandFn: actions.deleteSegmentationCommand, }, removeSegmentationFromViewport: { commandFn: actions.removeSegmentationFromViewportCommand, }, toggleRenderInactiveSegmentations: { commandFn: actions.toggleRenderInactiveSegmentationsCommand, }, setFillAlpha: { commandFn: actions.setFillAlphaCommand, }, setOutlineWidth: { commandFn: actions.setOutlineWidthCommand, }, setRenderFill: { commandFn: actions.setRenderFillCommand, }, setRenderOutline: { commandFn: actions.setRenderOutlineCommand, }, setFillAlphaInactive: { commandFn: actions.setFillAlphaInactiveCommand, }, editSegmentLabel: { commandFn: actions.editSegmentLabel, }, editSegmentationLabel: { commandFn: actions.editSegmentationLabel, }, editSegmentColor: { commandFn: actions.editSegmentColor, }, getRenderInactiveSegmentations: { commandFn: actions.getRenderInactiveSegmentations, }, deleteActiveAnnotation: { commandFn: actions.deleteActiveAnnotation, }, setDisplaySetsForViewports: actions.setDisplaySetsForViewports, undo: actions.undo, redo: actions.redo, interpolateLabelmap: actions.interpolateLabelmap, runSegmentBidirectional: actions.runSegmentBidirectional, downloadCSVSegmentationReport: actions.downloadCSVSegmentationReport, toggleSegmentPreviewEdit: actions.toggleSegmentPreviewEdit, toggleSegmentSelect: actions.toggleSegmentSelect, acceptPreview: actions.acceptPreview, rejectPreview: actions.rejectPreview, toggleUseCenterSegmentIndex: actions.toggleUseCenterSegmentIndex, toggleLabelmapAssist: actions.toggleLabelmapAssist, interpolateScrollForMarkerLabelmap: actions.interpolateScrollForMarkerLabelmap, clearMarkersForMarkerLabelmap: actions.clearMarkersForMarkerLabelmap, setBrushSize: actions.setBrushSize, setThresholdRange: actions.setThresholdRange, increaseBrushSize: actions.increaseBrushSize, decreaseBrushSize: actions.decreaseBrushSize, addNewSegment: actions.addNewSegment, loadSegmentationDisplaySetsForViewport: actions.loadSegmentationDisplaySetsForViewport, setViewportOrientation: actions.setViewportOrientation, hydrateSecondaryDisplaySet: actions.hydrateSecondaryDisplaySet, getVolumeIdForDisplaySet: actions.getVolumeIdForDisplaySet, triggerCreateAnnotationMemo: actions.triggerCreateAnnotationMemo, startRecordingForAnnotationGroup: actions.startRecordingForAnnotationGroup, endRecordingForAnnotationGroup: actions.endRecordingForAnnotationGroup, toggleSegmentLabel: actions.toggleSegmentLabel, }; return { actions, definitions, defaultContext: 'CORNERSTONE', }; } export default commandsModule;