fix: remaining bugs for MPR and toolbar icon colors (#3026)

* feat: warn for overlapping segments

* fix: typo

* refactor mpr toggle and stack sync

* fix: wip for more general mpr

* fix: tool groups for mpr

* fix: changing of viewport id

* fix: viewport Id for the seg viewports

* fix

* update cornerstone version
This commit is contained in:
Alireza 2022-11-21 10:44:19 -05:00 committed by GitHub
parent 6db4d7a917
commit 87eab39768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 726 additions and 384 deletions

View File

@ -36,6 +36,7 @@ function OHIFCornerstoneSEGViewport(props) {
DisplaySetService,
ToolGroupService,
SegmentationService,
UINotificationService,
} = servicesManager.services;
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
@ -105,7 +106,6 @@ function OHIFCornerstoneSEGViewport(props) {
} = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment
return (
<Component
{...props}
@ -114,6 +114,7 @@ function OHIFCornerstoneSEGViewport(props) {
viewportType: 'volume',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
}}
onElementEnabled={onElementEnabled}
onElementDisabled={onElementDisabled}
@ -176,6 +177,15 @@ function OHIFCornerstoneSEGViewport(props) {
) {
setSegIsLoading(false);
}
if (evt.overlappingSegments) {
UINotificationService.show({
title: 'Overlapping Segments',
message:
'Overlapping segments detected which is not currently supported',
type: 'warning',
});
}
}
);

View File

@ -45,7 +45,7 @@
"dependencies": {
"@babel/runtime": "7.16.3",
"classnames": "^2.2.6",
"@cornerstonejs/core": "^0.21.0",
"@cornerstonejs/tools": "^0.29.2"
"@cornerstonejs/core": "^0.21.4",
"@cornerstonejs/tools": "^0.29.6"
}
}

View File

