feat: Add extensibility for tmtv and segmentation modes (#6128)

* feat: Add extensibility for tmtv and segmentation modes

* Fixes for ordering issues on laod

* Remove unnecessary reference lookup

* Chane side panel timing to fix tests

* PR comments - change how mode definitions get created

* Improvements to mode customizations

* Start organizing customizations

* Misc fixes for a customization demo page

* Security fixes

* PR requested changes to naming
This commit is contained in:
Bill Wallace authored and GitHub committed 2026-07-10 12:43:17 -04:00
1 parent f79055f98e
commit b266c0a86a
49 files changed
+2653 -1215

No files matched your search

+1
View File
@@ -40,6 +40,7 @@
},
"dependencies": {
"@babel/runtime": "7.29.7",
"@ohif/mode-basic": "workspace:*",
"i18next": "17.3.1"
},
"devDependencies": {
+165 -208
View File
@@ -1,7 +1,17 @@
import i18n from 'i18next';
import { ToolbarService } from '@ohif/core';
import {
isValidMode,
layoutTemplate,
modeFactory,
modeInstance as basicModeInstance,
} from '@ohif/mode-basic';
import toolbarButtons from './toolbarButtons';
import { id } from './id';
import initToolGroups from './initToolGroups';
import i18n from 'i18next';
const { TOOLBAR_SECTIONS } = ToolbarService;
// Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it.
@@ -67,231 +77,178 @@ const extensionDependencies = {
'@ohif/extension-test': '^0.0.1',
};
function modeFactory() {
return {
id,
routeName: 'basic-test',
displayName: i18n.t('Modes:Basic Test Mode'),
/**
* Lifecycle hooks
*/
onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => {
const { measurementService, toolbarService, toolGroupService, customizationService } =
servicesManager.services;
/**
* The test mode's toolbar layout, supplied as literal values rather than
* `{ $reference }` capability-pack markers (the composition is resolved the
* same way either — literals pass through untouched).
*/
const toolbarSections = {
[TOOLBAR_SECTIONS.primary]: [
'MeasurementTools',
'Zoom',
'WindowLevelGroup',
'Pan',
'Capture',
'Layout',
'MPR',
'Crosshairs',
'MoreTools',
],
measurementService.clearMeasurements();
WindowLevelGroup: ['WindowLevel', 'Soft tissue', 'Lung', 'Liver', 'Bone', 'Brain'],
// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager);
[TOOLBAR_SECTIONS.viewportActionMenu.topLeft]: ['orientationMenu', 'dataOverlayMenu'],
// init customizations
customizationService.setCustomizations([
'@ohif/extension-test.customizationModule.custom-context-menu',
]);
[TOOLBAR_SECTIONS.viewportActionMenu.bottomMiddle]: ['AdvancedRenderingControls'],
toolbarService.register(toolbarButtons);
console.debug('toolbarButtons', toolbarButtons);
toolbarService.updateSection(toolbarService.sections.primary, [
'MeasurementTools',
'Zoom',
'WindowLevelGroup',
'Pan',
'Capture',
'Layout',
'MPR',
'Crosshairs',
'MoreTools',
]);
AdvancedRenderingControls: ['voiManualControlMenu', 'Colorbar', 'opacityMenu', 'thresholdMenu'],
toolbarService.updateSection('WindowLevelGroup', [
'WindowLevel',
'Soft tissue',
'Lung',
'Liver',
'Bone',
'Brain',
]);
[TOOLBAR_SECTIONS.viewportActionMenu.topRight]: [
'modalityLoadBadge',
'trackingStatus',
'navigationComponent',
],
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topLeft, [
'orientationMenu',
'dataOverlayMenu',
]);
[TOOLBAR_SECTIONS.viewportActionMenu.bottomLeft]: ['windowLevelMenu'],
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomMiddle, [
'AdvancedRenderingControls',
]);
MeasurementTools: [
'Length',
'Bidirectional',
'ArrowAnnotate',
'EllipticalROI',
'CircleROI',
'PlanarFreehandROI',
'SplineROI',
'LivewireContour',
],
toolbarService.updateSection('AdvancedRenderingControls', [
'voiManualControlMenu',
'Colorbar',
'opacityMenu',
'thresholdMenu',
]);
MoreTools: [
'Reset',
'rotate-right',
'flipHorizontal',
'ImageSliceSync',
'ReferenceLines',
'ImageOverlayViewer',
'StackScroll',
'invert',
'Probe',
'Cine',
'Angle',
'CobbAngle',
'Magnify',
'RectangleROI',
'CalibrationLine',
'TagBrowser',
'AdvancedMagnify',
'UltrasoundDirectionalTool',
'WindowLevelRegion',
],
};
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topRight, [
'modalityLoadBadge',
'trackingStatus',
'navigationComponent',
]);
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomLeft, [
'windowLevelMenu',
]);
toolbarService.updateSection('WindowLevelGroup', [
'WindowLevel',
'Soft tissue',
'Lung',
'Liver',
'Bone',
'Brain',
]);
toolbarService.updateSection('MeasurementTools', [
'Length',
'Bidirectional',
'ArrowAnnotate',
'EllipticalROI',
'CircleROI',
'PlanarFreehandROI',
'SplineROI',
'LivewireContour',
]);
toolbarService.updateSection('MoreTools', [
'Reset',
'rotate-right',
'flipHorizontal',
'ImageSliceSync',
'ReferenceLines',
'ImageOverlayViewer',
'StackScroll',
'invert',
'Probe',
'Cine',
'Angle',
'CobbAngle',
'Magnify',
'RectangleROI',
'CalibrationLine',
'TagBrowser',
'AdvancedMagnify',
'UltrasoundDirectionalTool',
'WindowLevelRegion',
]);
customizationService.setCustomizations(
{
'ohif.hotkeyBindings': {
$push: [
{
commandName: 'undo',
label: 'Undo',
keys: ['ctrl+z'],
isEditable: true,
},
],
},
},
'mode'
);
},
onModeExit: ({ servicesManager }: withAppTypes) => {
const {
toolGroupService,
syncGroupService,
segmentationService,
cornerstoneViewportService,
uiDialogService,
uiModalService,
} = servicesManager.services;
uiDialogService.hideAll();
uiModalService.hide();
toolGroupService.destroy();
syncGroupService.destroy();
segmentationService.destroy();
cornerstoneViewportService.destroy();
},
validationTags: {
study: [],
series: [],
},
isValidMode: function ({ modalities }) {
const modalities_list = modalities.split('\\');
// Exclude non-image modalities
return {
valid: !!modalities_list.filter(modality => NON_IMAGE_MODALITIES.indexOf(modality) === -1)
.length,
description:
'The mode does not support studies that ONLY include the following modalities: SM, ECG, SR, SEG',
};
},
routes: [
export const basicTestLayout = {
id: ohif.layout,
props: {
// Literal panel lists; the shared layout template also accepts
// customization names here.
leftPanels: [tracked.thumbnailList],
leftPanelResizable: true,
rightPanels: [cornerstone.panel, tracked.measurements, testExtension.measurements],
rightPanelResizable: true,
viewports: [
{
path: 'basic-test',
layoutTemplate: () => {
return {
id: ohif.layout,
props: {
leftPanels: [tracked.thumbnailList],
leftPanelResizable: true,
rightPanels: [cornerstone.panel, tracked.measurements, testExtension.measurements],
rightPanelResizable: true,
viewports: [
{
namespace: tracked.viewport,
displaySetsToDisplay: [
ohif.sopClassHandler,
dicomvideo.sopClassHandler,
ohif.wsiSopClassHandler,
],
},
{
namespace: dicomsr.viewport,
displaySetsToDisplay: [dicomsr.sopClassHandler, dicomsr.sopClassHandler3D],
},
{
namespace: dicompdf.viewport,
displaySetsToDisplay: [dicompdf.sopClassHandler],
},
{
namespace: dicomSeg.viewport,
displaySetsToDisplay: [dicomSeg.sopClassHandler],
},
{
namespace: dicomPmap.viewport,
displaySetsToDisplay: [dicomPmap.sopClassHandler],
},
],
},
};
},
namespace: tracked.viewport,
displaySetsToDisplay: [
ohif.sopClassHandler,
dicomvideo.sopClassHandler,
ohif.wsiSopClassHandler,
],
},
{
namespace: dicomsr.viewport,
displaySetsToDisplay: [dicomsr.sopClassHandler, dicomsr.sopClassHandler3D],
},
{
namespace: dicompdf.viewport,
displaySetsToDisplay: [dicompdf.sopClassHandler],
},
{
namespace: dicomSeg.viewport,
displaySetsToDisplay: [dicomSeg.sopClassHandler],
},
{
namespace: dicomPmap.viewport,
displaySetsToDisplay: [dicomPmap.sopClassHandler],
},
],
extensions: extensionDependencies,
hangingProtocol: 'default',
sopClassHandlers: [
dicomvideo.sopClassHandler,
dicomSeg.sopClassHandler,
ohif.wsiSopClassHandler,
ohif.sopClassHandler,
dicompdf.sopClassHandler,
dicomsr.sopClassHandler,
dicomsr.sopClassHandler3D,
],
hotkeys: {
name: 'basic-test-hotkeys',
},
};
export const basicTestRoute = {
path: 'basic-test',
layoutTemplate,
layoutInstance: basicTestLayout,
};
/**
* Extends the basic mode instance: the shared onModeEnter/onModeExit are
* inherited, and the test specifics (toolbar layout, tool groups, test
* customizations) are supplied as instance data.
*/
export const modeInstance = {
...basicModeInstance,
id,
routeName: 'basic-test',
displayName: i18n.t('Modes:Basic Test Mode'),
// Literal toolbar values instead of the basic mode's customization names.
toolbarButtons,
toolbarSections,
// Tool group setup used by the shared onModeEnter.
initToolGroups,
// The mode's own customizations, applied by the mode route as the bottom
// layer of the mode scope: the test extension's custom context menu, plus
// the undo hotkey used by the E2E tests. Given as a literal here; modes may
// also reference a registered block by name (see `basicModeCustomizations`).
modeCustomizations: [
'@ohif/extension-test.customizationModule.custom-context-menu',
{
'ohif.hotkeyBindings': {
$push: [
{
commandName: 'undo',
label: 'Undo',
keys: ['ctrl+z'],
isEditable: true,
},
],
},
},
};
}
],
isValidMode,
nonModeModalities: NON_IMAGE_MODALITIES,
routes: [basicTestRoute],
extensions: extensionDependencies,
hangingProtocol: 'default',
sopClassHandlers: [
dicomvideo.sopClassHandler,
dicomSeg.sopClassHandler,
ohif.wsiSopClassHandler,
ohif.sopClassHandler,
dicompdf.sopClassHandler,
dicomsr.sopClassHandler,
dicomsr.sopClassHandler3D,
],
hotkeys: {
name: 'basic-test-hotkeys',
},
};
const mode = {
id,
modeFactory,
modeInstance,
extensionDependencies,
};
export default mode;
export { initToolGroups };
+1 -1
View File
@@ -267,7 +267,7 @@ function initVolume3DToolGroup(extensionManager, toolGroupService) {
toolGroupService.createToolGroupAndAddTools('volume3d', tools);
}
function initToolGroups(extensionManager, toolGroupService, commandsManager) {
function initToolGroups({ extensionManager, toolGroupService, commandsManager }) {
initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, 'default');
initSRToolGroup(extensionManager, toolGroupService, commandsManager);
initMPRToolGroup(extensionManager, toolGroupService, commandsManager);