fix(StackSync): Miscellaneous fixes for stack image sync (#3663)

This commit is contained in:
Bill Wallace 2023-10-03 10:59:09 -04:00 committed by GitHub
parent b3429729f1
commit 8a335bd03d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 432 additions and 293 deletions

View File

@ -32,6 +32,26 @@ const COMMIT_HASH = fs.readFileSync(path.join(__dirname, '../commit.txt'), 'utf8
// //
dotenv.config(); dotenv.config();
const defineValues = {
/* Application */
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
'process.env.VERSION_NUMBER': JSON.stringify(VERSION_NUMBER),
'process.env.COMMIT_HASH': JSON.stringify(COMMIT_HASH),
/* i18n */
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(process.env.REACT_APP_I18N_DEBUG || ''),
};
// Only redefine updated values. This avoids warning messages in the logs
if (!process.env.APP_CONFIG) {
defineValues['process.env.APP_CONFIG'] = '';
}
module.exports = (env, argv, { SRC_DIR, ENTRY }) => { module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
if (!process.env.NODE_ENV) { if (!process.env.NODE_ENV) {
throw new Error('process.env.NODE_ENV not set'); throw new Error('process.env.NODE_ENV not set');
@ -133,21 +153,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
}, },
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin(defineValues),
/* Application */
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
'process.env.VERSION_NUMBER': JSON.stringify(VERSION_NUMBER),
'process.env.COMMIT_HASH': JSON.stringify(COMMIT_HASH),
/* i18n */
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(process.env.REACT_APP_I18N_DEBUG || ''),
}),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'], Buffer: ['buffer', 'Buffer'],
}), }),

View File

