Segmentation UI for VTKjs (#1685)
* Add single viewport configuration * Multiple viewport configuration * Improve performance by using independent set methods * Add jump to slice command * Add context configuration * Cache panel visibility * Fix sync between vtk and cornerstone * Remove apis index * Add approach * Add loading to update volumes * Fix broken configuration * Bump vtk version * Use loading label * Update cy tests after vtk loading label changed * Remove loading for segs
This commit is contained in:
parent
19a8e71317
commit
42c22df1b6
@ -38,6 +38,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"gl-matrix": "^3.3.0",
|
||||||
"react-select": "^3.0.8"
|
"react-select": "^3.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { TableListItem, Icon } from '@ohif/ui';
|
import { TableListItem, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
@ -19,6 +19,11 @@ ColoredCircle.propTypes = {
|
|||||||
|
|
||||||
const SegmentItem = ({ index, label, onClick, itemClass, color, visible = true, onVisibilityChange }) => {
|
const SegmentItem = ({ index, label, onClick, itemClass, color, visible = true, onVisibilityChange }) => {
|
||||||
const [isVisible, setIsVisible] = useState(visible);
|
const [isVisible, setIsVisible] = useState(visible);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsVisible(visible);
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dcmseg-segment-item">
|
<div className="dcmseg-segment-item">
|
||||||
<TableListItem
|
<TableListItem
|
||||||
|
|||||||
@ -32,7 +32,11 @@ const refreshViewport = () => {
|
|||||||
* @param {Array} props.viewports - Viewports data (viewportSpecificData)
|
* @param {Array} props.viewports - Viewports data (viewportSpecificData)
|
||||||
* @param {number} props.activeIndex - Active viewport index
|
* @param {number} props.activeIndex - Active viewport index
|
||||||
* @param {boolean} props.isOpen - Boolean that indicates if the panel is expanded
|
* @param {boolean} props.isOpen - Boolean that indicates if the panel is expanded
|
||||||
* @param {Function} props.onSegItemClick - Segment click handler
|
* @param {Function} props.onSegmentItemClick - Segment click handler
|
||||||
|
* @param {Function} props.onSegmentVisibilityChange - Segment visibiliy change handler
|
||||||
|
* @param {Function} props.onConfigurationChange - Configuration change handler
|
||||||
|
* @param {Function} props.activeContexts - List of active application contexts
|
||||||
|
* @param {Function} props.contexts - List of available application contexts
|
||||||
* @returns component
|
* @returns component
|
||||||
*/
|
*/
|
||||||
const SegmentationPanel = ({
|
const SegmentationPanel = ({
|
||||||
@ -40,25 +44,40 @@ const SegmentationPanel = ({
|
|||||||
viewports,
|
viewports,
|
||||||
activeIndex,
|
activeIndex,
|
||||||
isOpen,
|
isOpen,
|
||||||
onSegItemClick,
|
onSegmentItemClick,
|
||||||
UINotificationService,
|
onSegmentVisibilityChange,
|
||||||
|
onConfigurationChange,
|
||||||
|
onDisplaySetLoadFailure,
|
||||||
|
onSelectedSegmentationChange,
|
||||||
|
activeContexts,
|
||||||
|
contexts,
|
||||||
}) => {
|
}) => {
|
||||||
|
const isVTK = () => activeContexts.includes(contexts.VTK);
|
||||||
|
const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TODO: wrap get/set interactions with the cornerstoneTools
|
* TODO: wrap get/set interactions with the cornerstoneTools
|
||||||
* store with context to make these kind of things less blurry.
|
* store with context to make these kind of things less blurry.
|
||||||
*/
|
*/
|
||||||
const { configuration } = cornerstoneTools.getModule('segmentation');
|
const { configuration } = cornerstoneTools.getModule('segmentation');
|
||||||
const DEFAULT_BRUSH_RADIUS = configuration.radius || 10;
|
const DEFAULT_BRUSH_RADIUS = configuration.radius || 10;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: We shouldn't hardcode brushColor color, in the future
|
||||||
|
* the SEG may set the colorLUT to whatever it wants.
|
||||||
|
*/
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
brushRadius: DEFAULT_BRUSH_RADIUS,
|
brushRadius: DEFAULT_BRUSH_RADIUS,
|
||||||
brushColor:
|
brushColor:
|
||||||
'rgba(221, 85, 85, 1)' /* TODO: We shouldn't hardcode this color, in the future the SEG may set the colorLUT to whatever it wants. */,
|
'rgba(221, 85, 85, 1)',
|
||||||
selectedSegment: null,
|
selectedSegment: null,
|
||||||
selectedSegmentation: null,
|
selectedSegmentation: null,
|
||||||
showSegSettings: false,
|
showSegmentationSettings: false,
|
||||||
brushStackState: null,
|
brushStackState: null,
|
||||||
labelmapList: [],
|
labelmapList: [],
|
||||||
segmentList: [],
|
segmentList: [],
|
||||||
|
cachedSegmentsProperties: [],
|
||||||
|
isLoading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -75,6 +94,25 @@ const SegmentationPanel = ({
|
|||||||
updateState('brushStackState', module.state.series[firstImageId]);
|
updateState('brushStackState', module.state.series[firstImageId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Improve the way we notify parts of the app that depends on segs to be loaded.
|
||||||
|
*
|
||||||
|
* Currently we are using a non-ideal implementation through a custom event to notify the segmentation panel
|
||||||
|
* or other components that could rely on loaded segmentations that
|
||||||
|
* the segments were loaded so that e.g. when the user opens the panel
|
||||||
|
* before the segments are fully loaded, the panel can subscribe to this custom event
|
||||||
|
* and update itself with the new segments.
|
||||||
|
*
|
||||||
|
* This limitation is due to the fact that the cs segmentation module is an object (which will be
|
||||||
|
* updated after the segments are loaded) that React its not aware of its changes
|
||||||
|
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||||
|
*
|
||||||
|
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||||
|
* allows us to easily watch the module or the segmentations loading process in any other component
|
||||||
|
* without subscribing to external events.
|
||||||
|
*/
|
||||||
|
document.addEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* These are specific to each element;
|
* These are specific to each element;
|
||||||
* Need to iterate cornerstone-tools tracked enabled elements?
|
* Need to iterate cornerstone-tools tracked enabled elements?
|
||||||
@ -88,6 +126,7 @@ const SegmentationPanel = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
document.removeEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations);
|
||||||
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
||||||
enabledElement.removeEventListener(
|
enabledElement.removeEventListener(
|
||||||
'cornerstonetoolslabelmapmodified',
|
'cornerstonetoolslabelmapmodified',
|
||||||
@ -95,9 +134,9 @@ const SegmentationPanel = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
});
|
}, [activeIndex, viewports]);
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshSegmentations = useCallback(() => {
|
||||||
const module = cornerstoneTools.getModule('segmentation');
|
const module = cornerstoneTools.getModule('segmentation');
|
||||||
const activeViewport = viewports[activeIndex];
|
const activeViewport = viewports[activeIndex];
|
||||||
const studyMetadata = studyMetadataManager.get(
|
const studyMetadata = studyMetadataManager.get(
|
||||||
@ -107,7 +146,6 @@ const SegmentationPanel = ({
|
|||||||
activeViewport.displaySetInstanceUID
|
activeViewport.displaySetInstanceUID
|
||||||
);
|
);
|
||||||
const brushStackState = module.state.series[firstImageId];
|
const brushStackState = module.state.series[firstImageId];
|
||||||
|
|
||||||
if (brushStackState) {
|
if (brushStackState) {
|
||||||
const labelmap3D =
|
const labelmap3D =
|
||||||
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
||||||
@ -136,19 +174,20 @@ const SegmentationPanel = ({
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
studies,
|
|
||||||
viewports,
|
viewports,
|
||||||
activeIndex,
|
activeIndex,
|
||||||
getLabelmapList,
|
state.isLoading
|
||||||
getSegmentList,
|
|
||||||
state.selectedSegmentation,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshSegmentations();
|
||||||
|
}, [viewports, activeIndex, state.selectedSegmentation, activeContexts, state.isLoading]);
|
||||||
|
|
||||||
/* Handle open/closed panel behaviour */
|
/* Handle open/closed panel behaviour */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
showSegSettings: state.showSegSettings && !isOpen,
|
showSegmentationSettings: state.showSegmentationSettings && !isOpen,
|
||||||
}));
|
}));
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
@ -183,7 +222,8 @@ const SegmentationPanel = ({
|
|||||||
displaySet,
|
displaySet,
|
||||||
firstImageId,
|
firstImageId,
|
||||||
brushStackState.activeLabelmapIndex,
|
brushStackState.activeLabelmapIndex,
|
||||||
UINotificationService
|
() => onSelectedSegmentationChange(),
|
||||||
|
onDisplaySetLoadFailure
|
||||||
);
|
);
|
||||||
updateState('selectedSegmentation', activatedLabelmapIndex);
|
updateState('selectedSegmentation', activatedLabelmapIndex);
|
||||||
},
|
},
|
||||||
@ -262,65 +302,104 @@ const SegmentationPanel = ({
|
|||||||
: prev;
|
: prev;
|
||||||
});
|
});
|
||||||
|
|
||||||
const enabledElements = cornerstone.getEnabledElements();
|
if (isCornerstone()) {
|
||||||
const element = enabledElements[activeIndex].element;
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
const element = enabledElements[activeIndex].element;
|
||||||
|
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||||
|
|
||||||
if (!toolState) {
|
if (!toolState) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIds = toolState.data[0].imageIds;
|
||||||
|
const imageId = imageIds[closest];
|
||||||
|
const frameIndex = imageIds.indexOf(imageId);
|
||||||
|
|
||||||
|
const SOPInstanceUID = cornerstone.metaData.get(
|
||||||
|
'SOPInstanceUID',
|
||||||
|
imageId
|
||||||
|
);
|
||||||
|
const StudyInstanceUID = cornerstone.metaData.get(
|
||||||
|
'StudyInstanceUID',
|
||||||
|
imageId
|
||||||
|
);
|
||||||
|
|
||||||
|
onSegmentItemClick({
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
activeViewportIndex: activeIndex,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageIds = toolState.data[0].imageIds;
|
if (isVTK()) {
|
||||||
const imageId = imageIds[closest];
|
const activeViewport = viewports[activeIndex];
|
||||||
const frameIndex = imageIds.indexOf(imageId);
|
const studyMetadata = studyMetadataManager.get(
|
||||||
|
activeViewport.StudyInstanceUID
|
||||||
|
);
|
||||||
|
const allDisplaySets = studyMetadata.getDisplaySets();
|
||||||
|
const currentDisplaySet = allDisplaySets.find(
|
||||||
|
displaySet =>
|
||||||
|
displaySet.displaySetInstanceUID ===
|
||||||
|
activeViewport.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
const SOPInstanceUID = cornerstone.metaData.get(
|
const frame = labelmap3D.labelmaps2D[closest];
|
||||||
'SOPInstanceUID',
|
|
||||||
imageId
|
|
||||||
);
|
|
||||||
const StudyInstanceUID = cornerstone.metaData.get(
|
|
||||||
'StudyInstanceUID',
|
|
||||||
imageId
|
|
||||||
);
|
|
||||||
|
|
||||||
onSegItemClick({
|
onSegmentItemClick({
|
||||||
StudyInstanceUID,
|
studies,
|
||||||
SOPInstanceUID,
|
StudyInstanceUID: currentDisplaySet.StudyInstanceUID,
|
||||||
frameIndex,
|
displaySetInstanceUID: currentDisplaySet.displaySetInstanceUID,
|
||||||
activeViewportIndex: activeIndex,
|
SOPClassUID: viewports[activeIndex].sopClassUIDs[0],
|
||||||
});
|
SOPInstanceUID: currentDisplaySet.SOPInstanceUID,
|
||||||
|
segmentNumber,
|
||||||
|
frameIndex: closest,
|
||||||
|
frame,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const enabledElements = cornerstone.getEnabledElements();
|
const isSegmentVisible = () => {
|
||||||
const enabledElementViewport = enabledElements[activeIndex];
|
return !labelmap3D.segmentsHidden[segmentIndex];
|
||||||
|
};
|
||||||
|
|
||||||
let isVisible = true;
|
const toggleSegmentVisibility = () => {
|
||||||
if (enabledElementViewport) {
|
const segmentsHidden = labelmap3D.segmentsHidden;
|
||||||
const element = enabledElementViewport.element;
|
segmentsHidden[segmentIndex] = !segmentsHidden[segmentIndex];
|
||||||
const module = cornerstoneTools.getModule('segmentation');
|
return !segmentsHidden[segmentIndex];
|
||||||
isVisible = module.getters.isSegmentVisible(
|
};
|
||||||
element,
|
|
||||||
segmentNumber,
|
const cachedSegmentProperties = state.cachedSegmentsProperties[segmentNumber];
|
||||||
brushStackState.activeLabelmapIndex
|
let visible = isSegmentVisible();
|
||||||
);
|
if (cachedSegmentProperties && cachedSegmentProperties.visible !== visible) {
|
||||||
|
toggleSegmentVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
segmentList.push(
|
segmentList.push(
|
||||||
<SegmentItem
|
<SegmentItem
|
||||||
key={segmentNumber}
|
key={segmentNumber}
|
||||||
itemClass={`segment-item ${sameSegment && 'selected'}`}
|
itemClass={`segment-item ${sameSegment && 'selected'}`}
|
||||||
onClick={setCurrentSelectedSegment}
|
onClick={() => setCurrentSelectedSegment()}
|
||||||
label={segmentLabel}
|
label={segmentLabel}
|
||||||
index={segmentNumber}
|
index={segmentNumber}
|
||||||
color={color}
|
color={color}
|
||||||
visible={isVisible}
|
visible={visible}
|
||||||
onVisibilityChange={() => {
|
onVisibilityChange={newVisibility => {
|
||||||
const element = enabledElements[activeIndex].element;
|
if (isCornerstone()) {
|
||||||
module.setters.toggleSegmentVisibility(
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
element,
|
const element = enabledElements[activeIndex].element;
|
||||||
segmentNumber,
|
module.setters.toggleSegmentVisibility(
|
||||||
brushStackState.activeLabelmapIndex
|
element,
|
||||||
);
|
segmentNumber,
|
||||||
|
brushStackState.activeLabelmapIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVTK()) {
|
||||||
|
onSegmentVisibilityChange(segmentNumber, newVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCachedSegmentsProperties(segmentNumber, { visible: newVisibility });
|
||||||
refreshViewport();
|
refreshViewport();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -336,9 +415,25 @@ const SegmentationPanel = ({
|
|||||||
* Show default name
|
* Show default name
|
||||||
*/
|
*/
|
||||||
},
|
},
|
||||||
[activeIndex, onSegItemClick, state.selectedSegment]
|
[activeIndex, onSegmentItemClick, state.selectedSegment, state.isLoading]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateCachedSegmentsProperties = (segmentNumber, properties) => {
|
||||||
|
const segmentsProperties = state.cachedSegmentsProperties;
|
||||||
|
const segmentProperties = state.cachedSegmentsProperties[segmentNumber];
|
||||||
|
|
||||||
|
segmentsProperties[segmentNumber] =
|
||||||
|
segmentProperties ?
|
||||||
|
{ ...segmentProperties, ...properties } :
|
||||||
|
properties;
|
||||||
|
|
||||||
|
updateState('cachedSegmentsProperties', segmentsProperties);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateState('cachedSegmentsProperties', []);
|
||||||
|
}, [activeContexts]);
|
||||||
|
|
||||||
const updateState = (field, value) => {
|
const updateState = (field, value) => {
|
||||||
setState(state => ({ ...state, [field]: value }));
|
setState(state => ({ ...state, [field]: value }));
|
||||||
};
|
};
|
||||||
@ -387,7 +482,6 @@ const SegmentationPanel = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateConfiguration = newConfiguration => {
|
const updateConfiguration = newConfiguration => {
|
||||||
/* Supported configuration */
|
|
||||||
configuration.renderFill = newConfiguration.renderFill;
|
configuration.renderFill = newConfiguration.renderFill;
|
||||||
configuration.renderOutline = newConfiguration.renderOutline;
|
configuration.renderOutline = newConfiguration.renderOutline;
|
||||||
configuration.shouldRenderInactiveLabelmaps =
|
configuration.shouldRenderInactiveLabelmaps =
|
||||||
@ -397,14 +491,17 @@ const SegmentationPanel = ({
|
|||||||
configuration.outlineWidth = newConfiguration.outlineWidth;
|
configuration.outlineWidth = newConfiguration.outlineWidth;
|
||||||
configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive;
|
configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive;
|
||||||
configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive;
|
configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive;
|
||||||
|
onConfigurationChange(newConfiguration);
|
||||||
refreshViewport();
|
refreshViewport();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (state.showSegSettings) {
|
const disabledConfigurationFields = ['outlineAlpha', 'shouldRenderInactiveLabelmaps'];
|
||||||
|
if (state.showSegmentationSettings) {
|
||||||
return (
|
return (
|
||||||
<SegmentationSettings
|
<SegmentationSettings
|
||||||
|
disabledFields={isVTK() ? disabledConfigurationFields : []}
|
||||||
configuration={configuration}
|
configuration={configuration}
|
||||||
onBack={() => updateState('showSegSettings', false)}
|
onBack={() => updateState('showSegmentationSettings', false)}
|
||||||
onChange={updateConfiguration}
|
onChange={updateConfiguration}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -416,7 +513,7 @@ const SegmentationPanel = ({
|
|||||||
name="cog"
|
name="cog"
|
||||||
width="25px"
|
width="25px"
|
||||||
height="25px"
|
height="25px"
|
||||||
onClick={() => updateState('showSegSettings', true)}
|
onClick={() => updateState('showSegmentationSettings', true)}
|
||||||
/>
|
/>
|
||||||
{false && (
|
{false && (
|
||||||
<form className="selector-form">
|
<form className="selector-form">
|
||||||
@ -525,7 +622,8 @@ const _setActiveLabelmap = async (
|
|||||||
displaySet,
|
displaySet,
|
||||||
firstImageId,
|
firstImageId,
|
||||||
activeLabelmapIndex,
|
activeLabelmapIndex,
|
||||||
UINotificationService
|
callback = () => { },
|
||||||
|
onDisplaySetLoadFailure
|
||||||
) => {
|
) => {
|
||||||
if (displaySet.labelmapIndex === activeLabelmapIndex) {
|
if (displaySet.labelmapIndex === activeLabelmapIndex) {
|
||||||
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
|
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
|
||||||
@ -539,12 +637,7 @@ const _setActiveLabelmap = async (
|
|||||||
const loadPromise = displaySet.load(viewportSpecificData, studies);
|
const loadPromise = displaySet.load(viewportSpecificData, studies);
|
||||||
|
|
||||||
loadPromise.catch(error => {
|
loadPromise.catch(error => {
|
||||||
UINotificationService.show({
|
onDisplaySetLoadFailure(error);
|
||||||
title: 'DICOM Segmentation Loader',
|
|
||||||
message: error.message,
|
|
||||||
type: 'error',
|
|
||||||
autoClose: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return old index.
|
// Return old index.
|
||||||
return activeLabelmapIndex;
|
return activeLabelmapIndex;
|
||||||
@ -559,6 +652,8 @@ const _setActiveLabelmap = async (
|
|||||||
|
|
||||||
refreshViewport();
|
refreshViewport();
|
||||||
|
|
||||||
|
callback();
|
||||||
|
|
||||||
return displaySet.labelmapIndex;
|
return displaySet.labelmapIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Range } from '@ohif/ui';
|
|||||||
|
|
||||||
import './SegmentationSettings.css';
|
import './SegmentationSettings.css';
|
||||||
|
|
||||||
const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
const SegmentationSettings = ({ configuration, onBack, onChange, disabledFields = [] }) => {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
renderFill: configuration.renderFill,
|
renderFill: configuration.renderFill,
|
||||||
renderOutline: configuration.renderOutline,
|
renderOutline: configuration.renderOutline,
|
||||||
@ -70,28 +70,32 @@ const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
|||||||
/>
|
/>
|
||||||
{state.renderOutline && (
|
{state.renderOutline && (
|
||||||
<>
|
<>
|
||||||
<CustomRange
|
{!disabledFields.includes('outlineAlpha') && (
|
||||||
value={state.outlineAlpha * 100}
|
<CustomRange
|
||||||
label="Opacity"
|
value={state.outlineAlpha * 100}
|
||||||
showPercentage
|
label="Opacity"
|
||||||
step={1}
|
showPercentage
|
||||||
min={0}
|
step={1}
|
||||||
max={100}
|
min={0}
|
||||||
onChange={event => save('outlineAlpha', toFloat(event.target.value))}
|
max={100}
|
||||||
/>
|
onChange={event => save('outlineAlpha', toFloat(event.target.value))}
|
||||||
<CustomRange
|
/>
|
||||||
value={state.outlineWidth}
|
)}
|
||||||
label="Width"
|
{!disabledFields.includes('outlineWidth') && (
|
||||||
showValue
|
<CustomRange
|
||||||
step={1}
|
value={state.outlineWidth}
|
||||||
min={0}
|
label="Width"
|
||||||
max={5}
|
showValue
|
||||||
onChange={event => save('outlineWidth', parseInt(event.target.value))}
|
step={1}
|
||||||
/>
|
min={0}
|
||||||
|
max={5}
|
||||||
|
onChange={event => save('outlineWidth', parseInt(event.target.value))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(state.renderFill || state.renderOutline) && (
|
{(state.renderFill || state.renderOutline) && !disabledFields.includes('shouldRenderInactiveLabelmaps') && (
|
||||||
<div
|
<div
|
||||||
className="settings-group"
|
className="settings-group"
|
||||||
style={{ marginBottom: state.shouldRenderInactiveLabelmaps ? 15 : 0 }}
|
style={{ marginBottom: state.shouldRenderInactiveLabelmaps ? 15 : 0 }}
|
||||||
@ -103,7 +107,7 @@ const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
|||||||
/>
|
/>
|
||||||
{state.shouldRenderInactiveLabelmaps && (
|
{state.shouldRenderInactiveLabelmaps && (
|
||||||
<>
|
<>
|
||||||
{state.renderFill && (
|
{state.renderFill && !disabledFields.includes('fillAlphaInactive') && (
|
||||||
<CustomRange
|
<CustomRange
|
||||||
label="Fill Opacity"
|
label="Fill Opacity"
|
||||||
showPercentage
|
showPercentage
|
||||||
@ -114,7 +118,7 @@ const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
|||||||
onChange={event => save('fillAlphaInactive', toFloat(event.target.value))}
|
onChange={event => save('fillAlphaInactive', toFloat(event.target.value))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{state.renderOutline && (
|
{state.renderOutline && !disabledFields.includes('outlineAlphaInactive') && (
|
||||||
<CustomRange
|
<CustomRange
|
||||||
label="Outline Opacity"
|
label="Outline Opacity"
|
||||||
showPercentage
|
showPercentage
|
||||||
|
|||||||
@ -23,19 +23,56 @@ export default {
|
|||||||
getToolbarModule({ servicesManager }) {
|
getToolbarModule({ servicesManager }) {
|
||||||
return toolbarModule;
|
return toolbarModule;
|
||||||
},
|
},
|
||||||
getPanelModule({ commandsManager, servicesManager }) {
|
getPanelModule({ commandsManager, api, servicesManager }) {
|
||||||
|
const { UINotificationService } = servicesManager;
|
||||||
|
|
||||||
const ExtendedSegmentationPanel = props => {
|
const ExtendedSegmentationPanel = props => {
|
||||||
const segItemClickHandler = segData => {
|
const { activeContexts } = api.hooks.useAppContext();
|
||||||
commandsManager.runCommand('jumpToImage', segData);
|
|
||||||
|
const onDisplaySetLoadFailureHandler = error => {
|
||||||
|
UINotificationService.show({
|
||||||
|
title: 'DICOM Segmentation Loader',
|
||||||
|
message: error.message,
|
||||||
|
type: 'error',
|
||||||
|
autoClose: false,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { UINotificationService } = servicesManager.services;
|
const segmentItemClickHandler = data => {
|
||||||
|
commandsManager.runCommand('jumpToImage', data);
|
||||||
|
commandsManager.runCommand('jumpToSlice', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSegmentVisibilityChangeHandler = (segmentNumber, visible) => {
|
||||||
|
commandsManager.runCommand('setSegmentConfiguration', {
|
||||||
|
segmentNumber,
|
||||||
|
visible
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConfigurationChangeHandler = configuration => {
|
||||||
|
commandsManager.runCommand('setSegmentationConfiguration', {
|
||||||
|
globalOpacity: configuration.fillAlpha,
|
||||||
|
outlineThickness: configuration.outlineWidth,
|
||||||
|
renderOutline: configuration.renderOutline,
|
||||||
|
visible: configuration.renderFill
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectedSegmentationChangeHandler = () => {
|
||||||
|
commandsManager.runCommand('requestNewSegmentation');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SegmentationPanel
|
<SegmentationPanel
|
||||||
{...props}
|
{...props}
|
||||||
onSegItemClick={segItemClickHandler}
|
activeContexts={activeContexts}
|
||||||
UINotificationService={UINotificationService}
|
contexts={api.contexts}
|
||||||
|
onSegmentItemClick={segmentItemClickHandler}
|
||||||
|
onSegmentVisibilityChange={onSegmentVisibilityChangeHandler}
|
||||||
|
onConfigurationChange={onConfigurationChangeHandler}
|
||||||
|
onSelectedSegmentationChange={onSelectedSegmentationChangeHandler}
|
||||||
|
onDisplaySetLoadFailure={onDisplaySetLoadFailureHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -63,6 +63,27 @@ export default async function loadSegmentation(
|
|||||||
|
|
||||||
segDisplaySet.labelmapIndex = labelmapIndex;
|
segDisplaySet.labelmapIndex = labelmapIndex;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Improve the way we notify parts of the app that depends on segs to be loaded.
|
||||||
|
*
|
||||||
|
* Currently we are using a non-ideal implementation through a custom event to notify the segmentation panel
|
||||||
|
* or other components that could rely on loaded segmentations that
|
||||||
|
* the segments were loaded so that e.g. when the user opens the panel
|
||||||
|
* before the segments are fully loaded, the panel can subscribe to this custom event
|
||||||
|
* and update itself with the new segments.
|
||||||
|
*
|
||||||
|
* This limitation is due to the fact that the cs segmentation module is an object (which will be
|
||||||
|
* updated after the segments are loaded) that React its not aware of its changes
|
||||||
|
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||||
|
*
|
||||||
|
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||||
|
* allows us to easily watch the module or the segmentations loading process in any other component
|
||||||
|
* without subscribing to external events.
|
||||||
|
*/
|
||||||
|
console.log('Segmentation loaded.');
|
||||||
|
const event = new CustomEvent('extensiondicomsegmentationsegloaded');
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
|
||||||
resolve(labelmapIndex);
|
resolve(labelmapIndex);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"react-vtkjs-viewport": "^0.8.3"
|
"react-vtkjs-viewport": "^0.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohif/core": "^2.9.3",
|
"@ohif/core": "^2.9.3",
|
||||||
|
|||||||
@ -32,16 +32,16 @@ class LoadingIndicator extends PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="imageViewerLoadingIndicator loadingIndicator">
|
<div className="imageViewerLoadingIndicator loadingIndicator">
|
||||||
<div className="indicatorContents">
|
<div className="indicatorContents">
|
||||||
<p>
|
<p>
|
||||||
{this.props.t('Reformatting')}...
|
{this.props.t('Loading...')}
|
||||||
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
|
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
|
||||||
{percComplete}
|
{percComplete}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
state = {
|
state = {
|
||||||
volumes: null,
|
volumes: null,
|
||||||
paintFilterLabelMapImageData: null,
|
paintFilterLabelMapImageData: null,
|
||||||
paintFilterBackgroundImageData: null,
|
paintFilterBackgroundImageData: null
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -69,7 +69,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onScroll: () => {},
|
onScroll: () => { },
|
||||||
};
|
};
|
||||||
|
|
||||||
static id = 'OHIFVTKViewport';
|
static id = 'OHIFVTKViewport';
|
||||||
@ -156,6 +156,10 @@ class OHIFVTKViewport extends Component {
|
|||||||
const { activeLabelmapIndex } = brushStackState;
|
const { activeLabelmapIndex } = brushStackState;
|
||||||
const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex];
|
const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex];
|
||||||
|
|
||||||
|
this.segmentsDefaultProperties = labelmap3D.segmentsHidden.map(isHidden => {
|
||||||
|
return { visible: !isHidden };
|
||||||
|
});
|
||||||
|
|
||||||
const vtkLabelmapID = `${firstImageId}_${activeLabelmapIndex}`;
|
const vtkLabelmapID = `${firstImageId}_${activeLabelmapIndex}`;
|
||||||
|
|
||||||
if (labelmapCache[vtkLabelmapID]) {
|
if (labelmapCache[vtkLabelmapID]) {
|
||||||
@ -339,13 +343,13 @@ class OHIFVTKViewport extends Component {
|
|||||||
this.setStateFromProps();
|
this.setStateFromProps();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { displaySet } = this.props.viewportData;
|
const { displaySet } = this.props.viewportData;
|
||||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
displaySet.displaySetInstanceUID !==
|
displaySet.displaySetInstanceUID !==
|
||||||
prevDisplaySet.displaySetInstanceUID ||
|
prevDisplaySet.displaySetInstanceUID ||
|
||||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||||
) {
|
) {
|
||||||
@ -405,10 +409,6 @@ class OHIFVTKViewport extends Component {
|
|||||||
|
|
||||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||||
|
|
||||||
const visible = configuration.renderFill || configuration.renderOutline;
|
|
||||||
const opacity = configuration.fillAlpha;
|
|
||||||
const outlineThickness = configuration.outlineThickness;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
@ -428,10 +428,14 @@ class OHIFVTKViewport extends Component {
|
|||||||
dataDetails={this.state.dataDetails}
|
dataDetails={this.state.dataDetails}
|
||||||
labelmapRenderingOptions={{
|
labelmapRenderingOptions={{
|
||||||
colorLUT: this.state.labelmapColorLUT,
|
colorLUT: this.state.labelmapColorLUT,
|
||||||
globalOpacity: opacity,
|
globalOpacity: configuration.fillAlpha,
|
||||||
visible,
|
visible: configuration.renderFill,
|
||||||
outlineThickness,
|
outlineThickness: configuration.outlineWidth,
|
||||||
renderOutline: true,
|
renderOutline: configuration.renderOutline,
|
||||||
|
segmentsDefaultProperties: this.segmentsDefaultProperties,
|
||||||
|
onNewSegmentationRequested: () => {
|
||||||
|
this.setStateFromProps();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onScroll={this.props.onScroll}
|
onScroll={this.props.onScroll}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -5,10 +5,13 @@ import {
|
|||||||
vtkInteractorStyleMPRRotate,
|
vtkInteractorStyleMPRRotate,
|
||||||
vtkSVGCrosshairsWidget,
|
vtkSVGCrosshairsWidget,
|
||||||
} from 'react-vtkjs-viewport';
|
} from 'react-vtkjs-viewport';
|
||||||
|
import { getImageData } from 'react-vtkjs-viewport';
|
||||||
|
import { vec3 } from 'gl-matrix';
|
||||||
import setMPRLayout from './utils/setMPRLayout.js';
|
import setMPRLayout from './utils/setMPRLayout.js';
|
||||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||||
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
|
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
|
||||||
|
import OHIFVTKViewport from './OHIFVTKViewport';
|
||||||
|
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
|
||||||
|
|
||||||
const { BlendMode } = Constants;
|
const { BlendMode } = Constants;
|
||||||
|
|
||||||
@ -34,7 +37,6 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const displaySet = viewportSpecificData[activeViewportIndex];
|
const displaySet = viewportSpecificData[activeViewportIndex];
|
||||||
|
|
||||||
let api;
|
let api;
|
||||||
if (!api) {
|
if (!api) {
|
||||||
try {
|
try {
|
||||||
@ -99,6 +101,20 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _convertModelToWorldSpace = (position, vtkImageData) => {
|
||||||
|
const indexToWorld = vtkImageData.getIndexToWorld();
|
||||||
|
const pos = vec3.create();
|
||||||
|
|
||||||
|
position[0] += 0.5; /* Move to the centre of the voxel. */
|
||||||
|
position[1] += 0.5; /* Move to the centre of the voxel. */
|
||||||
|
position[2] += 0.5; /* Move to the centre of the voxel. */
|
||||||
|
|
||||||
|
vec3.set(pos, position[0], position[1], position[2]);
|
||||||
|
vec3.transformMat4(pos, pos, indexToWorld);
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
};
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
getVtkApis: ({ index }) => {
|
getVtkApis: ({ index }) => {
|
||||||
return apis[index];
|
return apis[index];
|
||||||
@ -124,6 +140,119 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
|
|
||||||
_setView(api, [0, 1, 0], [0, 0, 1]);
|
_setView(api, [0, 1, 0], [0, 0, 1]);
|
||||||
},
|
},
|
||||||
|
requestNewSegmentation: async ({ viewports }) => {
|
||||||
|
const allViewports = Object.values(viewports.viewportSpecificData);
|
||||||
|
const promises = allViewports.map(async (viewport, viewportIndex) => {
|
||||||
|
let api = apis[viewportIndex];
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
api = await _getActiveViewportVTKApi(viewports);
|
||||||
|
apis[viewportIndex] = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.requestNewSegmentation();
|
||||||
|
api.updateImage();
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
},
|
||||||
|
jumpToSlice: async ({
|
||||||
|
viewports,
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
SOPClassUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
segmentNumber,
|
||||||
|
frameIndex,
|
||||||
|
frame,
|
||||||
|
done = () => { }
|
||||||
|
}) => {
|
||||||
|
let api = apis[viewports.activeViewportIndex];
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
api = await _getActiveViewportVTKApi(viewports);
|
||||||
|
apis[viewports.activeViewportIndex] = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = OHIFVTKViewport.getCornerstoneStack(
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
SOPClassUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID);
|
||||||
|
|
||||||
|
let pixelIndex = 0;
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
const rows = imageDataObject.dimensions[1];
|
||||||
|
const cols = imageDataObject.dimensions[0];
|
||||||
|
|
||||||
|
for (let j = 0; j < rows; j++) {
|
||||||
|
for (let i = 0; i < cols; i++) {
|
||||||
|
// [i, j] =
|
||||||
|
const pixel = frame.pixelData[pixelIndex];
|
||||||
|
if (pixel === segmentNumber) {
|
||||||
|
x += i;
|
||||||
|
y += j;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
pixelIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x /= count;
|
||||||
|
y /= count;
|
||||||
|
|
||||||
|
const position = [x, y, frameIndex];
|
||||||
|
const worldPos = _convertModelToWorldSpace(position, imageDataObject.vtkImageData);
|
||||||
|
|
||||||
|
api.svgWidgets.crosshairsWidget.moveCrosshairs(worldPos, apis);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
setSegmentationConfiguration: async ({
|
||||||
|
viewports,
|
||||||
|
globalOpacity,
|
||||||
|
visible,
|
||||||
|
renderOutline,
|
||||||
|
outlineThickness,
|
||||||
|
}) => {
|
||||||
|
const allViewports = Object.values(viewports.viewportSpecificData);
|
||||||
|
const promises = allViewports.map(async (viewport, viewportIndex) => {
|
||||||
|
let api = apis[viewportIndex];
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
api = await _getActiveViewportVTKApi(viewports);
|
||||||
|
apis[viewportIndex] = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.setGlobalOpacity(globalOpacity);
|
||||||
|
api.setVisibility(visible);
|
||||||
|
api.setOutlineThickness(outlineThickness);
|
||||||
|
api.setOutlineRendering(renderOutline);
|
||||||
|
api.updateImage();
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
},
|
||||||
|
setSegmentConfiguration: async ({ viewports, visible, segmentNumber }) => {
|
||||||
|
const allViewports = Object.values(viewports.viewportSpecificData);
|
||||||
|
const promises = allViewports.map(async (viewport, viewportIndex) => {
|
||||||
|
let api = apis[viewportIndex];
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
api = await _getActiveViewportVTKApi(viewports);
|
||||||
|
apis[viewportIndex] = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.setSegmentVisibility(segmentNumber, visible);
|
||||||
|
api.updateImage();
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
},
|
||||||
enableRotateTool: () => {
|
enableRotateTool: () => {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||||
@ -280,6 +409,26 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
window.vtkActions = actions;
|
window.vtkActions = actions;
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
|
requestNewSegmentation: {
|
||||||
|
commandFn: actions.requestNewSegmentation,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
jumpToSlice: {
|
||||||
|
commandFn: actions.jumpToSlice,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
setSegmentationConfiguration: {
|
||||||
|
commandFn: actions.setSegmentationConfiguration,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
setSegmentConfiguration: {
|
||||||
|
commandFn: actions.setSegmentConfiguration,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
axial: {
|
axial: {
|
||||||
commandFn: actions.axial,
|
commandFn: actions.axial,
|
||||||
storeContexts: ['viewports'],
|
storeContexts: ['viewports'],
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export class StudyMetadata extends Metadata {
|
|||||||
Object.defineProperty(this, 'studyInstanceUID', {
|
Object.defineProperty(this, 'studyInstanceUID', {
|
||||||
configurable: false,
|
configurable: false,
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
get: function() {
|
get: function () {
|
||||||
return this.getStudyInstanceUID();
|
return this.getStudyInstanceUID();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import MODULE_TYPES from './MODULE_TYPES.js';
|
|||||||
import log from './../log.js';
|
import log from './../log.js';
|
||||||
|
|
||||||
export default class ExtensionManager {
|
export default class ExtensionManager {
|
||||||
constructor({ commandsManager, servicesManager, appConfig = {} }) {
|
constructor({ commandsManager, servicesManager, api, appConfig = {} }) {
|
||||||
this.modules = {};
|
this.modules = {};
|
||||||
this.registeredExtensionIds = [];
|
this.registeredExtensionIds = [];
|
||||||
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
||||||
@ -10,6 +10,7 @@ export default class ExtensionManager {
|
|||||||
this._commandsManager = commandsManager;
|
this._commandsManager = commandsManager;
|
||||||
this._servicesManager = servicesManager;
|
this._servicesManager = servicesManager;
|
||||||
this._appConfig = appConfig;
|
this._appConfig = appConfig;
|
||||||
|
this._api = api;
|
||||||
|
|
||||||
this.moduleTypeNames.forEach(moduleType => {
|
this.moduleTypeNames.forEach(moduleType => {
|
||||||
this.modules[moduleType] = [];
|
this.modules[moduleType] = [];
|
||||||
@ -119,6 +120,7 @@ export default class ExtensionManager {
|
|||||||
commandsManager: this._commandsManager,
|
commandsManager: this._commandsManager,
|
||||||
appConfig: this._appConfig,
|
appConfig: this._appConfig,
|
||||||
configuration,
|
configuration,
|
||||||
|
api: this._api
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!extensionModule) {
|
if (!extensionModule) {
|
||||||
|
|||||||
@ -21,8 +21,8 @@ describe('OHIF VTK Extension', () => {
|
|||||||
//Select 2D MPR button
|
//Select 2D MPR button
|
||||||
cy.get('[data-cy="2d mpr"]').click();
|
cy.get('[data-cy="2d mpr"]').click();
|
||||||
|
|
||||||
//Wait Reformatting Images
|
//Wait waitVTKLoading Images
|
||||||
cy.waitVTKReformatting();
|
cy.waitVTKLoading();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@ -18,8 +18,8 @@ describe('Visual Regression - OHIF VTK Extension', () => {
|
|||||||
//Select 2D MPR button
|
//Select 2D MPR button
|
||||||
cy.get('[data-cy="2d mpr"]').click();
|
cy.get('[data-cy="2d mpr"]').click();
|
||||||
|
|
||||||
//Wait Reformatting Images
|
//Wait waitVTKLoading Images
|
||||||
cy.waitVTKReformatting();
|
cy.waitVTKwaitVTKLoading();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@ -107,15 +107,15 @@ Cypress.Commands.add('waitStudyList', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('waitVTKReformatting', () => {
|
Cypress.Commands.add('waitVTKLoading', () => {
|
||||||
// Wait for start reformatting
|
// Wait for start loading
|
||||||
cy.get('[data-cy="viewprt-grid"]', { timeout: 10000 }).should($grid => {
|
cy.get('[data-cy="viewprt-grid"]', { timeout: 10000 }).should($grid => {
|
||||||
expect($grid).to.contain.text('Reform');
|
expect($grid).to.contain.text('Loading');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for finish reformatting
|
// Wait for finish loading
|
||||||
cy.get('[data-cy="viewprt-grid"]', { timeout: 30000 }).should($grid => {
|
cy.get('[data-cy="viewprt-grid"]', { timeout: 30000 }).should($grid => {
|
||||||
expect($grid).not.to.contain.text('Reform');
|
expect($grid).not.to.contain.text('Loading');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -53,7 +53,7 @@ import store from './store';
|
|||||||
/** Contexts */
|
/** Contexts */
|
||||||
import WhiteLabelingContext from './context/WhiteLabelingContext';
|
import WhiteLabelingContext from './context/WhiteLabelingContext';
|
||||||
import UserManagerContext from './context/UserManagerContext';
|
import UserManagerContext from './context/UserManagerContext';
|
||||||
import AppContext from './context/AppContext';
|
import { AppProvider, useAppContext, CONTEXTS } from './context/AppContext';
|
||||||
|
|
||||||
/** ~~~~~~~~~~~~~ Application Setup */
|
/** ~~~~~~~~~~~~~ Application Setup */
|
||||||
const commandsManagerConfig = {
|
const commandsManagerConfig = {
|
||||||
@ -159,8 +159,8 @@ class App extends Component {
|
|||||||
|
|
||||||
if (this._userManager) {
|
if (this._userManager) {
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ appConfig: this._appConfig }}>
|
<Provider store={store}>
|
||||||
<Provider store={store}>
|
<AppProvider config={this._appConfig}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<OidcProvider store={store} userManager={this._userManager}>
|
<OidcProvider store={store} userManager={this._userManager}>
|
||||||
<UserManagerContext.Provider value={this._userManager}>
|
<UserManagerContext.Provider value={this._userManager}>
|
||||||
@ -183,14 +183,14 @@ class App extends Component {
|
|||||||
</UserManagerContext.Provider>
|
</UserManagerContext.Provider>
|
||||||
</OidcProvider>
|
</OidcProvider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Provider>
|
</AppProvider>
|
||||||
</AppContext.Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ appConfig: this._appConfig }}>
|
<Provider store={store}>
|
||||||
<Provider store={store}>
|
<AppProvider config={this._appConfig}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<Router basename={routerBasename}>
|
<Router basename={routerBasename}>
|
||||||
<WhiteLabelingContext.Provider value={whiteLabeling}>
|
<WhiteLabelingContext.Provider value={whiteLabeling}>
|
||||||
@ -204,8 +204,8 @@ class App extends Component {
|
|||||||
</WhiteLabelingContext.Provider>
|
</WhiteLabelingContext.Provider>
|
||||||
</Router>
|
</Router>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Provider>
|
</AppProvider>
|
||||||
</AppContext.Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,6 +255,12 @@ function _initExtensions(extensions, cornerstoneExtensionConfig, appConfig) {
|
|||||||
commandsManager,
|
commandsManager,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
appConfig,
|
appConfig,
|
||||||
|
api: {
|
||||||
|
contexts: CONTEXTS,
|
||||||
|
hooks: {
|
||||||
|
useAppContext
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const requiredExtensions = [
|
const requiredExtensions = [
|
||||||
|
|||||||
@ -191,8 +191,8 @@ class OHIFStandaloneViewer extends Component {
|
|||||||
{match === null ? (
|
{match === null ? (
|
||||||
<></>
|
<></>
|
||||||
) : (
|
) : (
|
||||||
<Component match={match} location={this.props.location} />
|
<Component match={match} location={this.props.location} />
|
||||||
)}
|
)}
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
)}
|
)}
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
// TODO: REPLACE THIS WITH A CONTEXT PROVIDER
|
|
||||||
// EVERYTHING IN `VIEWER.JS` COULD USE THIS FOR APPROPRIATE CONTEXT
|
|
||||||
import ToolbarRow from './ToolbarRow';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { getActiveContexts } from './../store/layout/selectors.js';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
activeContexts: getActiveContexts(state),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedToolbarRow = connect(mapStateToProps)(ToolbarRow);
|
|
||||||
|
|
||||||
export default ConnectedToolbarRow;
|
|
||||||
@ -16,6 +16,7 @@ import { commandsManager, extensionManager } from './../App.js';
|
|||||||
|
|
||||||
import ConnectedCineDialog from './ConnectedCineDialog';
|
import ConnectedCineDialog from './ConnectedCineDialog';
|
||||||
import ConnectedLayoutButton from './ConnectedLayoutButton';
|
import ConnectedLayoutButton from './ConnectedLayoutButton';
|
||||||
|
import { withAppContext } from '../context/AppContext';
|
||||||
|
|
||||||
class ToolbarRow extends Component {
|
class ToolbarRow extends Component {
|
||||||
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
|
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
|
||||||
@ -381,5 +382,5 @@ function _handleBuiltIn(button) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation(['Common', 'ViewportDownloadForm'])(
|
export default withTranslation(['Common', 'ViewportDownloadForm'])(
|
||||||
withModal(withDialog(ToolbarRow))
|
withModal(withDialog(withAppContext(ToolbarRow)))
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import OHIF, { DICOMSR } from '@ohif/core';
|
|||||||
import { withDialog } from '@ohif/ui';
|
import { withDialog } from '@ohif/ui';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import ConnectedHeader from './ConnectedHeader.js';
|
import ConnectedHeader from './ConnectedHeader.js';
|
||||||
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
|
import ToolbarRow from './ToolbarRow.js';
|
||||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
||||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
||||||
import SidePanel from './../components/SidePanel.js';
|
import SidePanel from './../components/SidePanel.js';
|
||||||
@ -256,7 +256,7 @@ class Viewer extends Component {
|
|||||||
</WhiteLabelingContext.Consumer>
|
</WhiteLabelingContext.Consumer>
|
||||||
|
|
||||||
{/* TOOLBAR */}
|
{/* TOOLBAR */}
|
||||||
<ConnectedToolbarRow
|
<ToolbarRow
|
||||||
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
||||||
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
||||||
selectedLeftSidePanel={
|
selectedLeftSidePanel={
|
||||||
@ -307,11 +307,11 @@ class Viewer extends Component {
|
|||||||
activeIndex={this.props.activeViewportIndex}
|
activeIndex={this.props.activeViewportIndex}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConnectedStudyBrowser
|
<ConnectedStudyBrowser
|
||||||
studies={this.state.thumbnails}
|
studies={this.state.thumbnails}
|
||||||
studyMetadata={this.props.studies}
|
studyMetadata={this.props.studies}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
|
|
||||||
{/* MAIN */}
|
{/* MAIN */}
|
||||||
@ -349,7 +349,7 @@ export default withDialog(Viewer);
|
|||||||
* @param {Study[]} studies
|
* @param {Study[]} studies
|
||||||
* @param {DisplaySet[]} studies[].displaySets
|
* @param {DisplaySet[]} studies[].displaySets
|
||||||
*/
|
*/
|
||||||
const _mapStudiesToThumbnails = function(studies) {
|
const _mapStudiesToThumbnails = function (studies) {
|
||||||
return studies.map(study => {
|
return studies.map(study => {
|
||||||
const { StudyInstanceUID } = study;
|
const { StudyInstanceUID } = study;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,33 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { getActiveContexts } from '../store/layout/selectors.js';
|
||||||
|
|
||||||
let AppContext = React.createContext({});
|
let AppContext = React.createContext({});
|
||||||
|
|
||||||
|
export const CONTEXTS = {
|
||||||
|
CORNERSTONE: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||||
|
VTK: 'ACTIVE_VIEWPORT::VTK'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAppContext = () => useContext(AppContext);
|
||||||
|
|
||||||
|
export const AppProvider = ({ children, config }) => {
|
||||||
|
const activeContexts = useSelector(state => getActiveContexts(state));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContext.Provider value={{ appConfig: config, activeContexts }}>
|
||||||
|
{children}
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withAppContext = Component => {
|
||||||
|
return function WrappedComponent(props) {
|
||||||
|
const { appConfig, activeContexts } = useAppContext();
|
||||||
|
return (
|
||||||
|
<Component {...props} appConfig={appConfig} activeContexts={activeContexts} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default AppContext;
|
export default AppContext;
|
||||||
|
|||||||
15
yarn.lock
15
yarn.lock
@ -9110,6 +9110,11 @@ gl-matrix@^3.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.1.0.tgz#f5b2de17d8fed95a79e5025b10cded0ab9ccbed0"
|
resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.1.0.tgz#f5b2de17d8fed95a79e5025b10cded0ab9ccbed0"
|
||||||
integrity sha512-526NA+3EA+ztAQi0IZpSWiM0fyQXIp7IbRvfJ4wS/TjjQD0uv0fVybXwwqqSOlq33UckivI0yMDlVtboWm3k7A==
|
integrity sha512-526NA+3EA+ztAQi0IZpSWiM0fyQXIp7IbRvfJ4wS/TjjQD0uv0fVybXwwqqSOlq33UckivI0yMDlVtboWm3k7A==
|
||||||
|
|
||||||
|
gl-matrix@^3.3.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b"
|
||||||
|
integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA==
|
||||||
|
|
||||||
gl-preserve-state@^1.0.0:
|
gl-preserve-state@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz#4ef710d62873f1470ed015c6546c37dacddd4198"
|
resolved "https://registry.yarnpkg.com/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz#4ef710d62873f1470ed015c6546c37dacddd4198"
|
||||||
@ -15598,7 +15603,7 @@ react-codemirror2@^6.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-6.0.0.tgz#180065df57a64026026cde569a9708fdf7656525"
|
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-6.0.0.tgz#180065df57a64026026cde569a9708fdf7656525"
|
||||||
integrity sha512-D7y9qZ05FbUh9blqECaJMdDwKluQiO3A9xB+fssd5jKM7YAXucRuEOlX32mJQumUvHUkHRHqXIPBjm6g0FW0Ag==
|
integrity sha512-D7y9qZ05FbUh9blqECaJMdDwKluQiO3A9xB+fssd5jKM7YAXucRuEOlX32mJQumUvHUkHRHqXIPBjm6g0FW0Ag==
|
||||||
|
|
||||||
react-cornerstone-viewport@^2.3.8:
|
react-cornerstone-viewport@2.3.8:
|
||||||
version "2.3.8"
|
version "2.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-2.3.8.tgz#7af8360f29bca986ae4e36b4e503269b88ddc52f"
|
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-2.3.8.tgz#7af8360f29bca986ae4e36b4e503269b88ddc52f"
|
||||||
integrity sha512-aiG2uVNrDY6SQx4t/HBxIA3zsMsCwT+6TpcXK9qSSoXhs+X6OTmYEKncWUqL0jtxU1yfh6JTUz8ARTg03gtF+A==
|
integrity sha512-aiG2uVNrDY6SQx4t/HBxIA3zsMsCwT+6TpcXK9qSSoXhs+X6OTmYEKncWUqL0jtxU1yfh6JTUz8ARTg03gtF+A==
|
||||||
@ -16024,10 +16029,10 @@ react-transition-group@^4.1.1:
|
|||||||
loose-envify "^1.4.0"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
react-vtkjs-viewport@^0.8.3:
|
react-vtkjs-viewport@^0.9.0:
|
||||||
version "0.8.4"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-vtkjs-viewport/-/react-vtkjs-viewport-0.8.4.tgz#6bd9837d7762b845e77f053fb8fbf4469428d594"
|
resolved "https://registry.yarnpkg.com/react-vtkjs-viewport/-/react-vtkjs-viewport-0.9.0.tgz#5a7890643e511946f960db6b05142318be2dfe80"
|
||||||
integrity sha512-b90ENJzmStiahxVox+7mMwZij3NVUu//jE07PV2Qtp6E7q/eLXn6ZUwcEt9w5YHN36xLZVcx9b53ztLcXVgh/Q==
|
integrity sha512-kjpNr1n+eW0nU736HH07IsSYQXphxfn/RBvKtpBer9tNsl+FyLH8h+uY8KWdv8kVBHkEoaH6/srxB0JBa8l+jA==
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns "^2.2.1"
|
date-fns "^2.2.1"
|
||||||
gl-matrix "^3.1.0"
|
gl-matrix "^3.1.0"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user