fix(StackSync): Miscellaneous fixes for stack image sync (#3663)
This commit is contained in:
parent
b3429729f1
commit
8a335bd03d
@ -32,6 +32,26 @@ const COMMIT_HASH = fs.readFileSync(path.join(__dirname, '../commit.txt'), 'utf8
|
||||
//
|
||||
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 }) => {
|
||||
if (!process.env.NODE_ENV) {
|
||||
throw new Error('process.env.NODE_ENV not set');
|
||||
@ -133,21 +153,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
/* 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.DefinePlugin(defineValues),
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
|
||||
@ -265,6 +265,11 @@ function commandsModule({
|
||||
toolbarServiceRecordInteraction: 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 }) => {
|
||||
if (toolName === 'Crosshairs') {
|
||||
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
|
||||
@ -549,16 +554,16 @@ function commandsModule({
|
||||
|
||||
toggleStackImageSync: ({ toggledState }) => {
|
||||
toggleStackImageSync({
|
||||
getEnabledElement,
|
||||
servicesManager,
|
||||
toggledState,
|
||||
});
|
||||
},
|
||||
setSourceViewportForReferenceLinesTool: ({ toggledState }) => {
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||
setSourceViewportForReferenceLinesTool: ({ toggledState, viewportId }) => {
|
||||
if (!viewportId) {
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
viewportId = activeViewportId;
|
||||
}
|
||||
|
||||
const viewportId = viewportInfo.getViewportId();
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
toolGroup.setToolConfiguration(
|
||||
@ -699,8 +704,9 @@ function commandsModule({
|
||||
},
|
||||
storePresentation: {
|
||||
commandFn: actions.storePresentation,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setToolbarToggled: {
|
||||
commandFn: actions.setToolbarToggled,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
utilities as csUtilities,
|
||||
Enums as csEnums,
|
||||
} from '@cornerstonejs/core';
|
||||
import { Enums, utilities, ReferenceLinesTool } from '@cornerstonejs/tools';
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
|
||||
import initWADOImageLoader from './initWADOImageLoader';
|
||||
@ -93,6 +93,7 @@ export default async function init({
|
||||
cornerstoneViewportService,
|
||||
hangingProtocolService,
|
||||
toolGroupService,
|
||||
toolbarService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
@ -208,9 +209,45 @@ export default async function init({
|
||||
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;
|
||||
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 => {
|
||||
@ -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) {
|
||||
const { element } = evt.detail;
|
||||
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) {
|
||||
@ -262,33 +304,7 @@ export default async function init({
|
||||
|
||||
viewportGridService.subscribe(
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
({ viewportId }) => {
|
||||
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);
|
||||
}
|
||||
activeViewportEventListener
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -142,12 +142,16 @@ const CornerstoneViewportDownloadForm = ({
|
||||
const properties = viewport.getProperties();
|
||||
|
||||
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);
|
||||
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
||||
|
||||
resolve({ width: newWidth, height: newHeight });
|
||||
resolve({ width: newWidth, height: newHeight });
|
||||
} catch (e) {
|
||||
// Happens on clicking the cancel button
|
||||
console.warn('Unable to set properties', e);
|
||||
}
|
||||
});
|
||||
} else if (downloadViewport instanceof VolumeViewport) {
|
||||
const actors = viewport.getActors();
|
||||
|
||||
@ -1,90 +1,80 @@
|
||||
import calculateViewportRegistrations from './calculateViewportRegistrations';
|
||||
const STACK_SYNC_NAME = 'stackImageSync';
|
||||
|
||||
// [ {
|
||||
// synchronizerId: string,
|
||||
// viewports: [ { viewportId: string, renderingEngineId: string, index: number } , ...]
|
||||
// ]}
|
||||
let STACK_IMAGE_SYNC_GROUPS_INFO = [];
|
||||
export default function toggleStackImageSync({
|
||||
toggledState,
|
||||
servicesManager,
|
||||
viewports: providedViewports,
|
||||
}) {
|
||||
if (!toggledState) {
|
||||
return disableSync(STACK_SYNC_NAME, servicesManager);
|
||||
}
|
||||
|
||||
export default function toggleStackImageSync({ toggledState, servicesManager, getEnabledElement }) {
|
||||
const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } =
|
||||
servicesManager.services;
|
||||
|
||||
if (!toggledState) {
|
||||
STACK_IMAGE_SYNC_GROUPS_INFO.forEach(syncGroupInfo => {
|
||||
const { viewports, synchronizerId } = syncGroupInfo;
|
||||
const viewports = providedViewports || getReconstructableStackViewports(viewportGridService, displaySetService);
|
||||
|
||||
viewports.forEach(({ viewportId, renderingEngineId }) => {
|
||||
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, synchronizerId);
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
// create synchronization group and add the viewports to it.
|
||||
viewports.forEach(gridViewport => {
|
||||
const { viewportId } = gridViewport.viewportOptions;
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const { viewport: csViewport, renderingEngineId } = getEnabledElement(element);
|
||||
const { viewPlaneNormal } = csViewport.getCamera();
|
||||
|
||||
// Should we round here? I guess so, but not sure how much precision we need
|
||||
const orientation = viewPlaneNormal.map(v => Math.round(v)).join(',');
|
||||
|
||||
if (!acc[orientation]) {
|
||||
acc[orientation] = [];
|
||||
}
|
||||
|
||||
acc[orientation].push({ viewportId, renderingEngineId });
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// create synchronizer for each group
|
||||
Object.values(viewportsByOrientation).map(viewports => {
|
||||
let synchronizerId = viewports.map(({ viewportId }) => viewportId).join(',');
|
||||
|
||||
synchronizerId = `imageSync_${synchronizerId}`;
|
||||
|
||||
calculateViewportRegistrations(viewports);
|
||||
|
||||
viewports.forEach(({ viewportId, renderingEngineId }) => {
|
||||
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, {
|
||||
type: 'stackimage',
|
||||
id: synchronizerId,
|
||||
source: true,
|
||||
target: true,
|
||||
});
|
||||
});
|
||||
|
||||
STACK_IMAGE_SYNC_GROUPS_INFO.push({
|
||||
synchronizerId,
|
||||
viewports,
|
||||
syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, {
|
||||
type: 'stackimage',
|
||||
id: STACK_SYNC_NAME,
|
||||
source: true,
|
||||
target: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@ -134,7 +134,7 @@ const commandsModule = ({
|
||||
(!protocolId || protocolId === protocol.id) &&
|
||||
(stageIndex === undefined || stageIndex === toggleStageIndex) &&
|
||||
(!stageId || stageId === stage.id);
|
||||
toolbarService.setActive(button.id, isActive);
|
||||
toolbarService.setToggled(button.id, isActive);
|
||||
};
|
||||
Object.values(toolbarService.getButtons()).forEach(enableListener);
|
||||
},
|
||||
|
||||
@ -19,7 +19,7 @@ export default function getDisplaySetMessages(
|
||||
|
||||
const firstInstance = instances[0];
|
||||
// Due to current requirements, LOCALIZER series doesn't have any messages
|
||||
if (firstInstance.ImageType.includes('LOCALIZER')) {
|
||||
if (firstInstance?.ImageType?.includes('LOCALIZER')) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/ohif-mode-test.umd.js",
|
||||
"module": "src/index.js",
|
||||
"module": "src/index.ts",
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">=6",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import toolbarButtons from './toolbarButtons.js';
|
||||
import { id } from './id.js';
|
||||
import initToolGroups from './initToolGroups.js';
|
||||
import toolbarButtons from './toolbarButtons';
|
||||
import { id } from './id';
|
||||
import initToolGroups from './initToolGroups';
|
||||
|
||||
// Allow this mode by excluding non-imaging modalities such as SR, SEG
|
||||
// Also, SM is not a simple imaging modalities, so exclude it.
|
||||
@ -5,43 +5,15 @@ import {
|
||||
// ListMenu,
|
||||
WindowLevelMenuItem,
|
||||
} 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;
|
||||
/**
|
||||
*
|
||||
* @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) {
|
||||
return toolGroupIds.map(toolGroupId => ({
|
||||
/* It's a command that is being run when the button is clicked. */
|
||||
commandName,
|
||||
commandOptions: {
|
||||
toolName,
|
||||
toolGroupId,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
}));
|
||||
}
|
||||
|
||||
const _createActionButton = _createButton.bind(null, 'action');
|
||||
const _createToggleButton = _createButton.bind(null, 'toggle');
|
||||
const _createToolButton = _createButton.bind(null, 'tool');
|
||||
const _createActionButton = ToolbarService._createButton.bind(null, 'action');
|
||||
const _createToggleButton = ToolbarService._createButton.bind(null, 'toggle');
|
||||
const _createToolButton = ToolbarService._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
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
@ -515,24 +501,37 @@ const toolbarButtons = [
|
||||
],
|
||||
'Flip Horizontal'
|
||||
),
|
||||
_createToggleButton('StackImageSync', 'link', 'Stack Image Sync', [
|
||||
_createToggleButton(
|
||||
'StackImageSync',
|
||||
'link',
|
||||
'Stack Image Sync',
|
||||
[
|
||||
{
|
||||
commandName: 'toggleStackImageSync',
|
||||
},
|
||||
],
|
||||
'Enable position synchronization on stack viewports',
|
||||
{
|
||||
commandName: 'toggleStackImageSync',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
]),
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
|
||||
commandName: 'toggleStackImageSync',
|
||||
commandOptions: { toggledState: true },
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
_createToggleButton(
|
||||
'ReferenceLines',
|
||||
'tool-referenceLines', // change this with the new icon
|
||||
'Reference Lines',
|
||||
[
|
||||
{
|
||||
commandName: 'toggleReferenceLines',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
ReferenceLinesCommands,
|
||||
'Show Reference Lines',
|
||||
{
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
|
||||
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
|
||||
},
|
||||
]
|
||||
}
|
||||
),
|
||||
_createToolButton(
|
||||
'StackScroll',
|
||||
@ -1,7 +1,7 @@
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import toolbarButtons from './toolbarButtons.js';
|
||||
import { id } from './id.js';
|
||||
import initToolGroups from './initToolGroups.js';
|
||||
import toolbarButtons from './toolbarButtons';
|
||||
import { id } from './id';
|
||||
import initToolGroups from './initToolGroups';
|
||||
|
||||
// Allow this mode by excluding non-imaging modalities such as SR, SEG
|
||||
// Also, SM is not a simple imaging modalities, so exclude it.
|
||||
|
||||
@ -5,32 +5,15 @@ import {
|
||||
// ListMenu,
|
||||
WindowLevelMenuItem,
|
||||
} 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;
|
||||
/**
|
||||
*
|
||||
* @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 _createToggleButton = _createButton.bind(null, 'toggle');
|
||||
const _createToolButton = _createButton.bind(null, 'tool');
|
||||
const _createActionButton = ToolbarService._createButton.bind(null, 'action');
|
||||
const _createToggleButton = ToolbarService._createButton.bind(null, 'toggle');
|
||||
const _createToolButton = ToolbarService._createButton.bind(null, 'tool');
|
||||
|
||||
/**
|
||||
*
|
||||
@ -76,7 +59,21 @@ function _createSetToolActiveCommands(toolName) {
|
||||
return temp;
|
||||
}
|
||||
|
||||
const toolbarButtons = [
|
||||
const ReferenceLinesCommands: RunCommand = [
|
||||
{
|
||||
commandName: 'setSourceViewportForReferenceLinesTool',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'ReferenceLines',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
];
|
||||
|
||||
const toolbarButtons: Button[] = [
|
||||
// Measurement
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
@ -422,34 +419,37 @@ const toolbarButtons = [
|
||||
],
|
||||
'Flip Horizontal'
|
||||
),
|
||||
_createToggleButton('StackImageSync', 'link', 'Stack Image Sync', [
|
||||
_createToggleButton(
|
||||
'StackImageSync',
|
||||
'link',
|
||||
'Stack Image Sync',
|
||||
[
|
||||
{
|
||||
commandName: 'toggleStackImageSync',
|
||||
},
|
||||
],
|
||||
'Enable position synchronization on stack viewports',
|
||||
{
|
||||
commandName: 'toggleStackImageSync',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
]),
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
|
||||
commandName: 'toggleStackImageSync',
|
||||
commandOptions: { toggledState: true },
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
_createToggleButton(
|
||||
'ReferenceLines',
|
||||
'tool-referenceLines', // change this with the new icon
|
||||
'Reference Lines',
|
||||
// two commands for the reference lines tool:
|
||||
// - the first to set the source viewport for the tool when it is enabled
|
||||
// - the second to toggle the tool
|
||||
[
|
||||
{
|
||||
commandName: 'setSourceViewportForReferenceLinesTool',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
ReferenceLinesCommands,
|
||||
'Show Reference Lines',
|
||||
{
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
|
||||
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'ReferenceLines',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
]
|
||||
}
|
||||
),
|
||||
_createToggleButton(
|
||||
'ImageOverlayViewer',
|
||||
@ -465,8 +465,7 @@ const toolbarButtons = [
|
||||
},
|
||||
],
|
||||
'Image Overlay',
|
||||
null,
|
||||
true
|
||||
{ isActive: true }
|
||||
),
|
||||
_createToolButton(
|
||||
'StackScroll',
|
||||
@ -131,6 +131,8 @@ module.exports = (env, argv) => {
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
// Need to exclude the theme as it is updated independently
|
||||
exclude: [/theme/],
|
||||
// Cache large files for the manifests to avoid warning messages
|
||||
maximumFileSizeToCacheInBytes: 1024 * 1024 * 50,
|
||||
}),
|
||||
],
|
||||
// https://webpack.js.org/configuration/dev-server/
|
||||
|
||||
@ -74,7 +74,7 @@ function getRuntimeLoadModesExtensions(modules) {
|
||||
);
|
||||
});
|
||||
dynamicLoad.push(
|
||||
' return (await import(module)).default;',
|
||||
' return (await import(/* webpackIgnore: true */ module)).default;',
|
||||
'}\n',
|
||||
'// Import a list of items (modules or string names)',
|
||||
'// @return a Promise evaluating to a list of modules',
|
||||
|
||||
@ -409,20 +409,42 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
cy.get('@moreBtn').click();
|
||||
cy.get('.tooltip-toolbar-overlay').should('not.exist');
|
||||
});
|
||||
|
||||
it('check if Flip V tool will flip the image vertically 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();
|
||||
*/
|
||||
it('check if Flip tool will flip the image in the viewport', () => {
|
||||
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
|
||||
cy.get('@moreBtn').click();
|
||||
cy.get('.tooltip-toolbar-overlay').should('not.exist');
|
||||
});*/
|
||||
//Click on More button
|
||||
cy.get('@moreBtnSecondary').click();
|
||||
|
||||
//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)');
|
||||
});
|
||||
});
|
||||
|
||||
@ -36,6 +36,8 @@ describe('OHIF Download Snapshot File', () => {
|
||||
// Check buttons
|
||||
cy.get('[data-cy="cancel-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() {
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
describe('OHIF Study Viewer Page', function () {
|
||||
beforeEach(function () {
|
||||
cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78');
|
||||
|
||||
cy.expectMinimumThumbnails(3);
|
||||
cy.initCommonElementsAliases();
|
||||
cy.initCornerstoneToolsAliases();
|
||||
});
|
||||
describe('OHIF General Viewer', function () {
|
||||
beforeEach(() =>
|
||||
cy.initViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78', {
|
||||
minimumThumbnails: 3,
|
||||
})
|
||||
);
|
||||
|
||||
it('scrolls series stack using scrollbar', function () {
|
||||
cy.scrollToIndex(13);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
describe('OHIF Study Viewer Page', function () {
|
||||
describe('OHIF Study Browser', function () {
|
||||
beforeEach(function () {
|
||||
cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78');
|
||||
|
||||
|
||||
@ -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(
|
||||
'openStudyInViewer',
|
||||
(StudyInstanceUID, otherParams = '', mode = '/basic-test') => {
|
||||
@ -347,14 +359,9 @@ Cypress.Commands.add('percyCanvasSnapshot', (name, options = {}) => {
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setLayout', (columns = 1, rows = 1) => {
|
||||
cy.get('[data-cy="layout"]').click();
|
||||
cy.get('[data-cy="Layout"]').click();
|
||||
|
||||
cy.get('.layoutChooser')
|
||||
.find('tr')
|
||||
.eq(rows - 1)
|
||||
.find('td')
|
||||
.eq(columns - 1)
|
||||
.click();
|
||||
cy.get(`[data-cy="Layout-${columns - 1}-${rows - 1}"]`).click();
|
||||
|
||||
cy.wait(10);
|
||||
cy.waitDicomImage();
|
||||
|
||||
@ -38,8 +38,28 @@ window.config = {
|
||||
configuration: {
|
||||
friendlyName: 'Static WADO Local Data',
|
||||
name: 'DCM4CHEE',
|
||||
qidoRoot: '/dicomweb',
|
||||
wadoRoot: '/dicomweb',
|
||||
qidoRoot: 'http://localhost:5000/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,
|
||||
supportsReject: true,
|
||||
supportsStow: true,
|
||||
|
||||
@ -443,7 +443,7 @@ export default function ModeRoute({
|
||||
<ImageViewerProvider
|
||||
// initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }}
|
||||
StudyInstanceUIDs={studyInstanceUIDs}
|
||||
// reducer={reducer}
|
||||
// reducer={reducer}
|
||||
>
|
||||
<CombinedContextProvider>
|
||||
<DragAndDropProvider>
|
||||
|
||||
@ -2,12 +2,37 @@ import merge from 'lodash.merge';
|
||||
import { CommandsManager } from '../../classes';
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import type { RunCommand, Commands } from '../../types/Command';
|
||||
|
||||
const EVENTS = {
|
||||
TOOL_BAR_MODIFIED: 'event::toolBarService:toolBarModified',
|
||||
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 {
|
||||
public static REGISTRATION = {
|
||||
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: {
|
||||
primaryToolId: string;
|
||||
toggles: Record<string, boolean>;
|
||||
@ -54,7 +99,7 @@ export default class ToolbarService extends PubSubService {
|
||||
this.buttons = {};
|
||||
}
|
||||
|
||||
onModeEnter() {
|
||||
public onModeEnter(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
@ -65,7 +110,7 @@ export default class ToolbarService extends PubSubService {
|
||||
* used for calling the specified interaction. That is, the command is
|
||||
* called with {...commandOptions,...options}
|
||||
*/
|
||||
recordInteraction(interaction, options?: Record<string, unknown>) {
|
||||
public recordInteraction(interaction, options?: Record<string, unknown>) {
|
||||
if (!interaction) {
|
||||
return;
|
||||
}
|
||||
@ -174,12 +219,18 @@ export default class ToolbarService extends PubSubService {
|
||||
}
|
||||
|
||||
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 */
|
||||
public setActive(id: string, isActive: boolean): void {
|
||||
if (isActive) {
|
||||
/** Sets the toggle state of a button to the isToggled state */
|
||||
public setToggled(id: string, isToggled: boolean): void {
|
||||
if (isToggled) {
|
||||
this.state.toggles[id] = true;
|
||||
} else {
|
||||
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];
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
this.buttons = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||
@ -267,23 +333,22 @@ export default class ToolbarService extends PubSubService {
|
||||
if (!this.buttons[button.id]) {
|
||||
this.buttons[button.id] = button;
|
||||
}
|
||||
this._setTogglesForButtonItems(button.props?.items);
|
||||
});
|
||||
this._setTogglesForButtonItems(buttons);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||
}
|
||||
|
||||
_setTogglesForButtonItems(buttonItems) {
|
||||
if (!buttonItems) {
|
||||
_setTogglesForButtonItems(buttons) {
|
||||
if (!buttons) {
|
||||
return;
|
||||
}
|
||||
|
||||
buttonItems.forEach(buttonItem => {
|
||||
buttons.forEach(buttonItem => {
|
||||
if (buttonItem.type === 'toggle') {
|
||||
this.state.toggles[buttonItem.id] = buttonItem.isActive;
|
||||
} else {
|
||||
this._setTogglesForButtonItems(buttonItem.props?.items);
|
||||
this.setToggled(buttonItem.id, buttonItem.isActive);
|
||||
}
|
||||
this._setTogglesForButtonItems(buttonItem.props?.items);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,9 @@ export interface Command {
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export type RunCommand = Command | Command[];
|
||||
|
||||
/** A set of commands, typically contained in a tool item or other configuration */
|
||||
export interface Commands {
|
||||
commands: Command[];
|
||||
commands: RunCommand;
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ function LayoutSelector({ onSelection, rows, columns }) {
|
||||
border: '1px solid white',
|
||||
backgroundColor: isHovered(index) ? '#5acce6' : '#0b1a42',
|
||||
}}
|
||||
data-cy={`Layout-${index % columns}-${Math.floor(index / columns)}`}
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
const x = index % columns;
|
||||
|
||||
2
testdata
2
testdata
@ -1 +1 @@
|
||||
Subproject commit da63adc206455ce860b87c6fe723d71fb415a891
|
||||
Subproject commit 4d59660c2883ed749a680e5fb6d4624ab54c9422
|
||||
Loading…
Reference in New Issue
Block a user