@ -43,9 +43,9 @@
},
"dependencies": {
"@babel/runtime": "7.17.9",
"@cornerstonejs/core": "^0.21.0",
"@cornerstonejs/streaming-image-volume-loader": "^0.6.1",
"@cornerstonejs/tools": "^0.29.2",
"@cornerstonejs/core": "^0.21.4",
"@cornerstonejs/streaming-image-volume-loader": "^0.6.5",
"@cornerstonejs/tools": "^0.29.6",
"@kitware/vtk.js": "25.9.0",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",

View File

@ -1,7 +1,6 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import ReactResizeDetector from 'react-resize-detector';
import PropTypes from 'prop-types';
import { useViewportGrid } from '@ohif/ui';
import * as cs3DTools from '@cornerstonejs/tools';
import {
Enums,
@ -40,6 +39,13 @@ function areEqual(prevProps, nextProps) {
return false;
}
if (
prevProps.viewportOptions.toolGroupId !==
nextProps.viewportOptions.toolGroupId
) {
return false;
}
if (
prevProps.viewportOptions.viewportType !==
nextProps.viewportOptions.viewportType
@ -134,6 +140,28 @@ const OHIFCornerstoneViewport = React.memo(props => {
}
}, [elementRef]);
const cleanUpServices = useCallback(() => {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (!viewportInfo) {
return;
}
const viewportId = viewportInfo.getViewportId();
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
ToolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
SyncGroupService.removeViewportFromSyncGroup(
viewportId,
renderingEngineId,
syncGroups
);
}, [viewportIndex, viewportOptions.viewportId]);
const elementEnabledHandler = useCallback(
evt => {
// check this is this element reference and return early if doesn't match
@ -188,28 +216,15 @@ const OHIFCornerstoneViewport = React.memo(props => {
setImageScrollBarHeight();
return () => {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const viewportId = viewportInfo.getViewportId();
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
ToolGroupService.removeViewportFromToolGroup(
viewportId,
renderingEngineId
);
SyncGroupService.removeViewportFromSyncGroup(
viewportId,
renderingEngineId,
syncGroups
);
cleanUpServices();
CornerstoneViewportService.disableElement(viewportIndex);
if (onElementDisabled) {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
onElementDisabled(viewportInfo);
}
@ -326,8 +341,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
handleWidth
handleHeight
skipOnMount={true} // Todo: make these configurable
refreshMode={'throttle'}
refreshRate={100}
refreshMode={'debounce'}
refreshRate={200} // transition amount in side panel
onResize={onResize}
targetRef={elementRef.current}
/>
@ -355,7 +370,7 @@ function _subscribeToJumpToMeasurementEvents(
elementRef,
viewportIndex,
displaySets,
viewportGridService,
ViewportGridService,
CornerstoneViewportService
) {
const displaysUIDs = displaySets.map(

View File

@ -7,34 +7,20 @@ import {
ToolGroupManager,
Enums,
utilities as cstUtils,
segmentation as cstSegmentation,
ReferenceLinesTool,
synchronizers as cstSynchronizers,
} from '@cornerstonejs/tools';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import { getEnabledElement as OHIFgetEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import callInputDialog from './utils/callInputDialog';
import { setColormap } from './utils/colormap/transferFunctionHelpers';
import getProtocolViewportStructureFromGridViewports from './utils/getProtocolViewportStructureFromGridViewports';
import removeToolGroupSegmentationRepresentations from './utils/removeToolGroupSegmentationRepresentations';
import calculateViewportRegistrations from './utils/calculateViewportRegistrations';
const MPR_TOOLGROUP_ID = 'mpr';
// [ {
// synchronizerId: string,
// viewports: [ { viewportId: number, renderingEngineId: string, index: number } , ...]
// ]}
let STACK_IMAGE_SYNC_GROUPS_INFO = [];
import toggleMPRHangingProtocol from './utils/mpr/toggleMPRHangingProtocol';
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
const commandsModule = ({ servicesManager }) => {
const {
ViewportGridService,
ToolGroupService,
DisplaySetService,
SyncGroupService,
CineService,
ToolBarService,
UIDialogService,
@ -160,48 +146,51 @@ const commandsModule = ({ servicesManager }) => {
}
}
const toolGroup = _getToolGroup(toolGroupId);
if (!toolGroup) {
console.warn('No tool group found for toolGroupId:', toolGroupId);
return;
}
// Todo: we need to check if the viewports of the toolGroup is actually
// parts of the ViewportGrid's viewports, if not we return
const { viewports } = ViewportGridService.getState() || {
viewports: [],
};
// iterate over all viewports and set the tool active for the
// viewports that belong to the toolGroup
for (let index = 0; index < viewports.length; index++) {
const ohifEnabledElement = OHIFgetEnabledElement(index);
if (!ohifEnabledElement) {
continue;
}
const viewport = getEnabledElement(ohifEnabledElement.element);
if (!viewport) {
continue;
}
// Find the current active tool and set it to be passive
const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
if (activeTool) {
toolGroup.setToolPassive(activeTool);
}
// Set the new toolName to be active
toolGroup.setToolActive(toolName, {
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
});
const toolGroup = _getToolGroup(toolGroupId);
const toolGroupViewportIds = toolGroup.getViewportIds();
// if toolGroup has been destroyed, or its viewports have been removed
if (!toolGroupViewportIds || !toolGroupViewportIds.length) {
return;
}
const filteredViewports = viewports.filter(viewport => {
if (!viewport.viewportOptions) {
return false;
}
return toolGroupViewportIds.includes(
viewport.viewportOptions.viewportId
);
});
if (!filteredViewports.length) {
return;
}
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
if (activeToolName) {
// Todo: this is a hack to prevent the crosshairs to stick around
// after another tool is selected. We should find a better way to do this
if (activeToolName === 'Crosshairs') {
toolGroup.setToolDisabled(activeToolName);
} else {
toolGroup.setToolPassive(activeToolName);
}
}
// Set the new toolName to be active
toolGroup.setToolActive(toolName, {
bindings: [
{
mouseButton: Enums.MouseBindings.Primary,
},
],
});
},
showDownloadViewportModal: () => {
const { activeViewportIndex } = ViewportGridService.getState();
@ -375,229 +364,17 @@ const commandsModule = ({ servicesManager }) => {
HangingProtocolService.setProtocol(protocolId);
},
toggleMPR: ({ toggledState }) => {
const { activeViewportIndex, viewports } = ViewportGridService.getState();
const viewportDisplaySetInstanceUIDs =
viewports[activeViewportIndex].displaySetInstanceUIDs;
const errorCallback = error => {
UINotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Cannot create MPR for this DisplaySet since it is not reconstructable.',
type: 'info',
duration: 3000,
});
};
const cacheId = 'beforeMPR';
if (toggledState) {
ViewportGridService.setCachedLayout({
cacheId,
cachedLayout: ViewportGridService.getState(),
});
const matchDetails = {
displaySetInstanceUIDs: viewportDisplaySetInstanceUIDs,
};
HangingProtocolService.setProtocol(
MPR_TOOLGROUP_ID,
matchDetails,
errorCallback
);
return;
}
const { cachedLayout } = ViewportGridService.getState();
if (!cachedLayout || !cachedLayout[cacheId]) {
return;
}
const { viewports: cachedViewports, numRows, numCols } = cachedLayout[
cacheId
];
// Todo: The following assumes that when turning off MPR we are applying the default
// protocol which might not be the one that was used before MPR was turned on
// In order to properly implement this logic, we should modify the hanging protocol
// upon layout change with layout selector, and cache and restore it when turning
// MPR on and off
const viewportStructure = getProtocolViewportStructureFromGridViewports({
viewports: cachedViewports,
numRows,
numCols,
toggleMPRHangingProtocol({
toggledState,
servicesManager,
getToolGroup: _getToolGroup,
});
const viewportSpecificMatch = cachedViewports.reduce(
(acc, viewport, index) => {
const {
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
} = viewport;
acc[index] = {
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
};
return acc;
},
{}
);
const defaultProtocol = HangingProtocolService.getProtocolById('default');
// Todo: this assumes there is only one stage in the default protocol
const defaultProtocolStage = defaultProtocol.stages[0];
defaultProtocolStage.viewportStructure = viewportStructure;
const { primaryToolId } = ToolBarService.state;
const mprToolGroup = _getToolGroup(MPR_TOOLGROUP_ID);
// turn off crosshairs if it is on
if (
primaryToolId === 'Crosshairs' ||
mprToolGroup.getToolInstance('Crosshairs')?.mode ===
Enums.ToolModes.Active
) {
const toolGroup = _getToolGroup(MPR_TOOLGROUP_ID);
toolGroup.setToolDisabled('Crosshairs');
ToolBarService.recordInteraction({
groupId: 'WindowLevel',
itemId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
}
// clear segmentations if they exist
removeToolGroupSegmentationRepresentations(MPR_TOOLGROUP_ID);
HangingProtocolService.setProtocol(
'default',
viewportSpecificMatch,
error => {
UINotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Something went wrong while trying to restore the previous layout.',
type: 'info',
duration: 3000,
});
}
);
},
toggleStackImageSync: ({ toggledState }) => {
if (!toggledState) {
STACK_IMAGE_SYNC_GROUPS_INFO.forEach(syncGroupInfo => {
const { viewports, synchronizerId } = syncGroupInfo;
viewports.forEach(({ viewportId, renderingEngineId }) => {
SyncGroupService.removeViewportFromSyncGroup(
viewportId,
renderingEngineId,
synchronizerId
);
});
});
return;
}
STACK_IMAGE_SYNC_GROUPS_INFO = [];
// create synchronization groups and add viewports
let { viewports } = ViewportGridService.getState();
// filter empty viewports
viewports = viewports.filter(
viewport =>
viewport.displaySetInstanceUIDs &&
viewport.displaySetInstanceUIDs.length
);
// filter reconstructable viewports
viewports = viewports.filter(viewport => {
const { displaySetInstanceUIDs } = viewport;
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
if (displaySet && displaySet.isReconstructable) {
return true;
}
return false;
}
});
const viewportsByOrientation = viewports.reduce((acc, viewport) => {
const { viewportId, viewportType } = viewport.viewportOptions;
if (viewportType !== 'stack') {
console.warn('Viewport is not a stack, cannot sync images yet');
return acc;
}
const { element } = CornerstoneViewportService.getViewportInfo(
viewportId
);
const { viewport: csViewport, renderingEngineId } = getEnabledElement(
element
);
const { viewPlaneNormal } = csViewport.getCamera();
// Should we round here? I guess so, but not sure how much precision we need
const orientation = viewPlaneNormal.map(v => Math.round(v)).join(',');
if (!acc[orientation]) {
acc[orientation] = [];
}
acc[orientation].push({ viewportId, renderingEngineId });
return acc;
}, {});
// create synchronizer for each group
Object.values(viewportsByOrientation).map(viewports => {
let synchronizerId = viewports
.map(({ viewportId }) => viewportId)
.join(',');
synchronizerId = `imageSync_${synchronizerId}`;
calculateViewportRegistrations(viewports);
viewports.forEach(({ viewportId, renderingEngineId }) => {
SyncGroupService.addViewportToSyncGroup(
viewportId,
renderingEngineId,
{
type: 'stackimage',
id: synchronizerId,
source: true,
target: true,
}
);
});
STACK_IMAGE_SYNC_GROUPS_INFO.push({
synchronizerId,
viewports,
});
toggleStackImageSync({
getEnabledElement,
servicesManager,
toggledState,
});
},
toggleReferenceLines: ({ toggledState }) => {
@ -635,11 +412,6 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [],
options: {},
},
toggleCrosshairs: {
commandFn: actions.toggleCrosshairs,
storeContexts: [],
options: {},
},
rotateViewportCW: {
commandFn: actions.rotateViewport,
storeContexts: [],

View File

@ -362,13 +362,13 @@ export default async function init({
const viewportId = `viewport-${viewportIndex}`;
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) {
return;
}
// check if reference lines are active
const referenceLinesEnabled =
toolGroup._toolInstances?.['ReferenceLines'].mode ===
toolGroup._toolInstances['ReferenceLines'].mode ===
Enums.ToolModes.Enabled;
if (!referenceLinesEnabled) {

View File

@ -590,6 +590,8 @@ class SegmentationService {
// Note: ideally we could use the TypedArray set method, but since each
// slice can have multiple segments, we need to loop over each slice and
// set the segment value for each segment.
let overlappingSegments = false;
const _segmentInfoUpdate = (segmentInfo, segmentIndex) => {
const { pixelData: segPixelData } = segmentInfo;
@ -636,6 +638,10 @@ class SegmentationService {
i++, j++
) {
if (functionGroupPixelData[j] !== 0) {
if (derivedVolumeScalarData[i] !== 0) {
overlappingSegments = true;
}
derivedVolumeScalarData[i] = segmentIndex;
// centroid calculations
@ -716,6 +722,7 @@ class SegmentationService {
this._broadcastEvent(EVENTS.SEGMENTATION_PIXEL_DATA_CREATED, {
segmentationId,
segDisplaySet,
overlappingSegments,
});
return this.addOrUpdateSegmentation(segmentationSchema, suppressEvents);

View File

@ -77,17 +77,14 @@ class CornerstoneViewportService implements IViewportService {
viewportOptions: PublicViewportOptions,
elementRef: HTMLDivElement
) {
const viewportId =
viewportOptions.viewportId || this.getViewportId(viewportIndex);
const viewportInfo = new ViewportInfo(viewportIndex, viewportId);
const viewportInfo = new ViewportInfo(
viewportIndex,
viewportOptions.viewportId
);
viewportInfo.setElement(elementRef);
this.viewportsInfo.set(viewportIndex, viewportInfo);
}
public getViewportId(viewportIndex: number): string {
return `viewport-${viewportIndex}`;
}
public getViewportIds(): string[] {
const viewportIds = [];
@ -176,6 +173,22 @@ class CornerstoneViewportService implements IViewportService {
): void {
const renderingEngine = this.getRenderingEngine();
const viewportInfo = this.viewportsInfo.get(viewportIndex);
let viewportId = viewportInfo.getViewportId();
// if currently there is a viewport with the viewportId, but it is not the same
// as the one we are trying to set, we need to disable the old one
// and enable the new one, we could ideally change the name of the viewportId
// but the viewportId is an integral part in renderers map, tools svg cache
// etc. which would require a lot of refactoring, for now we will just disable
// the old one and enable the new one at the end of this function
let newViewportId = null;
if (publicViewportOptions?.viewportId !== viewportId) {
newViewportId = publicViewportOptions.viewportId;
viewportInfo.setViewportId(newViewportId);
renderingEngine.disableElement(viewportId);
}
viewportInfo.setRenderingEngineId(renderingEngine.id);
const {
@ -196,7 +209,7 @@ class CornerstoneViewportService implements IViewportService {
viewportIndex,
});
const viewportId = viewportInfo.getViewportId();
viewportId = viewportInfo.getViewportId();
const element = viewportInfo.getElement();
const type = viewportInfo.getViewportType();
const background = viewportInfo.getBackground();
@ -218,7 +231,8 @@ class CornerstoneViewportService implements IViewportService {
// ENABLED_ELEMENT again and again, which will run onEnableElement callbacks
renderingEngine.enableElement(viewportInput);
this._setDisplaySets(viewportId, viewportData, viewportInfo);
const viewport = renderingEngine.getViewport(viewportId);
this._setDisplaySets(viewport, viewportData, viewportInfo);
}
public getCornerstoneViewport(
@ -616,12 +630,10 @@ class CornerstoneViewportService implements IViewportService {
}
_setDisplaySets(
viewportId: string,
viewport: StackViewport | VolumeViewport,
viewportData: StackViewportData | VolumeViewportData,
viewportInfo: ViewportInfo
): void {
const viewport = this.getCornerstoneViewport(viewportId);
if (viewport instanceof StackViewport) {
this._setStackViewport(
viewport,
@ -687,10 +699,6 @@ class CornerstoneViewportService implements IViewportService {
} {
const viewportIndex = viewportInfo.getViewportIndex();
if (!publicViewportOptions.viewportId) {
publicViewportOptions.viewportId = this.getViewportId(viewportIndex);
}
// Creating a temporary viewportInfo to handle defaults
const newViewportInfo = new ViewportInfo(
viewportIndex,

View File

@ -0,0 +1,285 @@
import { Enums } from '@cornerstonejs/tools';
import removeToolGroupSegmentationRepresentations from '../removeToolGroupSegmentationRepresentations';
const MPR_TOOLGROUP_ID = 'mpr';
const cachedState = {
protocol: null,
stage: null,
viewportMatchDetails: null,
viewportStructure: null,
toolOptions: null,
};
const setCachedState = (
protocol,
stage,
viewportMatchDetails,
viewportStructure,
toolOptions
) => {
cachedState.protocol = protocol;
cachedState.stage = stage;
cachedState.viewportMatchDetails = viewportMatchDetails;
cachedState.viewportStructure = viewportStructure;
cachedState.toolOptions = JSON.parse(JSON.stringify(toolOptions));
};
const resetCachedState = () => {
cachedState.protocol = null;
cachedState.stage = null;
cachedState.viewportMatchDetails = null;
cachedState.viewportStructure = null;
cachedState.toolOptions = null;
};
export default function toggleMPRHangingProtocol({
toggledState,
servicesManager,
getToolGroup,
}) {
const {
UINotificationService,
HangingProtocolService,
ViewportGridService,
ToolBarService,
} = servicesManager.services;
const {
activeViewportIndex,
viewports,
numRows,
numCols,
} = ViewportGridService.getState();
const viewportDisplaySetInstanceUIDs =
viewports[activeViewportIndex].displaySetInstanceUIDs;
const errorCallback = error => {
UINotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Cannot create MPR for this DisplaySet since it is not reconstructable.',
type: 'info',
duration: 3000,
});
};
// What is the current active protocol and stage number to restore later
const { protocol, stage } = HangingProtocolService.getActiveProtocol();
if (toggledState) {
resetCachedState();
const {
viewportMatchDetails,
viewportStructure,
toolOptions,
} = _getViewportsInfo({
protocol,
stage,
viewports,
servicesManager,
});
setCachedState(
protocol,
stage,
viewportMatchDetails,
viewportStructure,
toolOptions
);
const matchDetails = {
displaySetInstanceUIDs: viewportDisplaySetInstanceUIDs,
};
_disableCrosshairs(
toolOptions.map(({ toolGroupId }) => toolGroupId),
getToolGroup
);
HangingProtocolService.setProtocol(
MPR_TOOLGROUP_ID,
matchDetails,
errorCallback
);
return;
}
const restoreErrorCallback = error => {
UINotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Something went wrong while trying to restore the previous layout.',
type: 'info',
duration: 3000,
});
};
_disableCrosshairs([MPR_TOOLGROUP_ID], getToolGroup);
const { layoutType, properties } = cachedState.viewportStructure;
const { viewportMatchDetails } = cachedState;
// The reason we split the flow here is that we don't allow viewport grid
// change in the non default hanging protocol, so we can just apply the
// cached protocol and stage. However, for the default protocol, we need
// to also apply the layout type and properties.
if (cachedState.protocol.id !== 'default') {
HangingProtocolService.setProtocol(
cachedState.protocol.id,
viewportMatchDetails,
restoreErrorCallback
);
return;
}
HangingProtocolService.setProtocol(
'default',
viewportMatchDetails,
restoreErrorCallback
);
if (numRows !== properties.rows || numCols !== properties.columns) {
ViewportGridService.setLayout({
numRows: properties.rows,
numCols: properties.columns,
layoutType,
layoutOptions: properties.layoutOptions,
});
}
const numViewports =
properties.layoutOptions.length || properties.rows * properties.columns;
// loop inside viewportMatchDetails map
// and set the viewportOptions for each viewport
[...Array(numViewports).keys()].forEach(viewportIndex => {
const viewportMatchDetailsForViewport = viewportMatchDetails.get(
viewportIndex
);
if (viewportMatchDetailsForViewport) {
const {
viewportOptions,
displaySetsInfo,
} = viewportMatchDetailsForViewport;
ViewportGridService.setDisplaySetsForViewport({
viewportIndex,
displaySetInstanceUIDs: displaySetsInfo.map(
displaySetInfo => displaySetInfo.displaySetInstanceUID
),
viewportOptions,
});
} else {
ViewportGridService.setDisplaySetsForViewport({
viewportIndex,
displaySetInstanceUIDs: [],
viewportOptions: {},
});
}
});
ToolBarService.recordInteraction({
groupId: 'WindowLevel',
itemId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
//clear segmentations if they exist
removeToolGroupSegmentationRepresentations(MPR_TOOLGROUP_ID);
}
function _disableCrosshairs(toolGroupIds, getToolGroup) {
toolGroupIds.forEach(toolGroupId => {
const toolGroup = getToolGroup(toolGroupId);
if (
toolGroup.getToolInstance('Crosshairs')?.mode === Enums.ToolModes.Active
) {
toolGroup.setToolDisabled('Crosshairs');
}
});
}
function _getViewportsInfo({ protocol, stage, viewports, servicesManager }) {
// here we need to use the viewports and try to map it into the
// viewportMatchDetails and displaySetMatch that HangingProtocolService
// expects
const {
ViewportGridService,
HangingProtocolService,
ToolGroupService,
} = servicesManager.services;
const { numRows, numCols } = ViewportGridService.getState();
let viewportMatchDetails = new Map();
const viewportStructure = {
layoutType: 'grid',
properties: {
rows: numRows,
columns: numCols,
layoutOptions: [],
},
};
viewports.forEach((viewport, viewportIndex) => {
viewportStructure.properties.layoutOptions.push({
x: viewport.x,
y: viewport.y,
width: viewport.width,
height: viewport.height,
});
});
if (protocol.id === 'default') {
viewports.forEach((viewport, viewportIndex) => {
if (viewport.displaySetInstanceUIDs) {
viewportMatchDetails.set(viewportIndex, {
displaySetsInfo: viewport.displaySetInstanceUIDs.map(
displaySetInstanceUID => {
return { displaySetInstanceUID };
}
),
viewportOptions: viewport.viewportOptions,
});
}
});
} else {
({ viewportMatchDetails } = HangingProtocolService.getMatchDetails());
}
// get the toolGroup state for viewports
let toolOptions = [];
const viewportIds = viewports
.map(
viewport =>
viewport.displaySetInstanceUIDs &&
viewport.displaySetInstanceUIDs.length > 0 &&
viewport.viewportOptions?.viewportId
)
.filter(Boolean);
if (viewportIds.length) {
toolOptions = viewportIds.map(viewportId => {
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId);
return {
toolGroupId: toolGroup.id,
toolOptions: toolGroup.toolOptions,
};
});
}
return { viewportMatchDetails, viewportStructure, toolOptions };
}

View File

@ -0,0 +1,115 @@
import calculateViewportRegistrations from './calculateViewportRegistrations';
// [ {
// synchronizerId: string,
// viewports: [ { viewportId: number, renderingEngineId: string, index: number } , ...]
// ]}
let STACK_IMAGE_SYNC_GROUPS_INFO = [];
export default function toggleStackImageSync({
toggledState,
servicesManager,
getEnabledElement,
}) {
const {
SyncGroupService,
ViewportGridService,
DisplaySetService,
CornerstoneViewportService,
} = servicesManager.services;
if (!toggledState) {
STACK_IMAGE_SYNC_GROUPS_INFO.forEach(syncGroupInfo => {
const { viewports, synchronizerId } = syncGroupInfo;
viewports.forEach(({ viewportId, renderingEngineId }) => {
SyncGroupService.removeViewportFromSyncGroup(
viewportId,
renderingEngineId,
synchronizerId
);
});
});
return;
}
STACK_IMAGE_SYNC_GROUPS_INFO = [];
// create synchronization groups and add viewports
let { viewports } = ViewportGridService.getState();
// filter empty viewports
viewports = viewports.filter(
viewport =>
viewport.displaySetInstanceUIDs && viewport.displaySetInstanceUIDs.length
);
// filter reconstructable viewports
viewports = viewports.filter(viewport => {
const { displaySetInstanceUIDs } = viewport;
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
if (displaySet && displaySet.isReconstructable) {
return true;
}
return false;
}
});
const viewportsByOrientation = viewports.reduce((acc, viewport) => {
const { viewportId, viewportType } = viewport.viewportOptions;
if (viewportType !== 'stack') {
console.warn('Viewport is not a stack, cannot sync images yet');
return acc;
}
const { element } = CornerstoneViewportService.getViewportInfo(viewportId);
const { viewport: csViewport, renderingEngineId } = getEnabledElement(
element
);
const { viewPlaneNormal } = csViewport.getCamera();
// Should we round here? I guess so, but not sure how much precision we need
const orientation = viewPlaneNormal.map(v => Math.round(v)).join(',');
if (!acc[orientation]) {
acc[orientation] = [];
}
acc[orientation].push({ viewportId, renderingEngineId });
return acc;
}, {});
// create synchronizer for each group
Object.values(viewportsByOrientation).map(viewports => {
let synchronizerId = viewports
.map(({ viewportId }) => viewportId)
.join(',');
synchronizerId = `imageSync_${synchronizerId}`;
calculateViewportRegistrations(viewports);
viewports.forEach(({ viewportId, renderingEngineId }) => {
SyncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, {
type: 'stackimage',
id: synchronizerId,
source: true,
target: true,
});
});
STACK_IMAGE_SYNC_GROUPS_INFO.push({
synchronizerId,
viewports,
});
});
}

View File

@ -92,7 +92,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs');
const StudyInstanceUIDs =
queryStudyInstanceUIDs.length && queryStudyInstanceUIDs ||
(queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) ||
paramsStudyInstanceUIDs;
const StudyInstanceUIDsAsArray =
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)

View File

@ -7,7 +7,7 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
HangingProtocolService,
UINotificationService,
ViewportGridService,
DisplaySetService
DisplaySetService,
} = servicesManager.services;
const actions = {
@ -30,16 +30,13 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
},
openDICOMTagViewer() {
const { activeViewportIndex, viewports } = ViewportGridService.getState();
const activeViewportSpecificData =
viewports[activeViewportIndex];
const {
displaySetInstanceUIDs,
} = activeViewportSpecificData;
const activeViewportSpecificData = viewports[activeViewportIndex];
const { displaySetInstanceUIDs } = activeViewportSpecificData;
const displaySets = DisplaySetService.activeDisplaySets;
const { UIModalService } = servicesManager.services;
const displaySetInstanceUID = displaySetInstanceUIDs[0]
const displaySetInstanceUID = displaySetInstanceUIDs[0];
UIModalService.show({
content: DicomTagBrowser,
contentProps: {
@ -47,9 +44,9 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
displaySetInstanceUID,
onClose: UIModalService.hide,
},
title: 'DICOM Tag Browser'
title: 'DICOM Tag Browser',
});
}
},
};
const definitions = {
@ -75,7 +72,7 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
},
openDICOMTagViewer: {
commandFn: actions.openDICOMTagViewer,
}
},
};
return {

View File

@ -32,8 +32,8 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"classnames": "^2.2.6",
"@cornerstonejs/core": "^0.21.0",
"@cornerstonejs/tools": "^0.29.2",
"@cornerstonejs/core": "^0.21.4",
"@cornerstonejs/tools": "^0.29.6",
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
"dcmjs": "^0.28.3",
"prop-types": "^15.6.2",

View File

@ -29,6 +29,7 @@ function TrackedCornerstoneViewport(props) {
servicesManager,
extensionManager,
commandsManager,
viewportOptions,
} = props;
const { t } = useTranslation('TrackedViewport');
@ -50,8 +51,7 @@ function TrackedCornerstoneViewport(props) {
const [element, setElement] = useState(null);
const { trackedSeries } = trackedMeasurements.context;
const viewportId = CornerstoneViewportService.getViewportId(viewportIndex);
const viewportId = viewportOptions.viewportId;
const {
Modality,
@ -86,8 +86,6 @@ function TrackedCornerstoneViewport(props) {
cineService.playClip(element, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(element);
}
};

View File

@ -91,8 +91,6 @@ function modeFactory({ modeConfiguration }) {
});
};
// Since we only have one viewport for the basic cs3d mode and it has
// only one hanging protocol, we can just use the first viewport
const { unsubscribe } = ToolGroupService.subscribe(
ToolGroupService.EVENTS.VIEWPORT_ADDED,
() => {

View File

@ -4,9 +4,10 @@ export const toolGroupIds = {
Fusion: 'fusionToolGroup',
MIP: 'mipToolGroup',
default: 'default',
// MPR: 'mpr',
};
function initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) {
function _initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) {
const tools = {
active: [
{
@ -43,7 +44,7 @@ function initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) {
[toolNames.Crosshairs]: {
viewportIndicators: false,
autoPan: {
enabled: true,
enabled: false,
panSize: 10,
},
},
@ -122,4 +123,70 @@ function initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) {
);
}
function initMPRToolGroup(toolNames, Enums, ToolGroupService, commandsManager) {
const tools = {
active: [
{
toolName: toolNames.WindowLevel,
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
},
{
toolName: toolNames.Pan,
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
},
{
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] },
],
passive: [
{ toolName: toolNames.Length },
{ toolName: toolNames.ArrowAnnotate },
{ toolName: toolNames.Bidirectional },
{ toolName: toolNames.DragProbe },
{ toolName: toolNames.EllipticalROI },
{ toolName: toolNames.RectangleROI },
{ toolName: toolNames.StackScroll },
{ toolName: toolNames.Angle },
{ toolName: toolNames.SegmentationDisplay },
],
disabled: [{ toolName: toolNames.Crosshairs }],
// enabled
// disabled
};
const toolsConfig = {
[toolNames.Crosshairs]: {
viewportIndicators: false,
autoPan: {
enabled: false,
panSize: 10,
},
},
[toolNames.ArrowAnnotate]: {
getTextCallback: (callback, eventDetails) =>
commandsManager.runCommand('arrowTextCallback', {
callback,
eventDetails,
}),
changeTextCallback: (data, eventDetails, callback) =>
commandsManager.runCommand('arrowTextCallback', {
callback,
data,
eventDetails,
}),
},
};
ToolGroupService.createToolGroupAndAddTools('mpr', tools, toolsConfig);
}
function initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) {
_initToolGroups(toolNames, Enums, ToolGroupService, commandsManager);
// initMPRToolGroup(toolNames, Enums, ToolGroupService, commandsManager);
}
export default initToolGroups;

View File

@ -105,6 +105,7 @@ const toolbarButtons = [
toolGroupIds.CT,
toolGroupIds.PT,
toolGroupIds.Fusion,
// toolGroupIds.MPR,
]),
],
'Length'
@ -125,6 +126,7 @@ const toolbarButtons = [
toolGroupIds.CT,
toolGroupIds.PT,
toolGroupIds.Fusion,
// toolGroupIds.MPR,
]),
],
'Length Tool'
@ -138,6 +140,7 @@ const toolbarButtons = [
toolGroupIds.CT,
toolGroupIds.PT,
toolGroupIds.Fusion,
// toolGroupIds.MPR,
]),
],
'Bidirectional Tool'
@ -151,6 +154,7 @@ const toolbarButtons = [
toolGroupIds.CT,
toolGroupIds.PT,
toolGroupIds.Fusion,
// toolGroupIds.MPR,
]),
],
'Arrow Annotate'
@ -164,6 +168,7 @@ const toolbarButtons = [
toolGroupIds.CT,
toolGroupIds.PT,
toolGroupIds.Fusion,
// toolGroupIds.MPR,
]),
],
'Ellipse Tool'
@ -184,10 +189,27 @@ const toolbarButtons = [
toolGroupIds.CT,
toolGroupIds.PT,
toolGroupIds.Fusion,
// toolGroupIds.MPR,
]),
],
},
},
{
id: 'MPR',
type: 'ohif.action',
props: {
type: 'toggle',
icon: 'icon-mpr',
label: 'MPR',
commands: [
{
commandName: 'toggleMPR',
commandOptions: {},
context: 'CORNERSTONE',
},
],
},
},
// Window Level + Presets...
{
id: 'WindowLevel',
@ -203,6 +225,7 @@ const toolbarButtons = [
toolGroupIds.CT,
toolGroupIds.PT,
toolGroupIds.Fusion,
// toolGroupIds.MPR,
]),
],
'Window Level'
@ -236,6 +259,7 @@ const toolbarButtons = [
toolGroupIds.CT,
toolGroupIds.PT,
toolGroupIds.Fusion,
// toolGroupIds.MPR,
]),
],
},
@ -253,6 +277,7 @@ const toolbarButtons = [
toolGroupIds.CT,
toolGroupIds.PT,
toolGroupIds.Fusion,
// toolGroupIds.MPR,
]),
],
},