@ -265,6 +265,11 @@ function commandsModule({
toolbarServiceRecordInteraction: props => { toolbarServiceRecordInteraction: props => {
toolbarService.recordInteraction(props); toolbarService.recordInteraction(props);
}, },
// Enable or disable a toggleable command, without calling the activation
// Used to setup already active tools from hanging protocols
setToolbarToggled: props => {
toolbarService.setToggled(props.toolId, props.isActive ?? true);
},
setToolActive: ({ toolName, toolGroupId = null, toggledState }) => { setToolActive: ({ toolName, toolGroupId = null, toggledState }) => {
if (toolName === 'Crosshairs') { if (toolName === 'Crosshairs') {
const activeViewportToolGroup = toolGroupService.getToolGroup(null); const activeViewportToolGroup = toolGroupService.getToolGroup(null);
@ -549,16 +554,16 @@ function commandsModule({
toggleStackImageSync: ({ toggledState }) => { toggleStackImageSync: ({ toggledState }) => {
toggleStackImageSync({ toggleStackImageSync({
getEnabledElement,
servicesManager, servicesManager,
toggledState, toggledState,
}); });
}, },
setSourceViewportForReferenceLinesTool: ({ toggledState }) => { setSourceViewportForReferenceLinesTool: ({ toggledState, viewportId }) => {
const { activeViewportId } = viewportGridService.getState(); if (!viewportId) {
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId); const { activeViewportId } = viewportGridService.getState();
viewportId = activeViewportId;
}
const viewportId = viewportInfo.getViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
toolGroup.setToolConfiguration( toolGroup.setToolConfiguration(
@ -699,8 +704,9 @@ function commandsModule({
}, },
storePresentation: { storePresentation: {
commandFn: actions.storePresentation, commandFn: actions.storePresentation,
storeContexts: [], },
options: {}, setToolbarToggled: {
commandFn: actions.setToolbarToggled,
}, },
}; };

View File

@ -14,7 +14,7 @@ import {
utilities as csUtilities, utilities as csUtilities,
Enums as csEnums, Enums as csEnums,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import { Enums, utilities, ReferenceLinesTool } from '@cornerstonejs/tools'; import { Enums } from '@cornerstonejs/tools';
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader'; import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
import initWADOImageLoader from './initWADOImageLoader'; import initWADOImageLoader from './initWADOImageLoader';
@ -93,6 +93,7 @@ export default async function init({
cornerstoneViewportService, cornerstoneViewportService,
hangingProtocolService, hangingProtocolService,
toolGroupService, toolGroupService,
toolbarService,
viewportGridService, viewportGridService,
stateSyncService, stateSyncService,
} = servicesManager.services as CornerstoneServices; } = servicesManager.services as CornerstoneServices;
@ -208,9 +209,45 @@ export default async function init({
commandsManager, commandsManager,
}); });
const newStackCallback = evt => { /**
* When a viewport gets a new display set, this call will go through all the
* active tools in the toolbar, and call any commands registered in the
* toolbar service with a callback to re-enable on displaying the viewport.
*/
const toolbarEventListener = evt => {
const { element } = evt.detail; const { element } = evt.detail;
utilities.stackPrefetch.enable(element); const activeTools = toolbarService.getActiveTools();
activeTools.forEach(tool => {
const toolData = toolbarService.getNestedButton(tool);
const commands = toolData?.listeners?.[evt.type];
commandsManager.run(commands, { element, evt });
});
};
/** Listens for active viewport events and fires the toolbar listeners */
const activeViewportEventListener = evt => {
const { viewportId } = evt;
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
const activeTools = toolbarService.getActiveTools();
activeTools.forEach(tool => {
if (!toolGroup?._toolInstances?.[tool]) {
return;
}
// check if tool is active on the new viewport
const toolEnabled = toolGroup._toolInstances[tool].mode === Enums.ToolModes.Enabled;
if (!toolEnabled) {
return;
}
const button = toolbarService.getNestedButton(tool);
const commands = button?.listeners?.[evt.type];
commandsManager.run(commands, { viewportId, evt });
});
}; };
const resetCrosshairs = evt => { const resetCrosshairs = evt => {
@ -237,11 +274,16 @@ export default async function init({
} }
}; };
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
const { element } = evt.detail;
cornerstoneTools.utilities.stackContextPrefetch.enable(element);
});
function elementEnabledHandler(evt) { function elementEnabledHandler(evt) {
const { element } = evt.detail; const { element } = evt.detail;
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs); element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, newStackCallback); eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
} }
function elementDisabledHandler(evt) { function elementDisabledHandler(evt) {
@ -262,33 +304,7 @@ export default async function init({
viewportGridService.subscribe( viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
({ viewportId }) => { activeViewportEventListener
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) {
return;
}
// check if reference lines are active
const referenceLinesEnabled =
toolGroup._toolInstances['ReferenceLines'].mode === Enums.ToolModes.Enabled;
if (!referenceLinesEnabled) {
return;
}
toolGroup.setToolConfiguration(
ReferenceLinesTool.toolName,
{
sourceViewportId: viewportId,
},
true // overwrite
);
// make sure to set it to enabled again since we want to recalculate
// the source-target lines
toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
}
); );
} }

View File

@ -142,12 +142,16 @@ const CornerstoneViewportDownloadForm = ({
const properties = viewport.getProperties(); const properties = viewport.getProperties();
downloadViewport.setStack([imageId]).then(() => { downloadViewport.setStack([imageId]).then(() => {
downloadViewport.setProperties(properties); try {
downloadViewport.setProperties(properties);
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE); resolve({ width: newWidth, height: newHeight });
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE); } catch (e) {
// Happens on clicking the cancel button
resolve({ width: newWidth, height: newHeight }); console.warn('Unable to set properties', e);
}
}); });
} else if (downloadViewport instanceof VolumeViewport) { } else if (downloadViewport instanceof VolumeViewport) {
const actors = viewport.getActors(); const actors = viewport.getActors();

View File

@ -1,90 +1,80 @@
import calculateViewportRegistrations from './calculateViewportRegistrations'; const STACK_SYNC_NAME = 'stackImageSync';
// [ { export default function toggleStackImageSync({
// synchronizerId: string, toggledState,
// viewports: [ { viewportId: string, renderingEngineId: string, index: number } , ...] servicesManager,
// ]} viewports: providedViewports,
let STACK_IMAGE_SYNC_GROUPS_INFO = []; }) {
if (!toggledState) {
return disableSync(STACK_SYNC_NAME, servicesManager);
}
export default function toggleStackImageSync({ toggledState, servicesManager, getEnabledElement }) {
const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } = const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } =
servicesManager.services; servicesManager.services;
if (!toggledState) { const viewports = providedViewports || getReconstructableStackViewports(viewportGridService, displaySetService);
STACK_IMAGE_SYNC_GROUPS_INFO.forEach(syncGroupInfo => {
const { viewports, synchronizerId } = syncGroupInfo;
viewports.forEach(({ viewportId, renderingEngineId }) => { // create synchronization group and add the viewports to it.
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, synchronizerId); viewports.forEach(gridViewport => {
}); const { viewportId } = gridViewport.viewportOptions;
}); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return; return;
}
STACK_IMAGE_SYNC_GROUPS_INFO = [];
// create synchronization groups and add viewports
const { viewports } = viewportGridService.getState();
// filter empty viewports
const viewportsArray = Array.from(viewports.values())
.filter(viewport => viewport.displaySetInstanceUIDs?.length)
// filter reconstructable viewports
.filter(viewport => {
const { displaySetInstanceUIDs } = viewport;
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
return !!displaySet?.isReconstructable;
}
});
const viewportsByOrientation = viewportsArray.reduce((acc, viewport) => {
const { viewportId, viewportType } = viewport.viewportOptions;
if (viewportType !== 'stack') {
console.warn('Viewport is not a stack, cannot sync images yet');
return acc;
} }
syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, {
const { element } = cornerstoneViewportService.getViewportInfo(viewportId); type: 'stackimage',
const { viewport: csViewport, renderingEngineId } = getEnabledElement(element); id: STACK_SYNC_NAME,
const { viewPlaneNormal } = csViewport.getCamera(); source: true,
target: true,
// 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,
}); });
}); });
} }
function disableSync(syncName, servicesManager) {
const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } =
servicesManager.services;
const viewports = getReconstructableStackViewports(viewportGridService, displaySetService);
viewports.forEach(gridViewport => {
const { viewportId } = gridViewport.viewportOptions;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
syncGroupService.removeViewportFromSyncGroup(
viewport.id,
viewport.getRenderingEngine().id,
syncName
);
});
};
/**
* Gets the consistent spacing stack viewport types, which are the ones which
* can be navigated using the stack image sync right now.
*/
function getReconstructableStackViewports(viewportGridService, displaySetService) {
let { viewports } = viewportGridService.getState();
viewports = [...viewports.values()];
// 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);
// TODO - add a better test than isReconstructable
if (displaySet && displaySet.isReconstructable) {
return true;
}
return false;
}
});
return viewports;
};

