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

+128 -233
View File
@@ -1,249 +1,144 @@
import { id } from './id';
import toolbarButtons from './toolbarButtons';
import initToolGroups from './initToolGroups';
import setUpAutoTabSwitchHandler from './utils/setUpAutoTabSwitchHandler';
import { ohif, cornerstone, extensionDependencies, dicomRT, segmentation } from '@ohif/mode-basic';
export * from './toolbarButtons';
import {
ohif,
cornerstone,
extensionDependencies,
dicomRT,
segmentation,
isValidMode,
onModeEnter as basicOnModeEnter,
onModeExit,
layoutTemplate,
modeFactory,
} from '@ohif/mode-basic';
function modeFactory({ modeConfiguration }) {
const _unsubscriptions = [];
return {
/**
* Mode ID, which should be unique among modes used by the viewer. This ID
* is used to identify the mode in the viewer's state.
*/
id,
routeName: 'segmentation',
/**
* Mode name, which is displayed in the viewer's UI in the workList, for the
* user to select the mode.
*/
displayName: 'Segmentation',
/**
* Runs when the Mode Route is mounted to the DOM. Usually used to initialize
* Services and other resources.
*/
onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => {
const {
measurementService,
toolbarService,
toolGroupService,
segmentationService,
viewportGridService,
panelService,
} = servicesManager.services;
/**
* Extends the basic mode enter with the segmentation panel auto tab switch
* handling (switching between labelmap/contour panels as segmentations of the
* relevant type become active).
*/
export function onModeEnter(ctx: withAppTypes) {
basicOnModeEnter.call(this, ctx);
measurementService.clearMeasurements();
const { segmentationService, viewportGridService, panelService } = ctx.servicesManager.services;
// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager);
const { unsubscribeAutoTabSwitchEvents } = setUpAutoTabSwitchHandler({
segmentationService,
viewportGridService,
panelService,
});
toolbarService.register(toolbarButtons);
toolbarService.updateSection(toolbarService.sections.primary, [
'WindowLevel',
'Pan',
'Zoom',
'TrackballRotate',
'Capture',
'Layout',
'Crosshairs',
'MoreTools',
]);
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topLeft, [
'orientationMenu',
'dataOverlayMenu',
]);
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomMiddle, [
'AdvancedRenderingControls',
]);
toolbarService.updateSection('AdvancedRenderingControls', [
'windowLevelMenuEmbedded',
'voiManualControlMenu',
'Colorbar',
'opacityMenu',
'thresholdMenu',
]);
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topRight, [
'modalityLoadBadge',
'trackingStatus',
'navigationComponent',
]);
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomLeft, [
'windowLevelMenu',
]);
toolbarService.updateSection('MoreTools', [
'Reset',
'rotate-right',
'flipHorizontal',
'ReferenceLines',
'ImageOverlayViewer',
'StackScroll',
'invert',
'Cine',
'Magnify',
'TagBrowser',
]);
toolbarService.updateSection(toolbarService.sections.labelMapSegmentationToolbox, [
'LabelMapTools',
]);
toolbarService.updateSection(toolbarService.sections.contourSegmentationToolbox, [
'ContourTools',
]);
toolbarService.updateSection('LabelMapTools', [
'LabelmapSlicePropagation',
'BrushTools',
'MarkerLabelmap',
'RegionSegmentPlus',
'Shapes',
'LabelMapEditWithContour',
]);
toolbarService.updateSection('ContourTools', [
'PlanarFreehandContourSegmentationTool',
'SculptorTool',
'SplineContourSegmentationTool',
'LivewireContourSegmentationTool',
]);
toolbarService.updateSection(toolbarService.sections.labelMapSegmentationUtilities, [
'LabelMapUtilities',
]);
toolbarService.updateSection(toolbarService.sections.contourSegmentationUtilities, [
'ContourUtilities',
]);
toolbarService.updateSection('LabelMapUtilities', [
'InterpolateLabelmap',
'SegmentBidirectional',
]);
toolbarService.updateSection('ContourUtilities', [
'LogicalContourOperations',
'SimplifyContours',
'SmoothContours',
]);
toolbarService.updateSection('BrushTools', ['Brush', 'Eraser', 'Threshold']);
const { unsubscribeAutoTabSwitchEvents } = setUpAutoTabSwitchHandler({
segmentationService,
viewportGridService,
panelService,
});
_unsubscriptions.push(...unsubscribeAutoTabSwitchEvents);
},
onModeExit: ({ servicesManager }: withAppTypes) => {
const {
toolGroupService,
syncGroupService,
segmentationService,
cornerstoneViewportService,
uiDialogService,
uiModalService,
} = servicesManager.services;
_unsubscriptions.forEach(unsubscribe => unsubscribe());
_unsubscriptions.length = 0;
uiDialogService.hideAll();
uiModalService.hide();
toolGroupService.destroy();
syncGroupService.destroy();
segmentationService.destroy();
cornerstoneViewportService.destroy();
},
/** */
validationTags: {
study: [],
series: [],
},
/**
* A boolean return value that indicates whether the mode is valid for the
* modalities of the selected studies. Currently we don't have stack viewport
* segmentations and we should exclude them
*/
isValidMode: ({ modalities }) => {
// Don't show the mode if the selected studies have only one modality
// that is not supported by the mode
const modalitiesArray = modalities.split('\\');
return {
valid:
modalitiesArray.length === 1
? !['SM', 'ECG', 'OT', 'DOC'].includes(modalitiesArray[0])
: true,
description:
'The mode does not support studies that ONLY include the following modalities: SM, OT, DOC',
};
},
/**
* Mode Routes are used to define the mode's behavior. A list of Mode Route
* that includes the mode's path and the layout to be used. The layout will
* include the components that are used in the layout. For instance, if the
* default layoutTemplate is used (id: '@ohif/extension-default.layoutTemplateModule.viewerLayout')
* it will include the leftPanels, rightPanels, and viewports. However, if
* you define another layoutTemplate that includes a Footer for instance,
* you should provide the Footer component here too. Note: We use Strings
* to reference the component's ID as they are registered in the internal
* ExtensionManager. The template for the string is:
* `${extensionId}.{moduleType}.${componentId}`.
*/
routes: [
{
path: 'template',
layoutTemplate: ({ location, servicesManager }) => {
return {
id: ohif.layout,
props: {
leftPanels: [ohif.thumbnailList],
leftPanelResizable: true,
rightPanels: [
cornerstone.labelMapSegmentationPanel,
cornerstone.contourSegmentationPanel,
],
rightPanelResizable: true,
// leftPanelClosed: true,
viewports: [
{
namespace: cornerstone.viewport,
displaySetsToDisplay: [ohif.sopClassHandler],
},
{
namespace: segmentation.viewport,
displaySetsToDisplay: [segmentation.sopClassHandler],
},
{
namespace: dicomRT.viewport,
displaySetsToDisplay: [dicomRT.sopClassHandler],
},
],
},
};
},
},
],
/** List of extensions that are used by the mode */
extensions: extensionDependencies,
/** HangingProtocol used by the mode */
// Commented out to just use the most applicable registered hanging protocol
// The example is used for a grid layout to specify that as a preferred layout
hangingProtocol: ['@ohif/mnGrid'],
/** SopClassHandlers used by the mode */
sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler, dicomRT.sopClassHandler],
};
this._unsubscriptions.push(...unsubscribeAutoTabSwitchEvents);
}
export const segmentationLayout = {
id: ohif.layout,
props: {
// Literal panel lists; the mode route seeds them into the standard
// `leftPanels` / `rightPanels` customizations so `mode` phase
// blocks and global customizations can modify them.
leftPanels: [ohif.thumbnailList],
leftPanelResizable: true,
rightPanels: [cornerstone.labelMapSegmentationPanel, cornerstone.contourSegmentationPanel],
rightPanelResizable: true,
viewports: [
{
namespace: cornerstone.viewport,
displaySetsToDisplay: [ohif.sopClassHandler],
},
{
namespace: segmentation.viewport,
displaySetsToDisplay: [segmentation.sopClassHandler],
},
{
namespace: dicomRT.viewport,
displaySetsToDisplay: [dicomRT.sopClassHandler],
},
],
},
};
export const segmentationRoute = {
path: 'template',
layoutTemplate,
layoutInstance: segmentationLayout,
};
export const modeInstance = {
id,
routeName: 'segmentation',
displayName: 'Segmentation',
// Toolbar/tool-group composition: which capability packs this mode uses.
// The mode route seeds these onto the Mode customization scope on enter, so
// `?customization=` modules extend them through the `mode` phase (e.g. add
// the annotation tools/buttons). Pack names are resolved when the toolbar is
// registered.
toolbarButtons: [
{ $reference: 'cornerstone.toolbarButtons' },
{ $reference: 'cornerstone.segmentationToolbarButtons' },
],
toolbarSections: [
{ $reference: 'cornerstone.segmentationModeToolbarSections' },
{ $reference: 'cornerstone.segmentationToolbarSections' },
],
toolGroupAdditions: {
default: [],
mpr: [],
volume3d: [],
},
// Tool group setup used by onModeEnter; extending modes can replace it.
initToolGroups,
// The mode's own customizations, applied by the mode route as the bottom
// layer of the mode scope. Unlike basic, the registered block is empty (no
// `panelSegmentation.disableEditing`): the segmentation panel is editable.
modeCustomizations: 'segmentationModeCustomizations',
activatePanelTriggers: [],
/**
* Lifecycle hooks
*/
onModeEnter,
onModeExit,
validationTags: {
study: [],
series: [],
},
// Data-driven validity: valid unless the study ONLY contains modalities that
// segmentation cannot be performed on.
isValidMode,
nonModeModalities: ['SM', 'ECG', 'OT', 'DOC'],
routes: [segmentationRoute],
extensions: extensionDependencies,
// Prefer the grid layout hanging protocol when applicable.
hangingProtocol: ['@ohif/mnGrid'],
sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler, dicomRT.sopClassHandler],
};
/**
* Customizations the mode registers (Default scope) when it loads. The mode's
* own block is empty — the segmentation panel is editable in this mode — but
* it is registered so bootstrap / `?customization=` modules can add
* mode-scoped values to it.
*/
export const customizations = {
segmentationModeCustomizations: {},
};
/**
* The mode uses the basic mode's `modeFactory`, which applies
* immutability-helper commands from `modeConfiguration` onto `modeInstance`,
* so a site can define a `mySegmentation` mode that extends this one.
*/
const mode = {
id,
modeFactory,
modeInstance,
extensionDependencies,
customizations,
};
export default mode;
export { initToolGroups };
+1 -1
View File
@@ -258,7 +258,7 @@ function initVolume3DToolGroup(extensionManager, toolGroupService) {
toolGroupService.createToolGroupAndAddTools('volume3d', tools);
}
function initToolGroups(extensionManager, toolGroupService, commandsManager) {
function initToolGroups({ extensionManager, toolGroupService, commandsManager }) {
initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, 'default');
initMPRToolGroup(extensionManager, toolGroupService, commandsManager);
initVolume3DToolGroup(extensionManager, toolGroupService);
File diff suppressed because it is too large. Load diff