fix(viewport): Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings (#4037)

This commit is contained in:
Alireza authored and GitHub committed 2024-04-16 11:04:48 -04:00
1 parent a6c6fff43a
commit f99a0bfb31
80 files changed
+1129 -801

No files matched your search

@@ -116,7 +116,10 @@ function OHIFCornerstoneRTViewport(props) {
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
}}
onElementEnabled={onElementEnabled}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
onElementDisabled={onElementDisabled}
></Component>
);
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.70.5",
"@cornerstonejs/core": "^1.70.5",
"@kitware/vtk.js": "30.3.1",
"@cornerstonejs/adapters": "^1.70.6",
"@cornerstonejs/core": "^1.70.6",
"@kitware/vtk.js": "30.3.3",
"react-color": "^2.19.3"
}
}
@@ -434,30 +434,6 @@ const commandsModule = ({
});
});
},
toggleThresholdRangeAndDynamic() {
const toolGroupIds = toolGroupService.getToolGroupIds();
if (!toolGroupIds) {
return;
}
toolGroupIds.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
const brushInstances = segmentationUtils.getBrushToolInstances(toolGroup.id);
brushInstances.forEach(({ configuration }) => {
const { activeStrategy, strategySpecificConfiguration } = configuration;
if (activeStrategy.startsWith('THRESHOLD')) {
const thresholdConfig = strategySpecificConfiguration.THRESHOLD;
if (thresholdConfig) {
thresholdConfig.isDynamic = !thresholdConfig.isDynamic;
}
}
});
});
},
};
const definitions = {
@@ -491,9 +467,6 @@ const commandsModule = ({
setThresholdRange: {
commandFn: actions.setThresholdRange,
},
toggleThresholdRangeAndDynamic: {
commandFn: actions.toggleThresholdRangeAndDynamic,
},
};
return {
@@ -59,12 +59,8 @@ function getToolNameForButton(button) {
const commands = props?.commands || button.commands;
const commandsArray = Array.isArray(commands) ? commands : [commands];
const firstCommand = commandsArray[0];
if (typeof firstCommand === 'string') {
// likely not a cornerstone tool
return null;
}
if ('commandOptions' in firstCommand) {
if (firstCommand?.commandOptions) {
return firstCommand.commandOptions.toolName ?? props?.id ?? button.id;
}
@@ -18,11 +18,13 @@ export default function PanelSegmentation({
extensionManager,
configuration,
}) {
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
const { segmentationService, viewportGridService, uiDialogService, displaySetService } =
servicesManager.services;
const { t } = useTranslation('PanelSegmentation');
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
const [addSegmentationClassName, setAddSegmentationClassName] = useState('');
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
segmentationService.getConfiguration()
);
@@ -52,6 +54,52 @@ export default function PanelSegmentation({
};
}, []);
// temporary measure to not allow add segmentation when the selected viewport
// is stack viewport
useEffect(() => {
const handleActiveViewportChange = viewportId => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
viewportId || viewportGridService.getActiveViewportId()
);
if (!displaySetUIDs) {
return;
}
const isReconstructable =
displaySetUIDs?.some(displaySetUID => {
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
return displaySet?.isReconstructable;
}) || false;
if (isReconstructable) {
setAddSegmentationClassName('');
} else {
setAddSegmentationClassName('ohif-disabled');
}
};
// Handle initial state
handleActiveViewportChange();
const changed = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED;
const ready = viewportGridService.EVENTS.VIEWPORTS_READY;
const subs = [];
[ready, changed].forEach(evt => {
const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => {
handleActiveViewportChange(viewportId);
});
subs.push(unsubscribe);
});
// Clean up
return () => {
subs.forEach(unsub => unsub());
};
}, []);
const getToolGroupIds = segmentationId => {
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
@@ -152,6 +200,7 @@ export default function PanelSegmentation({
segmentationService.removeSegment(segmentationId, segmentIndex);
};
// segment hide
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segmentInfo = segmentation.segments[segmentIndex];
@@ -257,6 +306,7 @@ export default function PanelSegmentation({
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAddWrapper}
addSegmentationClassName={addSegmentationClassName}
showAddSegment={allowAddSegment}
onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete}
@@ -112,7 +112,10 @@ function OHIFCornerstoneSEGViewport(props) {
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
}}
onElementEnabled={onElementEnabled}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
onElementDisabled={onElementDisabled}
></Component>
);
+3 -3
View File
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.70.5",
"@cornerstonejs/core": "^1.70.5",
"@cornerstonejs/tools": "^1.70.5",
"@cornerstonejs/adapters": "^1.70.6",
"@cornerstonejs/core": "^1.70.6",
"@cornerstonejs/tools": "^1.70.6",
"classnames": "^2.3.2"
}
}
@@ -65,7 +65,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
// Filter toolData to only render the data for the active SR.
const filteredAnnotations = annotations.filter(annotation =>
trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier)
trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier)
);
if (!viewport._actors?.size) {
@@ -82,8 +82,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
for (let i = 0; i < filteredAnnotations.length; i++) {
const annotation = filteredAnnotations[i];
const annotationUID = annotation.annotationUID;
const { renderableData } = annotation.data.cachedStats;
const { cachedStats } = annotation.data;
const { renderableData, TrackingUniqueIdentifier } = annotation.data;
const { referencedImageId } = annotation.metadata;
styleSpecifier.annotationUID = annotationUID;
@@ -95,7 +94,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
const color =
cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
? 'rgb(0, 255, 0)'
: this.getStyle('color', styleSpecifier, annotation);
@@ -51,10 +51,9 @@ export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameN
handles: {
textBox: measurement.textBox ?? {},
},
cachedStats: {
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
},
cachedStats: {},
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
frameNumber,
},
};
@@ -8,6 +8,7 @@ import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSR
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import { useAppConfig } from '@state';
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
@@ -203,7 +204,10 @@ function OHIFCornerstoneSRViewport(props) {
// The positionIds for the viewport aren't meaningful for the child display sets
positionIds: null,
}}
onElementEnabled={onElementEnabled}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
initialImageIndex={initialImageIndex}
isJumpToMeasurementDisabled={true}
></Component>
@@ -378,6 +382,10 @@ async function _getViewportReferencedDisplaySetData(
measurementSelected,
displaySetService
) {
const { measurements } = displaySet;
const measurement = measurements[measurementSelected];
const { displaySetInstanceUID } = measurement;
if (!displaySet.keyImageDisplaySet) {
// Create a new display set, and preserve a reference to it here,
// so that it can be re-displayed and shown inside the SR viewport.
@@ -42,9 +42,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^1.70.5",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.5",
"@cornerstonejs/tools": "^1.70.5",
"@cornerstonejs/core": "^1.70.6",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.6",
"@cornerstonejs/tools": "^1.70.6",
"classnames": "^2.3.2"
}
}
@@ -88,7 +88,7 @@ const DynamicVolumeControls = ({
className="w-1/2"
onClick={() => {
setComputedView(false);
onDynamicClick();
onDynamicClick?.();
}}
>
4D
@@ -202,7 +202,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager })
}
const { element } = viewportInfo;
cineService.playClip(element, { framesPerSecond: frameRate });
cineService.playClip(element, { framesPerSecond: frameRate, viewportId: activeViewportId });
};
const handleStop = () => {
+6 -6
View File
@@ -38,7 +38,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.70.5",
"@cornerstonejs/dicom-image-loader": "^1.70.6",
"@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.8.0-beta.80",
"@ohif/ui": "3.8.0-beta.80",
@@ -55,12 +55,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.70.5",
"@cornerstonejs/core": "^1.70.5",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.5",
"@cornerstonejs/tools": "^1.70.5",
"@cornerstonejs/adapters": "^1.70.6",
"@cornerstonejs/core": "^1.70.6",
"@cornerstonejs/streaming-image-volume-loader": "^1.70.6",
"@cornerstonejs/tools": "^1.70.6",
"@icr/polyseg-wasm": "^0.4.0",
"@kitware/vtk.js": "30.3.1",
"@kitware/vtk.js": "30.3.3",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",
"lodash.merge": "^4.6.2",
@@ -114,10 +114,21 @@ const OHIFCornerstoneViewport = React.memo(props => {
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIndex,
// if the viewport is part of a hanging protocol layout
// we should not really rely on the old synchronizers and
// you see below we only rehydrate the synchronizers if the viewport
// is not part of the hanging protocol layout. HPs should
// define their own synchronizers. Since the synchronizers are
// viewportId dependent and
isHangingProtocolLayout,
} = props;
const viewportId = viewportOptions.viewportId;
if (!viewportId) {
throw new Error('Viewport ID is required');
}
// Since we only have support for dynamic data in volume viewports, we should
// handle this case here and set the viewportType to volume if any of the
// displaySets are dynamic volumes
@@ -194,7 +205,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
const synchronizersStore = stateSyncService.getState().synchronizersStore;
if (synchronizersStore?.[viewportId]?.length) {
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
// If the viewport used to have a synchronizer, re apply it again
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
}
+42 -37
View File
@@ -51,6 +51,12 @@ function commandsModule({
function _getActiveViewportEnabledElement() {
return getActiveViewportEnabledElement(viewportGridService);
}
function _getActiveViewportToolGroupId() {
const viewport = _getActiveViewportEnabledElement();
return toolGroupService.getToolGroupForViewport(viewport.id);
}
const actions = {
/**
* Generates the selector props for the context menu, specific to
@@ -300,42 +306,45 @@ function commandsModule({
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
toggleEnabledDisabledToolbar({ value, itemId, toolGroupIds = [] }) {
toggleEnabledDisabledToolbar({ value, itemId, toolGroupId }) {
const toolName = itemId || value;
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
toolGroupIds.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return;
}
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId();
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return;
}
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
});
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
},
toggleActiveDisabledToolbar({ value, itemId, toolGroupIds = [] }) {
toggleActiveDisabledToolbar({ value, itemId, toolGroupId }) {
const toolName = itemId || value;
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
toolGroupIds.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return;
}
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId();
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return;
}
const toolIsActive = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Active;
const toolIsActive = [
Enums.ToolModes.Active,
Enums.ToolModes.Enabled,
Enums.ToolModes.Passive,
].includes(toolGroup.getToolOptions(toolName).mode);
toolIsActive
? toolGroup.setToolDisabled(toolName)
: actions.setToolActive({ toolName, toolGroupId });
toolIsActive
? toolGroup.setToolDisabled(toolName)
: actions.setToolActive({ toolName, toolGroupId });
// we should set the previously active tool to active after we set the
// current tool disabled
if (toolIsActive) {
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
// we should set the previously active tool to active after we set the
// current tool disabled
if (toolIsActive) {
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
if (prevToolName !== toolName) {
actions.setToolActive({ toolName: prevToolName, toolGroupId });
}
});
}
},
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
@@ -441,11 +450,9 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { flipHorizontal } = viewport.getCamera();
viewport.setCamera({ flipHorizontal: !flipHorizontal });
viewport.render();
}
const { flipHorizontal } = viewport.getCamera();
viewport.setCamera({ flipHorizontal: !flipHorizontal });
viewport.render();
},
flipViewportVertical: () => {
const enabledElement = _getActiveViewportEnabledElement();
@@ -456,11 +463,9 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
const { flipVertical } = viewport.getCamera();
viewport.setCamera({ flipVertical: !flipVertical });
viewport.render();
}
const { flipVertical } = viewport.getCamera();
viewport.setCamera({ flipVertical: !flipVertical });
viewport.render();
},
invertViewport: ({ element }) => {
let enabledElement;
@@ -819,7 +824,7 @@ function commandsModule({
}
crosshairInstances.forEach(ins => {
ins.resetCrosshairs();
ins?.resetCrosshairs();
});
},
};
@@ -21,7 +21,7 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
const validFrameRate = Math.max(frameRate, 1);
return isPlaying
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate })
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate, viewportId })
: cineService.stopClip(enabledVPElement);
};
+49 -30
View File
@@ -108,32 +108,27 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
};
},
},
{
name: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
evaluate: ({ viewportId, button, disabledText }) =>
_evaluateToggle({
viewportId,
button,
disabledText,
offModes: [Enums.ToolModes.Disabled],
toolGroupService,
}),
},
{
name: 'evaluate.cornerstoneTool.toggle',
evaluate: ({ viewportId, button, disabledText }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = getToolNameForButton(button);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isOff = [Enums.ToolModes.Disabled, Enums.ToolModes.Passive].includes(
toolGroup.getToolOptions(toolName).mode
);
return {
className: getToggledClassName(!isOff),
};
},
evaluate: ({ viewportId, button, disabledText }) =>
_evaluateToggle({
viewportId,
button,
disabledText,
offModes: [Enums.ToolModes.Disabled, Enums.ToolModes.Passive],
toolGroupService,
}),
},
{
name: 'evaluate.cornerstone.synchronizer',
@@ -146,7 +141,11 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
};
}
const synchronizerType = button?.commands?.[0]?.commandOptions?.type;
const isArray = Array.isArray(button.commands);
const synchronizerType = isArray
? button.commands?.[0].commandOptions.type
: button.commands?.commandOptions.type;
synchronizers = syncGroupService.getSynchronizersOfType(synchronizerType);
@@ -268,18 +267,38 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
];
}
function _evaluateToggle({ viewportId, button, disabledText, offModes, toolGroupService }) {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = getToolNameForButton(button);
if (!toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
return {
className: getToggledClassName(!isOff),
};
}
// Todo: this is duplicate, we should move it to a shared location
function getToolNameForButton(button) {
const { props } = button;
const commands = props?.commands || button.commands;
const commandsArray = Array.isArray(commands) ? commands : [commands];
const firstCommand = commandsArray[0];
if (typeof firstCommand === 'string') {
// likely not a cornerstone tool
return null;
}
if ('commandOptions' in firstCommand) {
if (firstCommand?.commandOptions) {
return firstCommand.commandOptions.toolName ?? props?.id ?? button.id;
}
+4 -1
View File
@@ -61,7 +61,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
*/
id,
onModeExit: (): void => {
onModeExit: ({ servicesManager }): void => {
const { cineService } = servicesManager.services;
// Empty out the image load and retrieval pools to prevent memory leaks
// on the mode exits
Object.values(cs3DEnums.RequestType).forEach(type => {
@@ -69,6 +70,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
imageRetrievalPoolManager.clearRequestStack(type);
});
cineService.setIsCineEnabled(false);
enabledElementReset();
},
+16 -1
View File
@@ -34,6 +34,8 @@ import { CornerstoneServices } from './types';
import initViewTiming from './utils/initViewTiming';
import { colormaps } from './utils/colormaps';
import { debounce } from 'lodash';
const { registerColormap } = csUtilities.colormap;
// TODO: Cypress tests are currently grabbing this from the window?
@@ -291,8 +293,21 @@ export default async function init({
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
colormaps.forEach(registerColormap);
// Create a debounced function that shows the notification
const debouncedShowNotification = debounce(detail => {
uiNotificationService.show({
title: detail.type,
message: detail.message,
type: 'error',
});
}, 300);
// Event listener
eventTarget.addEventListener(EVENTS.ERROR_EVENT, ({ detail }) => {
debouncedShowNotification(detail);
});
}
function CPUModal() {
@@ -59,8 +59,8 @@ function initCineService(servicesManager) {
return utilities.cine.playClip(element, playClipOptions);
};
const stopClip = element => {
return utilities.cine.stopClip(element);
const stopClip = (element, stopClipOptions) => {
return utilities.cine.stopClip(element, stopClipOptions);
};
cineService.setServiceImplementation({
@@ -45,7 +45,11 @@ class CornerstoneCacheService {
// as a reference volume, if so, we should hang a volume viewport
// instead of a stack viewport
if (this._shouldRenderSegmentation(displaySets)) {
viewportType = 'volume';
// if the viewport type is volume 3D, we should let it be as it is
// Todo: in future here we should kick start the conversion of the
// segmentation to closed surface
viewportType =
viewportType === Enums.ViewportType.VOLUME_3D ? Enums.ViewportType.VOLUME_3D : 'volume';
// update viewportOptions to reflect the new viewport type
viewportOptions.viewportType = viewportType;
@@ -183,7 +183,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { lutPresentation, positionPresentation } = presentations;
if (lutPresentation) {
const { presentation } = lutPresentation;
if (viewport instanceof VolumeViewport) {
if (viewport instanceof BaseVolumeViewport) {
if (presentation instanceof Map) {
presentation.forEach((properties, volumeId) => {
viewport.setProperties(properties, volumeId);
@@ -27,7 +27,11 @@ const CornerstoneViewportDownloadForm = ({
const activeViewportElement = enabledElement?.element;
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
const { viewportId: activeViewportId, renderingEngineId } = activeViewportEnabledElement;
const {
viewportId: activeViewportId,
renderingEngineId,
viewport: activeViewport,
} = activeViewportEnabledElement;
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
@@ -93,7 +97,7 @@ const CornerstoneViewportDownloadForm = ({
renderingEngine.resize();
// Trigger the render on the viewport to update the on screen
downloadViewport.resetCamera();
// downloadViewport.resetCamera();
downloadViewport.render();
downloadViewportElement.addEventListener(
@@ -120,6 +124,12 @@ const CornerstoneViewportDownloadForm = ({
resolve({ dataUrl, width: newWidth, height: newHeight });
downloadViewportElement.removeEventListener(Enums.Events.IMAGE_RENDERED, updateViewport);
// for some reason we need a reset camera here, and I don't know why
downloadViewport.resetCamera();
const presentation = activeViewport.getViewPresentation();
downloadViewport.setView(activeViewport.getViewReference(), presentation);
downloadViewport.render();
}
);
});
@@ -161,7 +171,6 @@ const CornerstoneViewportDownloadForm = ({
downloadViewport.addActor(actor);
});
downloadViewport.setCamera(viewport.getCamera());
downloadViewport.render();
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
@@ -51,20 +51,27 @@ export default function interleaveCenterLoader({
* listen to it and as the other viewports are created we can set the volumes for them
* since volumes are already started loading.
*/
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
const uniqueViewportVolumeDisplaySetUIDs = new Set();
viewportIdVolumeInputArrayMap.forEach((volumeInputArray, viewportId) => {
volumeInputArray.forEach(volumeInput => {
const { volumeId } = volumeInput;
uniqueViewportVolumeDisplaySetUIDs.add(volumeId);
});
});
const uniqueMatchedDisplaySetUIDs = new Set();
matchDetails.forEach(matchDetail => {
const { displaySetsInfo } = matchDetail;
displaySetsInfo.forEach(({ displaySetInstanceUID }) => {
uniqueMatchedDisplaySetUIDs.add(displaySetInstanceUID);
});
});
if (uniqueViewportVolumeDisplaySetUIDs.size !== uniqueMatchedDisplaySetUIDs.size) {
return;
}
// Check if all the matched volumes are loaded
for (const [_, details] of displaySetsMatchDetails.entries()) {
const { SeriesInstanceUID } = details;
// HangingProtocol has matched, but don't have all the volumes created yet, so return
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
return;
}
}
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
// get volumes from cache
const volumes = volumeIds.map(volumeId => {
@@ -75,23 +75,27 @@ export default function interleaveTopToBottom({
* listen to it and as the other viewports are created we can set the volumes for them
* since volumes are already started loading.
*/
if (filteredMatchDetails.length !== viewportIdVolumeInputArrayMap.size) {
const uniqueViewportVolumeDisplaySetUIDs = new Set();
viewportIdVolumeInputArrayMap.forEach((volumeInputArray, viewportId) => {
volumeInputArray.forEach(volumeInput => {
const { volumeId } = volumeInput;
uniqueViewportVolumeDisplaySetUIDs.add(volumeId);
});
});
const uniqueMatchedDisplaySetUIDs = new Set();
matchDetails.forEach(matchDetail => {
const { displaySetsInfo } = matchDetail;
displaySetsInfo.forEach(({ displaySetInstanceUID }) => {
uniqueMatchedDisplaySetUIDs.add(displaySetInstanceUID);
});
});
if (uniqueViewportVolumeDisplaySetUIDs.size !== uniqueMatchedDisplaySetUIDs.size) {
return;
}
// Check if all the matched volumes are loaded
for (const [_, details] of displaySetsMatchDetails.entries()) {
const { SeriesInstanceUID, displaySetInstanceUID } = details;
// HangingProtocol has matched, but don't have all the volumes created yet, so return
if (
displaySetsToLoad.has(displaySetInstanceUID) &&
!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)
) {
return;
}
}
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
// get volumes from cache
const volumes = volumeIds.map(volumeId => {
@@ -12,6 +12,9 @@ const defaultContextMenu = {
commands: [
{
commandName: 'deleteMeasurement',
// we only have support for cornerstoneTools context menu since
// they are svg based
context: 'CORNERSTONE',
},
],
},
@@ -227,7 +227,7 @@ export default function PanelMeasurementTable({
</div>
<div className="flex justify-center p-4">
<ActionButtons
t={t('MeasurementTable')}
t={t}
actions={[
{
label: 'Export',
@@ -5,13 +5,8 @@
function getImageSrcFromImageId(cornerstone, imageId) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
// Note: the default width and height of the canvas is 300x150
// but we need to set the width and height to the same number since
// the thumbnails are usually square and we want to maintain the aspect ratio
canvas.width = 128 / window.devicePixelRatio;
canvas.height = 128 / window.devicePixelRatio;
cornerstone.utilities
.loadImageToCanvas({ canvas, imageId })
.loadImageToCanvas({ canvas, imageId, thumbnail: true })
.then(imageId => {
resolve(canvas.toDataURL());
})
@@ -68,7 +68,6 @@ function getSopClassUids(instances) {
}
function _getDisplaySetsFromSeries(instances) {
debugger;
// If the series has no instances, stop here
if (!instances || !instances.length) {
throw new Error('No instances were provided');
@@ -34,19 +34,47 @@ const defaultCommonPresets = [
},
];
const generateAdvancedPresets = hangingProtocolService => {
const _areSelectorsValid = (hp, displaySets, hangingProtocolService) => {
if (!hp.displaySetSelectors || Object.values(hp.displaySetSelectors).length === 0) {
return true;
}
return hangingProtocolService.areRequiredSelectorsValid(
Object.values(hp.displaySetSelectors),
displaySets[0]
);
};
const generateAdvancedPresets = ({ servicesManager }) => {
const { hangingProtocolService, viewportGridService, displaySetService } =
servicesManager.services;
const hangingProtocols = Array.from(hangingProtocolService.protocols.values());
const viewportId = viewportGridService.getActiveViewportId();
if (!viewportId) {
return [];
}
const displaySetInsaneUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const displaySets = displaySetInsaneUIDs.map(uid => displaySetService.getDisplaySetByUID(uid));
return hangingProtocols
.map(hp => {
if (!hp.isPreset) {
return null;
}
const areValid = _areSelectorsValid(hp, displaySets, hangingProtocolService);
return {
icon: hp.icon,
title: hp.name,
commandOptions: {
protocolId: hp.id,
},
disabled: !areValid,
};
})
.filter(preset => preset !== null);
@@ -100,10 +128,10 @@ function LayoutSelector({
}) {
const [isOpen, setIsOpen] = useState(false);
const { customizationService, hangingProtocolService } = servicesManager.services;
const { customizationService } = servicesManager.services;
const commonPresets = customizationService.get('commonPresets') || defaultCommonPresets;
const advancedPresets =
customizationService.get('advancedPresets') || generateAdvancedPresets(hangingProtocolService);
customizationService.get('advancedPresets') || generateAdvancedPresets({ servicesManager });
const closeOnOutsideClick = () => {
if (isOpen) {
@@ -161,6 +189,7 @@ function LayoutSelector({
classNames="hover:bg-primary-dark group flex gap-2 p-1 cursor-pointer"
icon={preset.icon}
title={preset.title}
disabled={preset.disabled}
commandOptions={preset.commandOptions}
onSelection={onSelectionPreset}
/>
+4 -1
View File
@@ -271,7 +271,7 @@ const commandsModule = ({
/**
* Changes the viewport grid layout in terms of the MxN layout.
*/
setViewportGridLayout: ({ numRows, numCols }) => {
setViewportGridLayout: ({ numRows, numCols, isHangingProtocolLayout = false }) => {
const { protocol } = hangingProtocolService.getActiveProtocol();
const onLayoutChange = protocol.callbacks?.onLayoutChange;
if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) {
@@ -293,6 +293,7 @@ const commandsModule = ({
numRows,
numCols,
findOrCreateViewport,
isHangingProtocolLayout,
});
stateSyncService.store(stateReduce);
};
@@ -366,6 +367,7 @@ const commandsModule = ({
activeViewportId: viewportIdToUpdate,
layoutOptions,
findOrCreateViewport,
isHangingProtocolLayout: true,
});
} else {
// We are not in one-up, so toggle to one up.
@@ -390,6 +392,7 @@ const commandsModule = ({
numRows: 1,
numCols: 1,
findOrCreateViewport,
isHangingProtocolLayout: true,
});
// Subscribe to ANY (i.e. manual and hanging protocol) layout changes so that
+2 -2
View File
@@ -32,8 +32,8 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@cornerstonejs/core": "^1.70.5",
"@cornerstonejs/tools": "^1.70.5",
"@cornerstonejs/core": "^1.70.6",
"@cornerstonejs/tools": "^1.70.6",
"@ohif/core": "3.8.0-beta.80",
"@ohif/extension-cornerstone-dicom-sr": "3.8.0-beta.80",
"@ohif/ui": "3.8.0-beta.80",
@@ -5,14 +5,8 @@
function getImageSrcFromImageId(cornerstone, imageId) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
// Note: the default width and height of the canvas is 300x150
// but we need to set the width and height to the same number since
// the thumbnails are usually square and we want to maintain the aspect ratio
canvas.width = 128 / window.devicePixelRatio;
canvas.height = 128 / window.devicePixelRatio;
cornerstone.utilities
.loadImageToCanvas({ canvas, imageId })
.loadImageToCanvas({ canvas, imageId, thumbnail: true })
.then(imageId => {
resolve(canvas.toDataURL());
})
@@ -219,7 +219,10 @@ function TrackedCornerstoneViewport(props) {
return (
<Component
{...props}
onElementEnabled={onElementEnabled}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
onElementDisabled={onElementDisabled}
/>
);
@@ -47,8 +47,8 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
if (Number.isNaN(suvPeak)) {
uiNotificationService.show({
title: 'Unable to calculate SUV Peak',
message: 'The resulting threshold is not big enough.',
title: 'SUV Peak',
message: 'Segmented volume does not allow SUV Peak calculation',
type: 'warning',
});
}