View File

@ -134,7 +134,7 @@ const commandsModule = ({
(!protocolId || protocolId === protocol.id) && (!protocolId || protocolId === protocol.id) &&
(stageIndex === undefined || stageIndex === toggleStageIndex) && (stageIndex === undefined || stageIndex === toggleStageIndex) &&
(!stageId || stageId === stage.id); (!stageId || stageId === stage.id);
toolbarService.setActive(button.id, isActive); toolbarService.setToggled(button.id, isActive);
}; };
Object.values(toolbarService.getButtons()).forEach(enableListener); Object.values(toolbarService.getButtons()).forEach(enableListener);
}, },

View File

@ -19,7 +19,7 @@ export default function getDisplaySetMessages(
const firstInstance = instances[0]; const firstInstance = instances[0];
// Due to current requirements, LOCALIZER series doesn't have any messages // Due to current requirements, LOCALIZER series doesn't have any messages
if (firstInstance.ImageType.includes('LOCALIZER')) { if (firstInstance?.ImageType?.includes('LOCALIZER')) {
return messages; return messages;
} }

View File

@ -6,7 +6,7 @@
"license": "MIT", "license": "MIT",
"repository": "OHIF/Viewers", "repository": "OHIF/Viewers",
"main": "dist/ohif-mode-test.umd.js", "main": "dist/ohif-mode-test.umd.js",
"module": "src/index.js", "module": "src/index.ts",
"engines": { "engines": {
"node": ">=14", "node": ">=14",
"npm": ">=6", "npm": ">=6",

View File

@ -1,7 +1,7 @@
import { hotkeys } from '@ohif/core'; import { hotkeys } from '@ohif/core';
import toolbarButtons from './toolbarButtons.js'; import toolbarButtons from './toolbarButtons';
import { id } from './id.js'; import { id } from './id';
import initToolGroups from './initToolGroups.js'; import initToolGroups from './initToolGroups';
// Allow this mode by excluding non-imaging modalities such as SR, SEG // Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it. // Also, SM is not a simple imaging modalities, so exclude it.

View File

@ -5,43 +5,15 @@ import {
// ListMenu, // ListMenu,
WindowLevelMenuItem, WindowLevelMenuItem,
} from '@ohif/ui'; } from '@ohif/ui';
import { defaults } from '@ohif/core'; import { defaults, ToolbarService } from '@ohif/core';
import type { Button, RunCommand } from '@ohif/core/types';
import { EVENTS } from '@cornerstonejs/core';
const { windowLevelPresets } = defaults; const { windowLevelPresets } = defaults;
/**
*
* @param {*} type - 'tool' | 'action' | 'toggle'
* @param {*} id
* @param {*} icon
* @param {*} label
*/
function _createButton(type, id, icon, label, commands, tooltip, uiType) {
return {
id,
icon,
label,
type,
commands,
tooltip,
uiType,
};
}
function _createCommands(commandName, toolName, toolGroupIds) { const _createActionButton = ToolbarService._createButton.bind(null, 'action');
return toolGroupIds.map(toolGroupId => ({ const _createToggleButton = ToolbarService._createButton.bind(null, 'toggle');
/* It's a command that is being run when the button is clicked. */ const _createToolButton = ToolbarService._createButton.bind(null, 'tool');
commandName,
commandOptions: {
toolName,
toolGroupId,
},
context: 'CORNERSTONE',
}));
}
const _createActionButton = _createButton.bind(null, 'action');
const _createToggleButton = _createButton.bind(null, 'toggle');
const _createToolButton = _createButton.bind(null, 'tool');
/** /**
* *
@ -67,7 +39,21 @@ function _createWwwcPreset(preset, title, subtitle) {
}; };
} }
const toolbarButtons = [ const ReferenceLinesCommands: RunCommand = [
{
commandName: 'setSourceViewportForReferenceLinesTool',
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ReferenceLines',
},
context: 'CORNERSTONE',
},
];
const toolbarButtons: Button[] = [
// Measurement // Measurement
{ {
id: 'MeasurementTools', id: 'MeasurementTools',
@ -515,24 +501,37 @@ const toolbarButtons = [
], ],
'Flip Horizontal' 'Flip Horizontal'
), ),
_createToggleButton('StackImageSync', 'link', 'Stack Image Sync', [ _createToggleButton(
'StackImageSync',
'link',
'Stack Image Sync',
[
{
commandName: 'toggleStackImageSync',
},
],
'Enable position synchronization on stack viewports',
{ {
commandName: 'toggleStackImageSync', listeners: {
commandOptions: {}, [EVENTS.STACK_VIEWPORT_NEW_STACK]: {
context: 'CORNERSTONE', commandName: 'toggleStackImageSync',
}, commandOptions: { toggledState: true },
]), },
},
}
),
_createToggleButton( _createToggleButton(
'ReferenceLines', 'ReferenceLines',
'tool-referenceLines', // change this with the new icon 'tool-referenceLines', // change this with the new icon
'Reference Lines', 'Reference Lines',
[ ReferenceLinesCommands,
{ 'Show Reference Lines',
commandName: 'toggleReferenceLines', {
commandOptions: {}, listeners: {
context: 'CORNERSTONE', [EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
}, },
] }
), ),
_createToolButton( _createToolButton(
'StackScroll', 'StackScroll',

View File

@ -1,7 +1,7 @@
import { hotkeys } from '@ohif/core'; import { hotkeys } from '@ohif/core';
import toolbarButtons from './toolbarButtons.js'; import toolbarButtons from './toolbarButtons';
import { id } from './id.js'; import { id } from './id';
import initToolGroups from './initToolGroups.js'; import initToolGroups from './initToolGroups';
// Allow this mode by excluding non-imaging modalities such as SR, SEG // Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it. // Also, SM is not a simple imaging modalities, so exclude it.

View File

@ -5,32 +5,15 @@ import {
// ListMenu, // ListMenu,
WindowLevelMenuItem, WindowLevelMenuItem,
} from '@ohif/ui'; } from '@ohif/ui';
import { defaults } from '@ohif/core'; import { defaults, ToolbarService } from '@ohif/core';
import type { Button, RunCommand } from '@ohif/core/types';
import { EVENTS } from '@cornerstonejs/core';
const { windowLevelPresets } = defaults; const { windowLevelPresets } = defaults;
/**
*
* @param {*} type - 'tool' | 'action' | 'toggle'
* @param {*} id
* @param {*} icon
* @param {*} label
*/
function _createButton(type, id, icon, label, commands, tooltip, uiType, isActive) {
return {
id,
icon,
label,
type,
commands,
tooltip,
uiType,
isActive,
};
}
const _createActionButton = _createButton.bind(null, 'action'); const _createActionButton = ToolbarService._createButton.bind(null, 'action');
const _createToggleButton = _createButton.bind(null, 'toggle'); const _createToggleButton = ToolbarService._createButton.bind(null, 'toggle');
const _createToolButton = _createButton.bind(null, 'tool'); const _createToolButton = ToolbarService._createButton.bind(null, 'tool');
/** /**
* *
@ -76,7 +59,21 @@ function _createSetToolActiveCommands(toolName) {
return temp; return temp;
} }
const toolbarButtons = [ const ReferenceLinesCommands: RunCommand = [
{
commandName: 'setSourceViewportForReferenceLinesTool',
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ReferenceLines',
},
context: 'CORNERSTONE',
},
];
const toolbarButtons: Button[] = [
// Measurement // Measurement
{ {
id: 'MeasurementTools', id: 'MeasurementTools',
@ -422,34 +419,37 @@ const toolbarButtons = [
], ],
'Flip Horizontal' 'Flip Horizontal'
), ),
_createToggleButton('StackImageSync', 'link', 'Stack Image Sync', [ _createToggleButton(
'StackImageSync',
'link',
'Stack Image Sync',
[
{
commandName: 'toggleStackImageSync',
},
],
'Enable position synchronization on stack viewports',
{ {
commandName: 'toggleStackImageSync', listeners: {
commandOptions: {}, [EVENTS.STACK_VIEWPORT_NEW_STACK]: {
context: 'CORNERSTONE', commandName: 'toggleStackImageSync',
}, commandOptions: { toggledState: true },
]), },
},
}
),
_createToggleButton( _createToggleButton(
'ReferenceLines', 'ReferenceLines',
'tool-referenceLines', // change this with the new icon 'tool-referenceLines', // change this with the new icon
'Reference Lines', 'Reference Lines',
// two commands for the reference lines tool: ReferenceLinesCommands,
// - the first to set the source viewport for the tool when it is enabled 'Show Reference Lines',
// - the second to toggle the tool {
[ listeners: {
{ [EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
commandName: 'setSourceViewportForReferenceLinesTool', [EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
commandOptions: {},
context: 'CORNERSTONE',
}, },
{ }
commandName: 'setToolActive',
commandOptions: {
toolName: 'ReferenceLines',
},
context: 'CORNERSTONE',
},
]
), ),
_createToggleButton( _createToggleButton(
'ImageOverlayViewer', 'ImageOverlayViewer',
@ -465,8 +465,7 @@ const toolbarButtons = [
}, },
], ],
'Image Overlay', 'Image Overlay',
null, { isActive: true }
true
), ),
_createToolButton( _createToolButton(
'StackScroll', 'StackScroll',

View File

@ -131,6 +131,8 @@ module.exports = (env, argv) => {
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
// Need to exclude the theme as it is updated independently // Need to exclude the theme as it is updated independently
exclude: [/theme/], exclude: [/theme/],
// Cache large files for the manifests to avoid warning messages
maximumFileSizeToCacheInBytes: 1024 * 1024 * 50,
}), }),
], ],
// https://webpack.js.org/configuration/dev-server/ // https://webpack.js.org/configuration/dev-server/

View File

@ -74,7 +74,7 @@ function getRuntimeLoadModesExtensions(modules) {
); );
}); });
dynamicLoad.push( dynamicLoad.push(
' return (await import(module)).default;', ' return (await import(/* webpackIgnore: true */ module)).default;',
'}\n', '}\n',
'// Import a list of items (modules or string names)', '// Import a list of items (modules or string names)',
'// @return a Promise evaluating to a list of modules', '// @return a Promise evaluating to a list of modules',