View File

@ -122,6 +122,13 @@ class HangingProtocolService {
this.displaySetMatchDetails = new Map();
}
public getActiveProtocol(): {
protocol: HangingProtocol.Protocol;
stage: number;
} {
return { protocol: this.protocol, stage: this.stage };
}
public getDefaultProtocol(): HangingProtocol.Protocol {
return this.getProtocolById('default');
}

View File

@ -20,6 +20,10 @@ export default function sortInstances(instances: Array<any>) {
ImageOrientationPatient,
} = instances[Math.floor(instances.length / 2)]; // this prevents getting scout image as test image
if (!referenceImagePositionPatient || !ImageOrientationPatient) {
return instances;
}
const rowCosineVec = vec3.fromValues(
ImageOrientationPatient[0],
ImageOrientationPatient[1],

View File

@ -52,5 +52,4 @@ const utilityModule = extensionManager.getModuleEntry(
);
const { CornerstoneViewportService } = utilityModule.exports;
const viewportId = CornerstoneViewportService.getViewportId(viewportIndex);
```

View File

@ -33,27 +33,47 @@ const variantClasses = {
},
outlined: {
default:
'border bg-trasparent border-primary-light text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
'border border-primary-light text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
primary:
'border bg-transparent border-primary-main text-primary-main hover:opacity-80 active:opacity-100 focus:opacity-80',
'border border-primary-main text-primary-main hover:opacity-80 active:opacity-100 focus:opacity-80',
secondary:
'border bg-transparent border-secondary-light text-secondary-light hover:opacity-80 active:opacity-100 focus:opacity-80',
'border border-secondary-light text-secondary-light hover:opacity-80 active:opacity-100 focus:opacity-80',
white:
'border bg-transparent border-white text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
'border border-white text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
black:
'border bg-black border-primary-main text-white hover:bg-primary-main focus:bg-primary-main hover:border-black focus:border-black',
'border border-primary-main text-white hover:bg-primary-main focus:bg-primary-main hover:border-black focus:border-black',
},
contained: {
default:
'bg-primary-light text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
primary:
'bg-primary-main text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
default: 'text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
primary: 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
secondary:
'bg-secondary-light text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
white:
'bg-white text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
black:
'bg-black text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
'text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
white: 'text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
black: 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
},
};
const backgroundClasses = {
text: {
default: '',
primary: '',
secondary: '',
white: '',
black: '',
},
outlined: {
default: 'bg-transparent',
primary: 'bg-transparent',
secondary: 'bg-transparent',
white: 'bg-transparent',
black: 'bg-black',
},
contained: {
default: 'bg-primary-light',
primary: 'bg-primary-main',
secondary: 'bg-secondary-light',
white: 'bg-white',
black: 'bg-black',
},
};
@ -90,6 +110,7 @@ const IconButton = ({
className,
name,
id,
bgColor,
...rest
}) => {
const buttonElement = useRef(null);
@ -98,6 +119,9 @@ const IconButton = ({
buttonElement.current.blur();
onClick(e);
};
const bgColorToUse = bgColor ? bgColor : backgroundClasses[variant][color];
return (
<button
className={classnames(
@ -107,6 +131,7 @@ const IconButton = ({
sizeClasses[size],
fullWidthClasses[fullWidth],
disabledClasses[disabled],
bgColorToUse,
className
)}
style={{

View File

@ -5,7 +5,7 @@ import classnames from 'classnames';
import { IconButton, Icon, Tooltip } from '../';
const ToolbarButton = ({
type,
type = 'tool',
id,
icon,
label,
@ -20,14 +20,19 @@ const ToolbarButton = ({
const { primaryToolId } = bState;
const isActive = _isActive || (type === 'tool' && id === primaryToolId);
const classes = {
type: {
primary: isActive
? 'text-black'
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
secondary: isActive
? 'text-black'
: 'text-white hover:bg-secondary-dark hover:text-white focus:bg-secondary-dark focus:text-white',
},
tool: isActive
? 'text-black'
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
toggle: isActive
? 'text-[#348CFD]'
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
action: isActive
? 'text-black'
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
};
const bgClasses = {
toggle: isActive && 'bg-transparent',
};
const activeClass = isActive ? 'active' : '';
@ -42,8 +47,9 @@ const ToolbarButton = ({
>
<IconButton
variant={isActive ? 'contained' : 'text'}
bgColor={bgClasses[type]}
size="toolbar"
className={classnames('mx-1', activeClass, classes.type[type])}
className={classnames('mx-1', activeClass, classes[type])}
onClick={() => {
onInteraction({
itemId: id,

View File

@ -64,6 +64,10 @@ export function ViewportGridProvider({ children, service }) {
const viewports = state.viewports.slice();
if (!viewportOptions.viewportId) {
viewportOptions.viewportId = `viewport-${viewportIndex}`;
}
// merge the displaySetOptions and viewportOptions and displaySetInstanceUIDs
// into the viewport object at the given index
viewports[viewportIndex] = {

View File

@ -4,7 +4,7 @@ window.config = {
extensions: [],
modes: [],
showStudyList: true,
// maxNumberOfWebWorkers: 3,
maxNumberOfWebWorkers: 4,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showLoadingIndicator: true,

View File

@ -2383,28 +2383,28 @@
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-0.1.1.tgz#5bd1c52a33a425299299e970312731fa0cc2711b"
integrity sha512-HOMMOLV6xy8O/agNGGvrl0a8DwShpBvWxAzEzv2pqq12d3r5z/3MyIgNA3Oj/8bIBVvvVXxh9RX7rMDRHJdowg==
"@cornerstonejs/core@^0.21.0":
version "0.21.0"
resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.21.0.tgz#12f58f7552a5664d8c9085df2a8c12558c89d70e"
integrity sha512-p1aRTJxu1ZWV5uM5EctA870g37Okjpat4xDGfcqTupI2DgwVbCRNvJmuf44gRmBjVeHImDCybMMXXi9nWsCmLw==
"@cornerstonejs/core@^0.21.4":
version "0.21.4"
resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.21.4.tgz#d1dab2c985c382495a8ec91a28f2d62872cdb350"
integrity sha512-8D8QyK5bjqxefSVr3vLzsp2MUprmABAbX4qpvpp1XRmC76i1dKKR8Yo9G9jdreR5xwKvJN7pY6/n1F2ZYSN48w==
dependencies:
detect-gpu "^4.0.45"
lodash.clonedeep "4.5.0"
"@cornerstonejs/streaming-image-volume-loader@^0.6.1":
version "0.6.1"
resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.6.1.tgz#a8240c38607648dbc9983b7998e4de2d0ba916fa"
integrity sha512-QBuT22dojEH5A0UKgbuYleMEBjbVTJH7bunL6ePcgxI0DY82Lq87qVOMtODfFcHUomBvUlBjP08U/6uP7VJYtA==
"@cornerstonejs/streaming-image-volume-loader@^0.6.5":
version "0.6.5"
resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.6.5.tgz#f3f2ab362bf494e4072a146b45acedce0197fb36"
integrity sha512-dowoZYz0tMVtuUQl2WIZjOC/2TC0zMgas8KtpVcxJcGEu9HWA9oatgPlY0WqtFniDU7Oxnyy7OqYawtEoSSwdw==
dependencies:
"@cornerstonejs/core" "^0.21.0"
"@cornerstonejs/core" "^0.21.4"
cornerstone-wado-image-loader "^4.2.1"
"@cornerstonejs/tools@^0.29.2":
version "0.29.2"
resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.29.2.tgz#6655a91ab82681bd585ad7469ec3a2d372b45521"
integrity sha512-sy1pK6YG+vWhJvESDNkiVDnLLpEwgou3LG1VAXIYXvA5JBUVG98LVXBWeAFY8TqdseSrnc/nwW+VQPk899wG5g==
"@cornerstonejs/tools@^0.29.6":
version "0.29.6"
resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.29.6.tgz#6cfcb6b56284c4d1265330993ecafa3f48c429b8"
integrity sha512-Rmz4Nx6IfH+31C7fsGxLWwQXQ+w1nN5WmILb9/N7WB4M5mYUZvYiopXdWII4mzh91nHXcEfEiJimaihyuCVLJQ==
dependencies:
"@cornerstonejs/core" "^0.21.0"
"@cornerstonejs/core" "^0.21.4"
lodash.clonedeep "4.5.0"
lodash.get "^4.4.2"