View File

@ -409,20 +409,42 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('@moreBtn').click(); cy.get('@moreBtn').click();
cy.get('.tooltip-toolbar-overlay').should('not.exist'); cy.get('.tooltip-toolbar-overlay').should('not.exist');
}); });
*/
it('check if Flip V tool will flip the image vertically in the viewport', () => { it('check if Flip tool will flip the image in the viewport', () => {
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip V button
cy.get('[data-cy="flip v"]').click();
cy.get('@viewportInfoMidLeft').should('contains.text', 'R'); cy.get('@viewportInfoMidLeft').should('contains.text', 'R');
cy.get('@viewportInfoMidTop').should('contains.text', 'F'); cy.get('@viewportInfoMidTop').should('contains.text', 'A');
//Click on More button to close it //Click on More button
cy.get('@moreBtn').click(); cy.get('@moreBtnSecondary').click();
cy.get('.tooltip-toolbar-overlay').should('not.exist');
});*/ //Click on Flip button
cy.get('[data-cy="flip-horizontal"]').click();
cy.waitDicomImage();
cy.get('@viewportInfoMidLeft').should('contains.text', 'L');
cy.get('@viewportInfoMidTop').should('contains.text', 'A');
});
it('checks if stack sync is preserved on new display set and uses FOR', () => {
// Active stack image sync and reference lines
cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
cy.get('[data-cy="StackImageSync"]').click();
// Add reference lines as that sometimes throws an exception
cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
cy.get('[data-cy="ReferenceLines"]').click();
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
cy.get('body').type('{downarrow}{downarrow}');
// Change the layout and double load the first
cy.setLayout(2, 1);
cy.get('body').type('{rightarrow}');
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
cy.waitDicomImage();
// Now navigate down once and check that the left hand pane navigated
cy.get('body').type('{downarrow}');
cy.get('body').type('{leftarrow}');
cy.setLayout(1, 1);
cy.get('@viewportInfoTopRight').should('contains.text', 'I:2 (2/20)');
});
}); });

View File

@ -36,6 +36,8 @@ describe('OHIF Download Snapshot File', () => {
// Check buttons // Check buttons
cy.get('[data-cy="cancel-btn"]').scrollIntoView().should('be.visible'); cy.get('[data-cy="cancel-btn"]').scrollIntoView().should('be.visible');
cy.get('[data-cy="download-btn"]').scrollIntoView().should('be.visible'); cy.get('[data-cy="download-btn"]').scrollIntoView().should('be.visible');
cy.get('[data-cy="cancel-btn"]').click();
}); });
/*it('cancel changes on download modal', function() { /*it('cancel changes on download modal', function() {

View File

@ -1,11 +1,9 @@
describe('OHIF Study Viewer Page', function () { describe('OHIF General Viewer', function () {
beforeEach(function () { beforeEach(() =>
cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78'); cy.initViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78', {
minimumThumbnails: 3,
cy.expectMinimumThumbnails(3); })
cy.initCommonElementsAliases(); );
cy.initCornerstoneToolsAliases();
});
it('scrolls series stack using scrollbar', function () { it('scrolls series stack using scrollbar', function () {
cy.scrollToIndex(13); cy.scrollToIndex(13);

View File

@ -1,4 +1,4 @@
describe('OHIF Study Viewer Page', function () { describe('OHIF Study Browser', function () {
beforeEach(function () { beforeEach(function () {
cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78'); cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78');

View File

@ -69,6 +69,18 @@ Cypress.Commands.add(
} }
); );
Cypress.Commands.add('initViewer', (StudyInstanceUID, other = {}) => {
const { mode = '/basic-test', minimumThumbnails = 1, params = '' } = other;
cy.openStudyInViewer(StudyInstanceUID, params, mode);
cy.waitDicomImage();
// Very short wait to ensure pending updates are handled
cy.wait(25);
cy.expectMinimumThumbnails(minimumThumbnails);
cy.initCommonElementsAliases();
cy.initCornerstoneToolsAliases();
});
Cypress.Commands.add( Cypress.Commands.add(
'openStudyInViewer', 'openStudyInViewer',
(StudyInstanceUID, otherParams = '', mode = '/basic-test') => { (StudyInstanceUID, otherParams = '', mode = '/basic-test') => {
@ -347,14 +359,9 @@ Cypress.Commands.add('percyCanvasSnapshot', (name, options = {}) => {
}); });
Cypress.Commands.add('setLayout', (columns = 1, rows = 1) => { Cypress.Commands.add('setLayout', (columns = 1, rows = 1) => {
cy.get('[data-cy="layout"]').click(); cy.get('[data-cy="Layout"]').click();
cy.get('.layoutChooser') cy.get(`[data-cy="Layout-${columns - 1}-${rows - 1}"]`).click();
.find('tr')
.eq(rows - 1)
.find('td')
.eq(columns - 1)
.click();
cy.wait(10); cy.wait(10);
cy.waitDicomImage(); cy.waitDicomImage();

View File

@ -38,8 +38,28 @@ window.config = {
configuration: { configuration: {
friendlyName: 'Static WADO Local Data', friendlyName: 'Static WADO Local Data',
name: 'DCM4CHEE', name: 'DCM4CHEE',
qidoRoot: '/dicomweb', qidoRoot: 'http://localhost:5000/dicomweb',
wadoRoot: '/dicomweb', wadoRoot: 'http://localhost:5000/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: true,
supportsStow: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: false,
supportsWildcard: true,
staticWado: true,
singlepart: 'bulkdata,video,pdf',
},
},
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'docker',
configuration: {
friendlyName: 'Static WADO Docker Data',
name: 'DCM4CHEE',
qidoRoot: 'http://localhost:25080/dicomweb',
wadoRoot: 'http://localhost:25080/dicomweb',
qidoSupportsIncludeField: false, qidoSupportsIncludeField: false,
supportsReject: true, supportsReject: true,
supportsStow: true, supportsStow: true,

View File

@ -443,7 +443,7 @@ export default function ModeRoute({
<ImageViewerProvider <ImageViewerProvider
// initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }} // initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }}
StudyInstanceUIDs={studyInstanceUIDs} StudyInstanceUIDs={studyInstanceUIDs}
// reducer={reducer} // reducer={reducer}
> >
<CombinedContextProvider> <CombinedContextProvider>
<DragAndDropProvider> <DragAndDropProvider>

View File

@ -2,12 +2,37 @@ import merge from 'lodash.merge';
import { CommandsManager } from '../../classes'; import { CommandsManager } from '../../classes';
import { ExtensionManager } from '../../extensions'; import { ExtensionManager } from '../../extensions';
import { PubSubService } from '../_shared/pubSubServiceInterface'; import { PubSubService } from '../_shared/pubSubServiceInterface';
import type { RunCommand, Commands } from '../../types/Command';
const EVENTS = { const EVENTS = {
TOOL_BAR_MODIFIED: 'event::toolBarService:toolBarModified', TOOL_BAR_MODIFIED: 'event::toolBarService:toolBarModified',
TOOL_BAR_STATE_MODIFIED: 'event::toolBarService:toolBarStateModified', TOOL_BAR_STATE_MODIFIED: 'event::toolBarService:toolBarStateModified',
}; };
export type ButtonListeners = Record<string, RunCommand>;
export interface ButtonProps {
primary?: Button;
secondary?: Button;
items?: Button[];
}
export interface Button extends Commands {
id: string;
icon?: string;
label?: string;
type?: string;
tooltip?: string;
isActive?: boolean;
listeners?: ButtonListeners;
props?: ButtonProps;
}
export interface ExtraButtonOptions {
listeners?: ButtonListeners;
isActive?: boolean;
}
export default class ToolbarService extends PubSubService { export default class ToolbarService extends PubSubService {
public static REGISTRATION = { public static REGISTRATION = {
name: 'toolbarService', name: 'toolbarService',
@ -18,7 +43,27 @@ export default class ToolbarService extends PubSubService {
}, },
}; };
buttons: Record<string, unknown> = {}; public static _createButton(
type: string,
id: string,
icon: string,
label: string,
commands: Command | Commands,
tooltip?: string,
extraOptions?: ExtraButtonOptions
): Button {
return {
id,
icon,
label,
type,
commands,
tooltip,
...extraOptions,
};
}
buttons: Record<string, Button> = {};
state: { state: {
primaryToolId: string; primaryToolId: string;
toggles: Record<string, boolean>; toggles: Record<string, boolean>;
@ -54,7 +99,7 @@ export default class ToolbarService extends PubSubService {
this.buttons = {}; this.buttons = {};
} }
onModeEnter() { public onModeEnter(): void {
this.reset(); this.reset();
} }
@ -65,7 +110,7 @@ export default class ToolbarService extends PubSubService {
* used for calling the specified interaction. That is, the command is * used for calling the specified interaction. That is, the command is
* called with {...commandOptions,...options} * called with {...commandOptions,...options}
*/ */
recordInteraction(interaction, options?: Record<string, unknown>) { public recordInteraction(interaction, options?: Record<string, unknown>) {
if (!interaction) { if (!interaction) {
return; return;
} }
@ -174,12 +219,18 @@ export default class ToolbarService extends PubSubService {
} }
getActiveTools() { getActiveTools() {
return [this.state.primaryToolId, ...Object.keys(this.state.toggles)]; const activeTools = [this.state.primaryToolId];
Object.keys(this.state.toggles).forEach(key => {
if (this.state.toggles[key]) {
activeTools.push(key);
}
});
return activeTools;
} }
/** Sets the toggle state of a button to the isActive state */ /** Sets the toggle state of a button to the isToggled state */
public setActive(id: string, isActive: boolean): void { public setToggled(id: string, isToggled: boolean): void {
if (isActive) { if (isToggled) {
this.state.toggles[id] = true; this.state.toggles[id] = true;
} else { } else {
delete this.state.toggles[id]; delete this.state.toggles[id];
@ -197,10 +248,25 @@ export default class ToolbarService extends PubSubService {
} }
} }
getButton(id) { public getButton(id: string): Button {
return this.buttons[id]; return this.buttons[id];
} }
/** Gets a nested button, found in the items/props for the children */
public getNestedButton(id: string): Button {
if (this.buttons[id]) {
return this.buttons[id];
}
for (const buttonId of Object.keys(this.buttons)) {
const { primary, items } = this.buttons[buttonId].props || {};
if (primary?.id === id) { return primary; }
const found = items?.find(childButton => childButton.id === id);
if (found) {
return found;
}
}
}
setButtons(buttons) { setButtons(buttons) {
this.buttons = buttons; this.buttons = buttons;
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
@ -267,23 +333,22 @@ export default class ToolbarService extends PubSubService {
if (!this.buttons[button.id]) { if (!this.buttons[button.id]) {
this.buttons[button.id] = button; this.buttons[button.id] = button;
} }
this._setTogglesForButtonItems(button.props?.items);
}); });
this._setTogglesForButtonItems(buttons);
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {}); this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {});
} }
_setTogglesForButtonItems(buttonItems) { _setTogglesForButtonItems(buttons) {
if (!buttonItems) { if (!buttons) {
return; return;
} }
buttonItems.forEach(buttonItem => { buttons.forEach(buttonItem => {
if (buttonItem.type === 'toggle') { if (buttonItem.type === 'toggle') {
this.state.toggles[buttonItem.id] = buttonItem.isActive; this.setToggled(buttonItem.id, buttonItem.isActive);
} else {
this._setTogglesForButtonItems(buttonItem.props?.items);
} }
this._setTogglesForButtonItems(buttonItem.props?.items);
}); });
} }

View File

@ -4,7 +4,9 @@ export interface Command {
context?: string; context?: string;
} }
export type RunCommand = Command | Command[];
/** A set of commands, typically contained in a tool item or other configuration */ /** A set of commands, typically contained in a tool item or other configuration */
export interface Commands { export interface Commands {
commands: Command[]; commands: RunCommand;
} }

View File

@ -34,6 +34,7 @@ function LayoutSelector({ onSelection, rows, columns }) {
border: '1px solid white', border: '1px solid white',
backgroundColor: isHovered(index) ? '#5acce6' : '#0b1a42', backgroundColor: isHovered(index) ? '#5acce6' : '#0b1a42',
}} }}
data-cy={`Layout-${index % columns}-${Math.floor(index / columns)}`}
className="cursor-pointer" className="cursor-pointer"
onClick={() => { onClick={() => {
const x = index % columns; const x = index % columns;

@ -1 +1 @@
Subproject commit da63adc206455ce860b87c6fe723d71fb415a891 Subproject commit 4d59660c2883ed749a680e5fb6d4624ab54c9422