From 1b6fa2194e390576f1411d5e6475cb0c8edb241a Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 8 Jul 2026 14:11:24 -0400 Subject: [PATCH] Revert "feat: Add extensibility for tmtv and segmentation modes" This reverts commit 84c7d17b1333f076188d0cc59cc6f52ae6d0da59. --- README.md | 27 - .../toolGroupToolsCustomization.ts | 200 ------- .../toolbarButtonsCustomization.ts | 21 - .../src/getCustomizationModule.tsx | 4 - .../src/getCustomizationModule.ts | 15 +- extensions/tmtv/src/getCustomizationModule.ts | 17 - extensions/tmtv/src/index.tsx | 4 - extensions/tmtv/src/toolGroupIds.ts | 14 - modes/basic/src/index.tsx | 98 ++-- modes/basic/src/modeCustomization.ts | 124 ----- modes/longitudinal/src/index.ts | 8 +- modes/segmentation/src/index.tsx | 357 ++++++++----- .../segmentation/src/toolbarButtons.ts | 502 +++++++++++++----- modes/tmtv/package.json | 4 +- modes/tmtv/src/index.ts | 438 +++++++-------- modes/tmtv/src/initToolGroups.js | 10 +- .../tmtv/src/toolbarButtons.ts | 82 +-- platform/app/public/config/dev.js | 13 +- .../segmentationAnnotationTools.jsonc | 40 -- .../customizations/segmentationEditing.jsonc | 49 -- .../3p12-to-3p13/customization-url.md | 20 +- .../specificCustomizations.md | 40 -- 22 files changed, 887 insertions(+), 1200 deletions(-) delete mode 100644 extensions/cornerstone/src/customizations/toolGroupToolsCustomization.ts delete mode 100644 extensions/tmtv/src/getCustomizationModule.ts delete mode 100644 extensions/tmtv/src/toolGroupIds.ts delete mode 100644 modes/basic/src/modeCustomization.ts rename extensions/cornerstone/src/customizations/segmentationToolbarCustomization.ts => modes/segmentation/src/toolbarButtons.ts (72%) rename extensions/tmtv/src/customizations/toolbarCustomization.ts => modes/tmtv/src/toolbarButtons.ts (82%) delete mode 100644 platform/app/public/customizations/segmentationAnnotationTools.jsonc delete mode 100644 platform/app/public/customizations/segmentationEditing.jsonc diff --git a/README.md b/README.md index 005fa5fbe..a6d08f4bd 100644 --- a/README.md +++ b/README.md @@ -244,32 +244,6 @@ also supports a number of commands that can be found in their respective \* - For more information on different builds, check out our [Deploy Docs][deployment-docs] -### Which config each command uses - -The dev server and the production build select a different default -[configuration file][config-file] when `APP_CONFIG` is not set explicitly: - -| Command | Default config | Data sources | `?customization=` | -| ------------------------------- | -------------------- | ------------ | ----------------- | -| `dev`, `dev:fast`, `start` | `config/dev.js` | Full set | Enabled | -| `build` | `config/default.js` | One demo source | Disabled | - -- **`config/dev.js`** is the full-featured local-development config: every data - source is enabled, the `?customization=` URL feature is turned on (via - `customizationUrlPrefixes`), and it is kept at parity with the public demo - (`config/netlify.js`) so customizations behave locally the same way they do on - the demo. -- **`config/netlify.js`** is the public demo / Netlify deploy config - (`build:viewer:ci`), with the same full data-source set and `?customization=` - enabled. -- **`config/default.js`** is a locked-down baseline and is now **only** the - default for a plain production build (`build` with no `APP_CONFIG`): a single - read-only demo data source and `?customization=` off. - -Any explicit `APP_CONFIG` overrides the default, e.g. -`APP_CONFIG=config/default.js pnpm run dev` or -`APP_CONFIG=config/netlify.js pnpm run build`. - ## Project The OHIF Medical Image Viewing Platform is maintained as a @@ -397,7 +371,6 @@ MIT © [OHIF](https://github.com/OHIF) [ohif-architecture]: https://docs.ohif.org/architecture/index.html [ohif-extensions]: https://docs.ohif.org/architecture/index.html [deployment-docs]: https://docs.ohif.org/deployment/ -[config-file]: https://docs.ohif.org/configuration/configurationFiles [react-url]: https://reactjs.org/ [pwa-url]: https://developers.google.com/web/progressive-web-apps/ [ohif-viewer-url]: https://www.npmjs.com/package/@ohif/app diff --git a/extensions/cornerstone/src/customizations/toolGroupToolsCustomization.ts b/extensions/cornerstone/src/customizations/toolGroupToolsCustomization.ts deleted file mode 100644 index e8a862607..000000000 --- a/extensions/cornerstone/src/customizations/toolGroupToolsCustomization.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { toolNames } from '../initCornerstoneTools'; -import { - MIN_SEGMENTATION_DRAWING_RADIUS, - MAX_SEGMENTATION_DRAWING_RADIUS, -} from './segmentationToolbarCustomization'; - -/** - * Reusable tool group "capability blocks", registered as default - * customizations so modes and `?customization=` JSON modules can add them to - * a tool group by name via a mode's `toolGroupAdditions` customization, e.g. - * - * "basic.toolGroupAdditions": { - * "default": { "$push": ["cornerstone.segmentationToolGroupTools"] } - * } - * - * Each block is a `{ active/passive/enabled/disabled }` object suitable for - * `toolGroupService.addToolsToToolGroup`. - */ -function getToolGroupToolsCustomization({ commandsManager }) { - const brushInstances = [ - { - toolName: 'CircularBrush', - parentTool: 'Brush', - configuration: { - activeStrategy: 'FILL_INSIDE_CIRCLE', - minRadius: MIN_SEGMENTATION_DRAWING_RADIUS, - maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS, - }, - }, - { - toolName: 'CircularEraser', - parentTool: 'Brush', - configuration: { - activeStrategy: 'ERASE_INSIDE_CIRCLE', - minRadius: MIN_SEGMENTATION_DRAWING_RADIUS, - maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS, - }, - }, - { - toolName: 'SphereBrush', - parentTool: 'Brush', - configuration: { - activeStrategy: 'FILL_INSIDE_SPHERE', - minRadius: MIN_SEGMENTATION_DRAWING_RADIUS, - maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS, - }, - }, - { - toolName: 'SphereEraser', - parentTool: 'Brush', - configuration: { - activeStrategy: 'ERASE_INSIDE_SPHERE', - minRadius: MIN_SEGMENTATION_DRAWING_RADIUS, - maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS, - }, - }, - { - toolName: 'ThresholdCircularBrush', - parentTool: 'Brush', - configuration: { - activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', - minRadius: MIN_SEGMENTATION_DRAWING_RADIUS, - maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS, - }, - }, - { - toolName: 'ThresholdSphereBrush', - parentTool: 'Brush', - configuration: { - activeStrategy: 'THRESHOLD_INSIDE_SPHERE', - minRadius: MIN_SEGMENTATION_DRAWING_RADIUS, - maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS, - }, - }, - { - toolName: 'ThresholdCircularBrushDynamic', - parentTool: 'Brush', - configuration: { - activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', - minRadius: MIN_SEGMENTATION_DRAWING_RADIUS, - maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS, - threshold: { - isDynamic: true, - dynamicRadius: 3, - }, - }, - }, - { - toolName: 'ThresholdSphereBrushDynamic', - parentTool: 'Brush', - configuration: { - activeStrategy: 'THRESHOLD_INSIDE_SPHERE', - minRadius: MIN_SEGMENTATION_DRAWING_RADIUS, - maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS, - threshold: { - isDynamic: true, - dynamicRadius: 3, - }, - }, - }, - ]; - - const splineInstances = [ - { - toolName: 'CatmullRomSplineROI', - parentTool: toolNames.SplineContourSegmentation, - configuration: { - spline: { - type: 'CATMULLROM', - enableTwoPointPreview: true, - }, - }, - }, - { - toolName: 'LinearSplineROI', - parentTool: toolNames.SplineContourSegmentation, - configuration: { - spline: { - type: 'LINEAR', - enableTwoPointPreview: true, - }, - }, - }, - { - toolName: 'BSplineROI', - parentTool: toolNames.SplineContourSegmentation, - configuration: { - spline: { - type: 'BSPLINE', - enableTwoPointPreview: true, - }, - }, - }, - ]; - - return { - /** - * The segmentation editing tools (labelmap brushes/scissors and contour - * segmentation tools), matching the buttons in - * `cornerstone.segmentationToolbarButtons`. - */ - 'cornerstone.segmentationToolGroupTools': { - passive: [ - ...brushInstances, - { toolName: toolNames.LabelmapSlicePropagation }, - { toolName: toolNames.MarkerLabelmap }, - { toolName: toolNames.RegionSegmentPlus }, - { toolName: toolNames.LabelMapEditWithContourTool }, - { toolName: toolNames.SegmentSelect }, - { toolName: toolNames.CircleScissors }, - { toolName: toolNames.RectangleScissors }, - { toolName: toolNames.SphereScissors }, - { toolName: toolNames.LivewireContourSegmentation }, - { toolName: toolNames.SculptorTool }, - ...splineInstances, - ], - }, - - /** - * The measurement/annotation tools, matching the `MeasurementTools` - * buttons in `cornerstone.toolbarButtons`. Useful for adding annotations - * to modes (such as segmentation) whose tool groups omit them. - */ - 'cornerstone.annotationToolGroupTools': { - passive: [ - { toolName: toolNames.Length }, - { - toolName: toolNames.ArrowAnnotate, - configuration: { - getTextCallback: (callback, eventDetails) => { - commandsManager.runCommand('arrowTextCallback', { - callback, - eventDetails, - }); - }, - changeTextCallback: (data, eventDetails, callback) => { - commandsManager.runCommand('arrowTextCallback', { - callback, - data, - eventDetails, - }); - }, - }, - }, - { toolName: toolNames.Bidirectional }, - { toolName: toolNames.Probe }, - { toolName: toolNames.DragProbe }, - { toolName: toolNames.EllipticalROI }, - { toolName: toolNames.CircleROI }, - { toolName: toolNames.RectangleROI }, - { toolName: toolNames.Angle }, - { toolName: toolNames.CobbAngle }, - { toolName: toolNames.SplineROI }, - { toolName: toolNames.LivewireContour }, - ], - }, - }; -} - -export default getToolGroupToolsCustomization; diff --git a/extensions/cornerstone/src/customizations/toolbarButtonsCustomization.ts b/extensions/cornerstone/src/customizations/toolbarButtonsCustomization.ts index 704dbdd53..c134cd363 100644 --- a/extensions/cornerstone/src/customizations/toolbarButtonsCustomization.ts +++ b/extensions/cornerstone/src/customizations/toolbarButtonsCustomization.ts @@ -794,27 +794,6 @@ export const toolbarSections = { const toolbarButtonsCustomization = { 'cornerstone.toolbarButtons': toolbarButtons, 'cornerstone.toolbarSections': toolbarSections, - - // What the basic (and longitudinal) mode reads by name. The list values may - // reference other customizations, so a `?customization=` module can extend a - // mode with an exported capability block by pushing its name, e.g. - // `"basic.toolbarButtons": { "$push": ["cornerstone.segmentationToolbarButtons"] }`. - 'basic.toolbarButtons': ['cornerstone.toolbarButtons'], - 'basic.toolbarSections': ['cornerstone.toolbarSections'], - // Extra tools layered onto the mode's tool groups after creation; entries - // reference tool blocks such as `cornerstone.segmentationToolGroupTools`. - 'basic.toolGroupAdditions': { - default: [], - mpr: [], - SRToolGroup: [], - volume3d: [], - }, - // Panel lists resolved by the basic mode's layout template. - 'basic.leftPanels': ['@ohif/extension-default.panelModule.seriesList'], - 'basic.rightPanels': [ - '@ohif/extension-cornerstone.panelModule.panelSegmentation', - '@ohif/extension-cornerstone.panelModule.panelMeasurement', - ], }; export { toolbarButtons }; diff --git a/extensions/cornerstone/src/getCustomizationModule.tsx b/extensions/cornerstone/src/getCustomizationModule.tsx index ddb4e8dfe..206afc5ab 100644 --- a/extensions/cornerstone/src/getCustomizationModule.tsx +++ b/extensions/cornerstone/src/getCustomizationModule.tsx @@ -9,8 +9,6 @@ import colorbarCustomization from './customizations/colorbarCustomization'; import modalityColorMapCustomization from './customizations/modalityColorMapCustomization'; import windowLevelPresetsCustomization from './customizations/windowLevelPresetsCustomization'; import toolbarButtonsCustomization from './customizations/toolbarButtonsCustomization'; -import segmentationToolbarCustomization from './customizations/segmentationToolbarCustomization'; -import getToolGroupToolsCustomization from './customizations/toolGroupToolsCustomization'; import miscCustomization from './customizations/miscCustomization'; import captureViewportModalCustomization from './customizations/captureViewportModalCustomization'; import viewportDownloadWarningCustomization from './customizations/viewportDownloadWarningCustomization'; @@ -36,8 +34,6 @@ function getCustomizationModule({ commandsManager, servicesManager, extensionMan ...modalityColorMapCustomization, ...windowLevelPresetsCustomization, ...toolbarButtonsCustomization, - ...segmentationToolbarCustomization, - ...getToolGroupToolsCustomization({ commandsManager }), ...miscCustomization, ...captureViewportModalCustomization, ...viewportDownloadWarningCustomization, diff --git a/extensions/measurement-tracking/src/getCustomizationModule.ts b/extensions/measurement-tracking/src/getCustomizationModule.ts index 83becb253..d2bd35775 100644 --- a/extensions/measurement-tracking/src/getCustomizationModule.ts +++ b/extensions/measurement-tracking/src/getCustomizationModule.ts @@ -4,20 +4,7 @@ export default function getCustomizationModule() { return [ { name: 'default', - value: { - ...measurementTrackingPrompts, - - // Panel lists resolved by the longitudinal mode's layout template; - // `?customization=` modules can replace them (e.g. to swap in the - // segmentation panels with editing tools). - 'longitudinal.leftPanels': [ - '@ohif/extension-measurement-tracking.panelModule.seriesList', - ], - 'longitudinal.rightPanels': [ - '@ohif/extension-cornerstone.panelModule.panelSegmentation', - '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', - ], - }, + value: { ...measurementTrackingPrompts }, }, ]; } diff --git a/extensions/tmtv/src/getCustomizationModule.ts b/extensions/tmtv/src/getCustomizationModule.ts deleted file mode 100644 index 5d02e3fd2..000000000 --- a/extensions/tmtv/src/getCustomizationModule.ts +++ /dev/null @@ -1,17 +0,0 @@ -import toolbarCustomization from './customizations/toolbarCustomization'; - -/** - * Registers the TMTV defaults (toolbar buttons/sections, tool group - * additions and panel lists) so the TMTV mode can reference them by name and - * `?customization=` modules can extend them. - */ -export default function getCustomizationModule() { - return [ - { - name: 'default', - value: { - ...toolbarCustomization, - }, - }, - ]; -} diff --git a/extensions/tmtv/src/index.tsx b/extensions/tmtv/src/index.tsx index b9e034815..9d853c996 100644 --- a/extensions/tmtv/src/index.tsx +++ b/extensions/tmtv/src/index.tsx @@ -4,8 +4,6 @@ import getPanelModule from './getPanelModule'; import init from './init'; import commandsModule from './commandsModule'; import getToolbarModule from './getToolbarModule'; -import getCustomizationModule from './getCustomizationModule'; -import { toolGroupIds } from './toolGroupIds'; /** * @@ -21,7 +19,6 @@ const tmtvExtension = { getToolbarModule, getPanelModule, getHangingProtocolModule, - getCustomizationModule, getCommandsModule({ servicesManager, commandsManager, extensionManager }) { return commandsModule({ servicesManager, @@ -32,4 +29,3 @@ const tmtvExtension = { }; export default tmtvExtension; -export { toolGroupIds }; diff --git a/extensions/tmtv/src/toolGroupIds.ts b/extensions/tmtv/src/toolGroupIds.ts deleted file mode 100644 index d741d04f6..000000000 --- a/extensions/tmtv/src/toolGroupIds.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * The tool group ids used by the TMTV hanging protocols and mode. Defined in - * the extension so both the extension (hanging protocol viewports, toolbar - * buttons) and the mode (tool group creation) share one definition. - */ -export const toolGroupIds = { - CT: 'ctToolGroup', - PT: 'ptToolGroup', - Fusion: 'fusionToolGroup', - MIP: 'mipToolGroup', - default: 'default', -}; - -export default toolGroupIds; diff --git a/modes/basic/src/index.tsx b/modes/basic/src/index.tsx index 57e1d62f3..223d1d1a0 100644 --- a/modes/basic/src/index.tsx +++ b/modes/basic/src/index.tsx @@ -2,11 +2,6 @@ import update from 'immutability-helper'; import { utils } from '@ohif/core'; import initToolGroups from './initToolGroups'; -import { - applyToolGroupAdditions, - registerModeToolbar, - resolvePanelList, -} from './modeCustomization'; import { id } from './id'; const { structuredCloneWithFunctions } = utils; @@ -133,38 +128,41 @@ export function isValidMode({ modalities }) { }; } -export function onModeEnter({ servicesManager, extensionManager, commandsManager }: withAppTypes) { - const { - measurementService, - toolbarService, - toolGroupService, - customizationService, - panelService, - segmentationService, - } = servicesManager.services; +export function onModeEnter({ + servicesManager, + extensionManager, + commandsManager, + panelService, + segmentationService, +}: withAppTypes) { + const { measurementService, toolbarService, toolGroupService, customizationService } = + servicesManager.services; measurementService.clearMeasurements(); - // Init the mode's tool groups. The function is a mode instance property so - // extending modes can substitute their own tool group setup. - this.initToolGroups?.(extensionManager, toolGroupService, commandsManager); + // Init Default and SR ToolGroups + initToolGroups(extensionManager, toolGroupService, commandsManager); - // Toolbar buttons and layout are supplied as customization references - // (extensions register the defaults; `?customization=` modules can extend - // them) or as literal values for modes that define them inline. - registerModeToolbar({ toolbarService, customizationService }, this); + // Toolbar buttons and layout may be supplied either as a customization name + // (a string, resolved through the customization service so `?customization=` + // modules can extend the cornerstone-registered defaults) or as a literal + // value (the button array / sections object) for modes that define them inline. + const resolveToolbarCustomization = (value: unknown) => + typeof value === 'string' ? customizationService.getCustomization(value) : value; - // Extra tools (e.g. segmentation editing tools added by a customization) - // are layered onto the tool groups created above. - applyToolGroupAdditions({ toolGroupService, customizationService }, this.toolGroupAdditions); + const toolbarButtons = resolveToolbarCustomization(this.toolbarButtons) as any; + const toolbarSections = (resolveToolbarCustomization(this.toolbarSections) ?? {}) as Record< + string, + string[] + >; - // Segmentation panel editing is disabled by default in this mode, but only - // when no global/mode customization has expressed a preference — a site - // customization (e.g. `segmentationEditing`) can enable it. - if ( - !this.enableSegmentationEdit && - !customizationService.hasCustomization('panelSegmentation.disableEditing') - ) { + toolbarService.register(toolbarButtons); + + for (const [key, section] of Object.entries(toolbarSections)) { + toolbarService.updateSection(key, section); + } + + if (!this.enableSegmentationEdit) { customizationService.setCustomizations({ 'panelSegmentation.disableEditing': { $set: true, @@ -228,12 +226,9 @@ export function onModeExit({ servicesManager }: withAppTypes) { export const basicLayout = { id: ohif.layout, props: { - // Panel lists are customization names; the cornerstone extension registers - // the defaults and `?customization=` modules can replace them (e.g. to - // swap in the segmentation panels with editing tools). - leftPanels: 'basic.leftPanels', + leftPanels: [ohif.thumbnailList], leftPanelResizable: true, - rightPanels: 'basic.rightPanels', + rightPanels: [cornerstone.segmentation, cornerstone.measurements], rightPanelClosed: true, rightPanelResizable: true, viewports: [ @@ -270,15 +265,8 @@ export const basicLayout = { }, }; -export function layoutTemplate({ servicesManager }: withAppTypes = {} as withAppTypes) { - const layout = structuredCloneWithFunctions(this.layoutInstance); - const customizationService = servicesManager?.services?.customizationService; - if (customizationService) { - const { props } = layout; - props.leftPanels = resolvePanelList(customizationService, props.leftPanels); - props.rightPanels = resolvePanelList(customizationService, props.rightPanels); - } - return layout; +export function layoutTemplate() { + return structuredCloneWithFunctions(this.layoutInstance); } export const basicRoute = { @@ -297,14 +285,10 @@ export const modeInstance = { hide: false, displayName: 'Non-Longitudinal Basic', _activatePanelTriggersSubscriptions: [], - // Toolbar buttons/layout and tool group additions are referenced by - // customization name; the cornerstone extension registers the defaults and - // `?customization=` modules can extend them. onModeEnter resolves these - // names via the customization service. - toolbarSections: 'basic.toolbarSections', - toolGroupAdditions: 'basic.toolGroupAdditions', - // Tool group setup used by onModeEnter; extending modes can replace it. - initToolGroups, + // Toolbar buttons and layout are referenced by customization name; the + // cornerstone extension registers the defaults and `?customization=` modules + // can extend them. onModeEnter resolves these names via the customization service. + toolbarSections: 'cornerstone.toolbarSections', /** * Lifecycle hooks @@ -326,7 +310,7 @@ export const modeInstance = { // general handler needs to come last. For this case, the dicomvideo must // come first to remove video transfer syntax before ohif uses images sopClassHandlers, - toolbarButtons: 'basic.toolbarButtons', + toolbarButtons: 'cornerstone.toolbarButtons', enableSegmentationEdit: false, nonModeModalities: NON_IMAGE_MODALITIES, }; @@ -352,9 +336,3 @@ export const mode = { export default mode; export { initToolGroups }; -export { - applyToolGroupAdditions, - registerModeToolbar, - resolveCustomizationList, - resolvePanelList, -} from './modeCustomization'; diff --git a/modes/basic/src/modeCustomization.ts b/modes/basic/src/modeCustomization.ts deleted file mode 100644 index e6d2ab8f3..000000000 --- a/modes/basic/src/modeCustomization.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Helpers for resolving mode configuration (toolbar buttons, toolbar sections, - * tool group additions and panel lists) through the customization service. - * - * The pattern: extensions register the default values as named customizations - * (at Default scope), modes reference those values by name, and site - * `?customization=` JSON modules extend or replace the named values with - * immutability-helper commands. Wherever a list is resolved, a string entry is - * treated as the name of another customization, so a JSON module can add a - * whole capability block that an extension exports (for example - * `$push: ['cornerstone.segmentationToolbarButtons']`) without having to - * restate its contents. - */ - -/** - * Resolves a customization list value into a flat array of concrete values. - * - * The input may be: - * - a string: the name of a customization to resolve (recursively) - * - an array: each entry resolved recursively (strings are names, objects - * are literal values) - * - an object: a literal value, returned as a single-element array - * - * Names are only resolved once per call (repeats and cycles are skipped), so a - * capability block referenced from two places is applied a single time. - */ -export function resolveCustomizationList( - customizationService: AppTypes.CustomizationService, - value: unknown, - _seen = new Set() -): any[] { - if (value === undefined || value === null) { - return []; - } - if (typeof value === 'string') { - if (_seen.has(value)) { - return []; - } - _seen.add(value); - const resolved = customizationService.getCustomization(value); - if (resolved === undefined) { - console.warn(`resolveCustomizationList: no customization registered for "${value}"`); - return []; - } - return resolveCustomizationList(customizationService, resolved, _seen); - } - if (Array.isArray(value)) { - return value.flatMap(entry => resolveCustomizationList(customizationService, entry, _seen)); - } - return [value]; -} - -/** - * Registers a mode's toolbar from customization references. - * - * `toolbarButtons` resolves to a flat list of button definitions which are - * registered with the toolbar service. `toolbarSections` resolves to one or - * more `{ sectionKey: buttonIds[] }` objects which are shallow-merged in order - * (later values win per section) and applied with `updateSection`. - */ -export function registerModeToolbar( - { toolbarService, customizationService }, - { toolbarButtons, toolbarSections } -): void { - const buttons = resolveCustomizationList(customizationService, toolbarButtons); - toolbarService.register(buttons); - - const sections: Record = Object.assign( - {}, - ...resolveCustomizationList(customizationService, toolbarSections) - ); - for (const [key, section] of Object.entries(sections)) { - toolbarService.updateSection(key, section); - } -} - -/** - * Adds extra tools to the tool groups a mode has already created. - * - * `toolGroupAdditions` is (the name of) an object mapping a tool group id to a - * list of tool blocks; each block is either a literal - * `{ active/passive/enabled/disabled }` object or the name of a customization - * holding one (for example `cornerstone.segmentationToolGroupTools`). Tool - * groups the mode did not create are skipped, so a single additions object can - * be shared between modes with different tool group sets. - */ -export function applyToolGroupAdditions( - { toolGroupService, customizationService }, - toolGroupAdditions -): void { - if (!toolGroupAdditions) { - return; - } - for (const additions of resolveCustomizationList(customizationService, toolGroupAdditions)) { - for (const [toolGroupId, toolBlocks] of Object.entries(additions)) { - if (!toolGroupService.getToolGroup(toolGroupId)) { - continue; - } - for (const tools of resolveCustomizationList(customizationService, toolBlocks)) { - toolGroupService.addToolsToToolGroup(toolGroupId, tools); - } - } - } -} - -/** - * Resolves a layout panel list. Panel lists hold extension module ids (which - * are themselves strings), so unlike the list helpers above only the top-level - * value may be a customization name. - */ -export function resolvePanelList( - customizationService: AppTypes.CustomizationService, - panels: string | string[] -): string[] { - if (typeof panels !== 'string') { - return panels; - } - const resolved = customizationService.getCustomization(panels); - if (resolved === undefined) { - console.warn(`resolvePanelList: no customization registered for "${panels}"`); - return []; - } - return resolved as string[]; -} diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts index 5cb6a342d..792ac3a01 100644 --- a/modes/longitudinal/src/index.ts +++ b/modes/longitudinal/src/index.ts @@ -1,6 +1,6 @@ import i18n from 'i18next'; import { id } from './id'; -import { initToolGroups, +import { initToolGroups, cornerstone, ohif, dicomsr, dicomvideo, @@ -28,10 +28,8 @@ export const longitudinalInstance = { id: ohif.layout, props: { ...basicLayout.props, - // Panel lists are customization names; the measurement-tracking extension - // registers the defaults and `?customization=` modules can replace them. - leftPanels: 'longitudinal.leftPanels', - rightPanels: 'longitudinal.rightPanels', + leftPanels: [tracked.thumbnailList], + rightPanels: [cornerstone.segmentation, tracked.measurements], viewports: [ { namespace: tracked.viewport, diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx index 70fd0772c..51f08e2d7 100644 --- a/modes/segmentation/src/index.tsx +++ b/modes/segmentation/src/index.tsx @@ -1,138 +1,249 @@ import { id } from './id'; +import toolbarButtons from './toolbarButtons'; import initToolGroups from './initToolGroups'; import setUpAutoTabSwitchHandler from './utils/setUpAutoTabSwitchHandler'; -import { - ohif, - cornerstone, - extensionDependencies, - dicomRT, - segmentation, - onModeEnter as basicOnModeEnter, - onModeExit as basicOnModeExit, - layoutTemplate, - modeFactory, -} from '@ohif/mode-basic'; +import { ohif, cornerstone, extensionDependencies, dicomRT, segmentation } from '@ohif/mode-basic'; +export * from './toolbarButtons'; -/** - * Indicate this is a valid mode unless the studies ONLY contain modalities - * that segmentation cannot be performed on. - */ -export function isValidMode({ modalities }) { - const modalitiesArray = modalities.split('\\'); +function modeFactory({ modeConfiguration }) { + const _unsubscriptions = []; 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 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; + + measurementService.clearMeasurements(); + + // Init Default and SR ToolGroups + initToolGroups(extensionManager, toolGroupService, commandsManager); + + 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], }; } -/** - * 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); - - const { segmentationService, viewportGridService, panelService } = - ctx.servicesManager.services; - - const { unsubscribeAutoTabSwitchEvents } = setUpAutoTabSwitchHandler({ - segmentationService, - viewportGridService, - panelService, - }); - - this._unsubscriptions.push(...unsubscribeAutoTabSwitchEvents); -} - -export function onModeExit(ctx: withAppTypes) { - this._unsubscriptions.forEach(unsubscribe => unsubscribe()); - this._unsubscriptions.length = 0; - - basicOnModeExit.call(this, ctx); -} - -export const segmentationLayout = { - id: ohif.layout, - props: { - // Panel lists are customization names; the cornerstone extension registers - // the defaults and `?customization=` modules can replace them. - leftPanels: 'segmentation.leftPanels', - leftPanelResizable: true, - rightPanels: 'segmentation.rightPanels', - 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', - _activatePanelTriggersSubscriptions: [], - _unsubscriptions: [], - // Toolbar buttons/layout and tool group additions are referenced by - // customization name; the cornerstone extension registers the defaults and - // `?customization=` modules can extend them (e.g. add new segmentation - // tools, remove defaults, or add the annotation tools). - toolbarButtons: 'segmentation.toolbarButtons', - toolbarSections: 'segmentation.toolbarSections', - toolGroupAdditions: 'segmentation.toolGroupAdditions', - // Tool group setup used by onModeEnter; extending modes can replace it. - initToolGroups, - // The segmentation panel is editable in this mode. - enableSegmentationEdit: true, - - /** - * Lifecycle hooks - */ - onModeEnter, - onModeExit, - validationTags: { - study: [], - series: [], - }, - - isValidMode, - routes: [segmentationRoute], - extensions: extensionDependencies, - // Prefer the grid layout hanging protocol when applicable. - hangingProtocol: ['@ohif/mnGrid'], - sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler, dicomRT.sopClassHandler], -}; - -/** - * 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, }; export default mode; -export { initToolGroups }; diff --git a/extensions/cornerstone/src/customizations/segmentationToolbarCustomization.ts b/modes/segmentation/src/toolbarButtons.ts similarity index 72% rename from extensions/cornerstone/src/customizations/segmentationToolbarCustomization.ts rename to modes/segmentation/src/toolbarButtons.ts index 0de4a5d41..ecf2a5b36 100644 --- a/extensions/cornerstone/src/customizations/segmentationToolbarCustomization.ts +++ b/modes/segmentation/src/toolbarButtons.ts @@ -1,11 +1,8 @@ import type { Button } from '@ohif/core/types'; -import { ViewportGridService, ToolbarService } from '@ohif/core'; +import { ViewportGridService } from '@ohif/core'; import i18n from 'i18next'; -const { TOOLBAR_SECTIONS } = ToolbarService; - -export const MIN_SEGMENTATION_DRAWING_RADIUS = 0.5; -export const MAX_SEGMENTATION_DRAWING_RADIUS = 99.5; +import { MIN_SEGMENTATION_DRAWING_RADIUS, MAX_SEGMENTATION_DRAWING_RADIUS } from './constants'; const setToolActiveToolbar = { commandName: 'setToolActiveToolbar', @@ -23,16 +20,155 @@ const callbacks = (toolName: string) => [ }, ]; -/** - * Segmentation editing toolbar buttons: the toolbox section containers plus - * the labelmap / contour editing tools and utilities. These complement the - * general buttons in `cornerstone.toolbarButtons`; together they are the - * default button set for the segmentation mode, and modes such as basic / - * longitudinal can pull them in via a customization (see - * `segmentationEditing.jsonc`). - */ -const segmentationToolbarButtons: Button[] = [ - // section containers for the nested toolboxes and toolbars +export const toolbarButtons: Button[] = [ + { + id: 'AdvancedRenderingControls', + uiType: 'ohif.advancedRenderingControls', + props: { + buttonSection: true, + }, + }, + { + id: 'modalityLoadBadge', + uiType: 'ohif.modalityLoadBadge', + props: { + icon: 'Status', + label: i18n.t('Buttons:Status'), + tooltip: i18n.t('Buttons:Status'), + evaluate: { + name: 'evaluate.modalityLoadBadge', + hideWhenDisabled: true, + }, + }, + }, + { + id: 'navigationComponent', + uiType: 'ohif.navigationComponent', + props: { + icon: 'Navigation', + label: i18n.t('Buttons:Navigation'), + tooltip: i18n.t('Buttons:Navigate between segments/measurements and manage their visibility'), + evaluate: { + name: 'evaluate.navigationComponent', + hideWhenDisabled: true, + }, + }, + }, + { + id: 'trackingStatus', + uiType: 'ohif.trackingStatus', + props: { + icon: 'TrackingStatus', + label: i18n.t('Buttons:Tracking Status'), + tooltip: i18n.t('Buttons:View and manage tracking status of measurements and annotations'), + evaluate: { + name: 'evaluate.trackingStatus', + hideWhenDisabled: true, + }, + }, + }, + { + id: 'dataOverlayMenu', + uiType: 'ohif.dataOverlayMenu', + props: { + icon: 'ViewportViews', + label: i18n.t('Buttons:Data Overlay'), + tooltip: i18n.t( + 'Buttons:Configure data overlay options and manage foreground/background display sets' + ), + evaluate: 'evaluate.dataOverlayMenu', + }, + }, + { + id: 'orientationMenu', + uiType: 'ohif.orientationMenu', + props: { + icon: 'OrientationSwitch', + label: i18n.t('Buttons:Orientation'), + tooltip: i18n.t( + 'Buttons:Change viewport orientation between axial, sagittal, coronal and reformat planes' + ), + evaluate: { + name: 'evaluate.orientationMenu', + // hideWhenDisabled: true, + }, + }, + }, + { + id: 'windowLevelMenuEmbedded', + uiType: 'ohif.windowLevelMenuEmbedded', + props: { + icon: 'WindowLevel', + label: i18n.t('Buttons:Window Level'), + tooltip: i18n.t('Buttons:Adjust window/level presets and customize image contrast settings'), + evaluate: { + name: 'evaluate.windowLevelMenuEmbedded', + hideWhenDisabled: true, + }, + }, + }, + { + id: 'windowLevelMenu', + uiType: 'ohif.windowLevelMenu', + props: { + icon: 'WindowLevel', + label: i18n.t('Buttons:Window Level'), + tooltip: i18n.t('Buttons:Adjust window/level presets and customize image contrast settings'), + evaluate: 'evaluate.windowLevelMenu', + }, + }, + { + id: 'voiManualControlMenu', + uiType: 'ohif.voiManualControlMenu', + props: { + icon: 'WindowLevelAdvanced', + label: i18n.t('Buttons:Advanced Window Level'), + tooltip: i18n.t('Buttons:Advanced window/level settings with manual controls and presets'), + evaluate: 'evaluate.voiManualControlMenu', + }, + }, + { + id: 'thresholdMenu', + uiType: 'ohif.thresholdMenu', + props: { + icon: 'Threshold', + label: i18n.t('Buttons:Threshold'), + tooltip: i18n.t('Buttons:Image threshold settings'), + evaluate: { + name: 'evaluate.thresholdMenu', + hideWhenDisabled: true, + }, + }, + }, + { + id: 'opacityMenu', + uiType: 'ohif.opacityMenu', + props: { + icon: 'Opacity', + label: i18n.t('Buttons:Opacity'), + tooltip: i18n.t('Buttons:Image opacity settings'), + evaluate: { + name: 'evaluate.opacityMenu', + hideWhenDisabled: true, + }, + }, + }, + { + id: 'Colorbar', + uiType: 'ohif.colorbar', + props: { + type: 'tool', + label: i18n.t('Buttons:Colorbar'), + }, + }, + // sections + { + id: 'MoreTools', + uiType: 'ohif.toolButtonList', + props: { + buttonSection: true, + }, + }, { id: 'BrushTools', uiType: 'ohif.toolBoxButtonGroup', @@ -40,6 +176,7 @@ const segmentationToolbarButtons: Button[] = [ buttonSection: true, }, }, + // Section containers for the nested toolboxes and toolbars. { id: 'LabelMapUtilities', uiType: 'ohif.Toolbar', @@ -69,6 +206,215 @@ const segmentationToolbarButtons: Button[] = [ }, }, // tool defs + { + id: 'Zoom', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-zoom', + label: i18n.t('Buttons:Zoom'), + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'WindowLevel', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-window-level', + label: i18n.t('Buttons:Window Level'), + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'Pan', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-move', + label: i18n.t('Buttons:Pan'), + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'TrackballRotate', + uiType: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-3d-rotate', + label: i18n.t('Buttons:3D Rotate'), + commands: setToolActiveToolbar, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: i18n.t('Buttons:Select a 3D viewport to enable this tool'), + }, + }, + }, + { + id: 'Capture', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-capture', + label: i18n.t('Buttons:Capture'), + commands: 'showDownloadViewportModal', + evaluate: [ + 'evaluate.action', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'wholeSlide'], + }, + ], + }, + }, + { + id: 'Layout', + uiType: 'ohif.layoutSelector', + props: { + rows: 3, + columns: 4, + evaluate: 'evaluate.action', + commands: 'setViewportGridLayout', + }, + }, + { + id: 'Crosshairs', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-crosshair', + label: i18n.t('Buttons:Crosshairs'), + commands: { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: ['mpr'], + }, + }, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: i18n.t('Buttons:Select an MPR viewport to enable this tool'), + }, + }, + }, + { + id: 'Reset', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-reset', + label: i18n.t('Buttons:Reset View'), + tooltip: i18n.t('Buttons:Reset View'), + commands: 'resetViewport', + evaluate: 'evaluate.action', + }, + }, + { + id: 'rotate-right', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-rotate-right', + label: i18n.t('Buttons:Rotate Right'), + tooltip: i18n.t('Buttons:Rotate +90'), + commands: 'rotateViewportCW', + evaluate: 'evaluate.action', + }, + }, + { + id: 'flipHorizontal', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-flip-horizontal', + label: i18n.t('Buttons:Flip Horizontal'), + tooltip: i18n.t('Buttons:Flip Horizontally'), + commands: 'flipViewportHorizontal', + evaluate: [ + 'evaluate.viewportProperties.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, + ], + }, + }, + { + id: 'ReferenceLines', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-referenceLines', + label: i18n.t('Buttons:Reference Lines'), + tooltip: i18n.t('Buttons:Show Reference Lines'), + commands: 'toggleEnabledDisabledToolbar', + evaluate: 'evaluate.cornerstoneTool.toggle', + }, + }, + { + id: 'ImageOverlayViewer', + uiType: 'ohif.toolButton', + props: { + icon: 'toggle-dicom-overlay', + label: i18n.t('Buttons:Image Overlay'), + tooltip: i18n.t('Buttons:Toggle Image Overlay'), + commands: 'toggleEnabledDisabledToolbar', + evaluate: 'evaluate.cornerstoneTool.toggle', + }, + }, + { + id: 'StackScroll', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-stack-scroll', + label: i18n.t('Buttons:Stack Scroll'), + tooltip: i18n.t('Buttons:Stack Scroll'), + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'invert', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-invert', + label: i18n.t('Buttons:Invert'), + tooltip: i18n.t('Buttons:Invert Colors'), + commands: 'invertViewport', + evaluate: 'evaluate.viewportProperties.toggle', + }, + }, + { + id: 'Cine', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-cine', + label: i18n.t('Buttons:Cine'), + tooltip: i18n.t('Buttons:Cine'), + commands: 'toggleCine', + evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, + ], + }, + }, + { + id: 'Magnify', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-magnify', + label: i18n.t('Buttons:Zoom-in'), + tooltip: i18n.t('Buttons:Zoom-in'), + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'TagBrowser', + uiType: 'ohif.toolButton', + props: { + icon: 'dicom-tag-browser', + label: i18n.t('Buttons:Dicom Tag Browser'), + tooltip: i18n.t('Buttons:Dicom Tag Browser'), + commands: 'openDICOMTagViewer', + }, + }, { id: 'PlanarFreehandContourSegmentationTool', uiType: 'ohif.toolBoxButton', @@ -213,11 +559,7 @@ const segmentationToolbarButtons: Button[] = [ value: 'CatmullRomSplineROI', label: i18n.t('Buttons:Catmull Rom Spline'), }, - { - id: 'LinearSplineROI', - value: 'LinearSplineROI', - label: i18n.t('Buttons:Linear Spline'), - }, + { id: 'LinearSplineROI', value: 'LinearSplineROI', label: i18n.t('Buttons:Linear Spline') }, { id: 'BSplineROI', value: 'BSplineROI', label: i18n.t('Buttons:B-Spline') }, ], commands: { @@ -853,122 +1195,4 @@ const segmentationToolbarButtons: Button[] = [ }, ]; -/** - * The toolbox / utilities section wiring for segmentation editing. These are - * the sections rendered by the `panelSegmentationWithTools*` panels, so any - * mode that shows those panels can merge this block into its toolbar sections. - */ -export const segmentationToolboxSections: Record = { - [TOOLBAR_SECTIONS.labelMapSegmentationToolbox]: ['LabelMapTools'], - [TOOLBAR_SECTIONS.contourSegmentationToolbox]: ['ContourTools'], - [TOOLBAR_SECTIONS.labelMapSegmentationUtilities]: ['LabelMapUtilities'], - [TOOLBAR_SECTIONS.contourSegmentationUtilities]: ['ContourUtilities'], - - LabelMapTools: [ - 'LabelmapSlicePropagation', - 'BrushTools', - 'MarkerLabelmap', - 'RegionSegmentPlus', - 'Shapes', - 'LabelMapEditWithContour', - ], - ContourTools: [ - 'PlanarFreehandContourSegmentationTool', - 'SculptorTool', - 'SplineContourSegmentationTool', - 'LivewireContourSegmentationTool', - ], - - LabelMapUtilities: ['InterpolateLabelmap', 'SegmentBidirectional'], - ContourUtilities: ['LogicalContourOperations', 'SimplifyContours', 'SmoothContours'], - - BrushTools: ['Brush', 'Eraser', 'Threshold'], -}; - -/** - * The segmentation mode's main toolbar layout (primary bar and viewport - * action corners). Kept separate from the toolbox wiring above so other modes - * can adopt segmentation editing without adopting this mode layout. - */ -export const segmentationModeToolbarSections: Record = { - [TOOLBAR_SECTIONS.primary]: [ - 'WindowLevel', - 'Pan', - 'Zoom', - 'TrackballRotate', - 'Capture', - 'Layout', - 'Crosshairs', - 'MoreTools', - ], - - [TOOLBAR_SECTIONS.viewportActionMenu.topLeft]: ['orientationMenu', 'dataOverlayMenu'], - - [TOOLBAR_SECTIONS.viewportActionMenu.bottomMiddle]: ['AdvancedRenderingControls'], - - AdvancedRenderingControls: [ - 'windowLevelMenuEmbedded', - 'voiManualControlMenu', - 'Colorbar', - 'opacityMenu', - 'thresholdMenu', - ], - - [TOOLBAR_SECTIONS.viewportActionMenu.topRight]: [ - 'modalityLoadBadge', - 'trackingStatus', - 'navigationComponent', - ], - - [TOOLBAR_SECTIONS.viewportActionMenu.bottomLeft]: ['windowLevelMenu'], - - MoreTools: [ - 'Reset', - 'rotate-right', - 'flipHorizontal', - 'ReferenceLines', - 'ImageOverlayViewer', - 'StackScroll', - 'invert', - 'Cine', - 'Magnify', - 'TagBrowser', - ], -}; - -/** - * Customizations registered (at default scope) by the cornerstone extension: - * - `cornerstone.segmentationToolbarButtons` – segmentation editing button definitions - * - `cornerstone.segmentationToolbarSections` – toolbox/utilities section wiring - * - `cornerstone.segmentationModeToolbarSections` – the segmentation mode's toolbar layout - * - `segmentation.*` – what the segmentation mode reads by name; lists may - * reference other customizations, so `?customization=` modules can extend - * them by pushing names of exported capability blocks. - */ -const segmentationToolbarCustomization = { - 'cornerstone.segmentationToolbarButtons': segmentationToolbarButtons, - 'cornerstone.segmentationToolbarSections': segmentationToolboxSections, - 'cornerstone.segmentationModeToolbarSections': segmentationModeToolbarSections, - - 'segmentation.toolbarButtons': [ - 'cornerstone.toolbarButtons', - 'cornerstone.segmentationToolbarButtons', - ], - 'segmentation.toolbarSections': [ - 'cornerstone.segmentationModeToolbarSections', - 'cornerstone.segmentationToolbarSections', - ], - 'segmentation.toolGroupAdditions': { - default: [], - mpr: [], - volume3d: [], - }, - 'segmentation.leftPanels': ['@ohif/extension-default.panelModule.seriesList'], - 'segmentation.rightPanels': [ - '@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsLabelMap', - '@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsContour', - ], -}; - -export { segmentationToolbarButtons }; -export default segmentationToolbarCustomization; +export default toolbarButtons; diff --git a/modes/tmtv/package.json b/modes/tmtv/package.json index db33c51de..3e38791d6 100644 --- a/modes/tmtv/package.json +++ b/modes/tmtv/package.json @@ -35,9 +35,7 @@ "@ohif/extension-default": "workspace:*", "@ohif/extension-dicom-pdf": "workspace:*", "@ohif/extension-dicom-video": "workspace:*", - "@ohif/extension-measurement-tracking": "workspace:*", - "@ohif/extension-tmtv": "workspace:*", - "@ohif/mode-basic": "workspace:*" + "@ohif/extension-measurement-tracking": "workspace:*" }, "dependencies": { "@babel/runtime": "7.29.7", diff --git a/modes/tmtv/src/index.ts b/modes/tmtv/src/index.ts index 49c6fceb1..33f74a798 100644 --- a/modes/tmtv/src/index.ts +++ b/modes/tmtv/src/index.ts @@ -1,38 +1,32 @@ import { classes } from '@ohif/core'; -import { - applyToolGroupAdditions, - layoutTemplate, - modeFactory, - registerModeToolbar, -} from '@ohif/mode-basic'; -import i18n from 'i18next'; - +import toolbarButtons from './toolbarButtons'; import { id } from './id.js'; import initToolGroups from './initToolGroups.js'; import setCrosshairsConfiguration from './utils/setCrosshairsConfiguration.js'; import setFusionActiveVolume from './utils/setFusionActiveVolume.js'; +import i18n from 'i18next'; const { MetadataProvider } = classes; -export const ohif = { +const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', thumbnailList: '@ohif/extension-default.panelModule.seriesList', }; -export const cs3d = { +const cs3d = { viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', segPanel: '@ohif/extension-cornerstone.panelModule.panelSegmentationNoHeader', measurements: '@ohif/extension-cornerstone.panelModule.measurements', }; -export const tmtv = { +const tmtv = { hangingProtocol: '@ohif/extension-tmtv.hangingProtocolModule.ptCT', petSUV: '@ohif/extension-tmtv.panelModule.petSUV', tmtv: '@ohif/extension-tmtv.panelModule.tmtv', }; -export const extensionDependencies = { +const extensionDependencies = { // Can derive the versions at least process.env.from npm_package_version '@ohif/extension-default': '^3.0.0', '@ohif/extension-cornerstone': '^3.0.0', @@ -40,214 +34,234 @@ export const extensionDependencies = { '@ohif/extension-tmtv': '^3.0.0', }; -export function onModeEnter({ servicesManager, extensionManager, commandsManager }: withAppTypes) { - const { - toolbarService, - toolGroupService, - customizationService, - hangingProtocolService, - displaySetService, - } = servicesManager.services; - - const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone.utilityModule.tools' - ); - - const { toolNames, Enums } = utilityModule.exports; - - // Init Default and SR ToolGroups - this.initToolGroups?.(toolNames, Enums, toolGroupService, commandsManager); - - const { unsubscribe } = toolGroupService.subscribe( - toolGroupService.EVENTS.VIEWPORT_ADDED, - () => { - // For fusion toolGroup we need to add the volumeIds for the crosshairs - // since in the fusion viewport we don't want both PT and CT to render MIP - // when slabThickness is modified - const { displaySetMatchDetails } = hangingProtocolService.getMatchDetails(); - - setCrosshairsConfiguration( - displaySetMatchDetails, - toolNames, - toolGroupService, - displaySetService - ); - - setFusionActiveVolume( - displaySetMatchDetails, - toolNames, - toolGroupService, - displaySetService - ); - } - ); - - this._unsubscriptions.push(unsubscribe); - - // Toolbar buttons and layout are supplied as customization references (the - // tmtv extension registers the defaults; `?customization=` modules can - // extend them) or as literal values for modes that define them inline. - registerModeToolbar({ toolbarService, customizationService }, this); - - // Extra tools (e.g. annotation tools added by a customization) are layered - // onto the tool groups created above. - applyToolGroupAdditions({ toolGroupService, customizationService }, this.toolGroupAdditions); - - customizationService.setCustomizations({ - 'panelSegmentation.tableMode': { - $set: 'expanded', - }, - 'panelSegmentation.onSegmentationAdd': { - $set: () => { - commandsManager.run('createNewLabelmapFromPT'); - }, - }, - }); - - // For the hanging protocol we need to decide on the window level - // based on whether the SUV is corrected or not, hence we can't hard - // code the window level in the hanging protocol but we add a custom - // attribute to the hanging protocol that will be used to get the - // window level based on the metadata - hangingProtocolService.addCustomAttribute( - 'getPTVOIRange', - 'get PT VOI based on corrected or not', - props => { - const ptDisplaySet = props.find(imageSet => imageSet.Modality === 'PT'); - - if (!ptDisplaySet) { - return; - } - - const { imageId } = ptDisplaySet.images[0]; - const imageIdScalingFactor = MetadataProvider.get('scalingModule', imageId); - - const isSUVAvailable = imageIdScalingFactor && imageIdScalingFactor.suvbw; - - if (isSUVAvailable) { - return { - windowWidth: 5, - windowCenter: 2.5, - }; - } - - return; - } - ); -} - -export function onModeExit({ servicesManager }: withAppTypes) { - const { - toolGroupService, - syncGroupService, - segmentationService, - cornerstoneViewportService, - uiDialogService, - uiModalService, - } = servicesManager.services; - - this._unsubscriptions.forEach(unsubscribe => unsubscribe()); - this._unsubscriptions.length = 0; - - uiDialogService.hideAll(); - uiModalService.hide(); - toolGroupService.destroy(); - syncGroupService.destroy(); - segmentationService.destroy(); - cornerstoneViewportService.destroy(); -} - -export function isValidMode({ modalities, study }) { - const modalities_list = modalities.split('\\'); - const invalidModalities = ['SM']; - - const isValid = - modalities_list.includes('CT') && - study.mrn !== 'M1' && - modalities_list.includes('PT') && - !invalidModalities.some(modality => modalities_list.includes(modality)) && - // This is study is a 4D study with PT and CT and not a 3D study for the tmtv - // mode, until we have a better way to identify 4D studies we will use the - // StudyInstanceUID to identify the study - // Todo: when we add the 4D mode which comes with a mechanism to identify - // 4D studies we can use that - study.studyInstanceUid !== '1.3.6.1.4.1.12842.1.1.14.3.20220915.105557.468.2963630849'; - - // there should be both CT and PT modalities and the modality should not be SM +const unsubscriptions = []; +function modeFactory({ modeConfiguration }) { return { - valid: isValid, - description: 'The mode requires both PT and CT series in the study', + // TODO: We're using this as a route segment + // We should not be. + id, + routeName: 'tmtv', + displayName: i18n.t('Modes:Total Metabolic Tumor Volume'), + /** + * Lifecycle hooks + */ + onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => { + const { + toolbarService, + toolGroupService, + customizationService, + hangingProtocolService, + displaySetService, + } = servicesManager.services; + + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + // Init Default and SR ToolGroups + initToolGroups(toolNames, Enums, toolGroupService, commandsManager); + + const { unsubscribe } = toolGroupService.subscribe( + toolGroupService.EVENTS.VIEWPORT_ADDED, + () => { + // For fusion toolGroup we need to add the volumeIds for the crosshairs + // since in the fusion viewport we don't want both PT and CT to render MIP + // when slabThickness is modified + const { displaySetMatchDetails } = hangingProtocolService.getMatchDetails(); + + setCrosshairsConfiguration( + displaySetMatchDetails, + toolNames, + toolGroupService, + displaySetService + ); + + setFusionActiveVolume( + displaySetMatchDetails, + toolNames, + toolGroupService, + displaySetService + ); + } + ); + + unsubscriptions.push(unsubscribe); + toolbarService.register(toolbarButtons); + toolbarService.updateSection(toolbarService.sections.primary, [ + 'MeasurementTools', + 'Zoom', + 'Pan', + 'WindowLevel', + 'Crosshairs', + ]); + + 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('MeasurementTools', [ + 'Length', + 'Bidirectional', + 'ArrowAnnotate', + 'EllipticalROI', + ]); + + toolbarService.updateSection('ROIThresholdToolbox', ['SegmentationTools']); + toolbarService.updateSection('SegmentationTools', [ + 'RectangleROIStartEndThreshold', + 'BrushTools', + ]); + + toolbarService.updateSection('BrushTools', ['Brush', 'Eraser', 'Threshold']); + + customizationService.setCustomizations({ + 'panelSegmentation.tableMode': { + $set: 'expanded', + }, + 'panelSegmentation.onSegmentationAdd': { + $set: () => { + commandsManager.run('createNewLabelmapFromPT'); + }, + }, + }); + + // For the hanging protocol we need to decide on the window level + // based on whether the SUV is corrected or not, hence we can't hard + // code the window level in the hanging protocol but we add a custom + // attribute to the hanging protocol that will be used to get the + // window level based on the metadata + hangingProtocolService.addCustomAttribute( + 'getPTVOIRange', + 'get PT VOI based on corrected or not', + props => { + const ptDisplaySet = props.find(imageSet => imageSet.Modality === 'PT'); + + if (!ptDisplaySet) { + return; + } + + const { imageId } = ptDisplaySet.images[0]; + const imageIdScalingFactor = MetadataProvider.get('scalingModule', imageId); + + const isSUVAvailable = imageIdScalingFactor && imageIdScalingFactor.suvbw; + + if (isSUVAvailable) { + return { + windowWidth: 5, + windowCenter: 2.5, + }; + } + + return; + } + ); + }, + onModeExit: ({ servicesManager }: withAppTypes) => { + const { + toolGroupService, + syncGroupService, + segmentationService, + cornerstoneViewportService, + uiDialogService, + uiModalService, + } = servicesManager.services; + + unsubscriptions.forEach(unsubscribe => unsubscribe()); + uiDialogService.hideAll(); + uiModalService.hide(); + toolGroupService.destroy(); + syncGroupService.destroy(); + segmentationService.destroy(); + cornerstoneViewportService.destroy(); + }, + validationTags: { + study: [], + series: [], + }, + isValidMode: ({ modalities, study }) => { + const modalities_list = modalities.split('\\'); + const invalidModalities = ['SM']; + + const isValid = + modalities_list.includes('CT') && + study.mrn !== 'M1' && + modalities_list.includes('PT') && + !invalidModalities.some(modality => modalities_list.includes(modality)) && + // This is study is a 4D study with PT and CT and not a 3D study for the tmtv + // mode, until we have a better way to identify 4D studies we will use the + // StudyInstanceUID to identify the study + // Todo: when we add the 4D mode which comes with a mechanism to identify + // 4D studies we can use that + study.studyInstanceUid !== '1.3.6.1.4.1.12842.1.1.14.3.20220915.105557.468.2963630849'; + + // there should be both CT and PT modalities and the modality should not be SM + return { + valid: isValid, + description: 'The mode requires both PT and CT series in the study', + }; + }, + routes: [ + { + path: 'tmtv', + /*init: ({ servicesManager, extensionManager }) => { + //defaultViewerRouteInit + },*/ + layoutTemplate: () => { + return { + id: ohif.layout, + props: { + leftPanels: [ohif.thumbnailList], + leftPanelResizable: true, + leftPanelClosed: true, + rightPanels: [tmtv.tmtv, tmtv.petSUV], + rightPanelResizable: true, + viewports: [ + { + namespace: cs3d.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + hangingProtocol: tmtv.hangingProtocol, + sopClassHandlers: [ohif.sopClassHandler], + ...modeConfiguration, }; } -export const tmtvLayout = { - id: ohif.layout, - props: { - // Panel lists are customization names; the tmtv extension registers the - // defaults and `?customization=` modules can replace them. - leftPanels: 'tmtv.leftPanels', - leftPanelResizable: true, - leftPanelClosed: true, - rightPanels: 'tmtv.rightPanels', - rightPanelResizable: true, - viewports: [ - { - namespace: cs3d.viewport, - displaySetsToDisplay: [ohif.sopClassHandler], - }, - ], - }, -}; - -export const tmtvRoute = { - path: 'tmtv', - layoutTemplate, - layoutInstance: tmtvLayout, -}; - -export const modeInstance = { - // TODO: We're using this as a route segment - // We should not be. - id, - routeName: 'tmtv', - displayName: i18n.t('Modes:Total Metabolic Tumor Volume'), - _unsubscriptions: [], - // Toolbar buttons/layout and tool group additions are referenced by - // customization name; the tmtv extension registers the defaults and - // `?customization=` modules can extend them. - toolbarButtons: 'tmtv.toolbarButtons', - toolbarSections: 'tmtv.toolbarSections', - toolGroupAdditions: 'tmtv.toolGroupAdditions', - // Tool group setup used by onModeEnter; extending modes can replace it. - initToolGroups, - - /** - * Lifecycle hooks - */ - onModeEnter, - onModeExit, - validationTags: { - study: [], - series: [], - }, - isValidMode, - routes: [tmtvRoute], - extensions: extensionDependencies, - hangingProtocol: tmtv.hangingProtocol, - sopClassHandlers: [ohif.sopClassHandler], -}; - -/** - * The mode uses the basic mode's `modeFactory`, which applies - * immutability-helper commands from `modeConfiguration` onto `modeInstance`, - * so a site can define a mode that extends this one. - */ const mode = { id, modeFactory, - modeInstance, extensionDependencies, }; export default mode; -export { initToolGroups }; diff --git a/modes/tmtv/src/initToolGroups.js b/modes/tmtv/src/initToolGroups.js index 6d283e099..f88b1cf84 100644 --- a/modes/tmtv/src/initToolGroups.js +++ b/modes/tmtv/src/initToolGroups.js @@ -1,8 +1,12 @@ -import { toolGroupIds } from '@ohif/extension-tmtv'; - import { MIN_SEGMENTATION_DRAWING_RADIUS, MAX_SEGMENTATION_DRAWING_RADIUS } from './constants'; -export { toolGroupIds }; +export const toolGroupIds = { + CT: 'ctToolGroup', + PT: 'ptToolGroup', + Fusion: 'fusionToolGroup', + MIP: 'mipToolGroup', + default: 'default', +}; function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager) { const tools = { diff --git a/extensions/tmtv/src/customizations/toolbarCustomization.ts b/modes/tmtv/src/toolbarButtons.ts similarity index 82% rename from extensions/tmtv/src/customizations/toolbarCustomization.ts rename to modes/tmtv/src/toolbarButtons.ts index 611e77d7c..c47357c56 100644 --- a/extensions/tmtv/src/customizations/toolbarCustomization.ts +++ b/modes/tmtv/src/toolbarButtons.ts @@ -1,12 +1,7 @@ -import { ToolbarService } from '@ohif/core'; +import { toolGroupIds } from './initToolGroups'; import i18n from 'i18next'; -import { toolGroupIds } from '../toolGroupIds'; - -const { TOOLBAR_SECTIONS } = ToolbarService; - -export const MIN_SEGMENTATION_DRAWING_RADIUS = 0.5; -export const MAX_SEGMENTATION_DRAWING_RADIUS = 99.5; +import { MIN_SEGMENTATION_DRAWING_RADIUS, MAX_SEGMENTATION_DRAWING_RADIUS } from './constants'; const setToolActiveToolbar = { commandName: 'setToolActiveToolbar', @@ -15,11 +10,6 @@ const setToolActiveToolbar = { }, }; -/** - * Default toolbar buttons for the TMTV mode, registered as the - * `tmtv.toolbarButtons` customization so `?customization=` modules can extend - * or replace them. - */ const toolbarButtons = [ { id: 'MeasurementTools', @@ -311,11 +301,7 @@ const toolbarButtons = [ evaluate: [ { name: 'evaluate.cornerstone.segmentation', - toolNames: [ - 'ThresholdCircularBrush', - 'ThresholdSphereBrush', - 'ThresholdCircularBrushDynamic', - ], + toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush', 'ThresholdCircularBrushDynamic'], }, { name: 'evaluate.cornerstone.segmentation.synchronizeDrawingRadius', @@ -472,64 +458,4 @@ const toolbarButtons = [ }, ]; -/** - * Default toolbar layout for the TMTV mode, registered as the - * `tmtv.toolbarSections` customization. - */ -export const toolbarSections: Record = { - [TOOLBAR_SECTIONS.primary]: ['MeasurementTools', 'Zoom', 'Pan', 'WindowLevel', 'Crosshairs'], - - [TOOLBAR_SECTIONS.viewportActionMenu.topLeft]: ['orientationMenu', 'dataOverlayMenu'], - - [TOOLBAR_SECTIONS.viewportActionMenu.bottomMiddle]: ['AdvancedRenderingControls'], - - AdvancedRenderingControls: [ - 'windowLevelMenuEmbedded', - 'voiManualControlMenu', - 'Colorbar', - 'opacityMenu', - 'thresholdMenu', - ], - - [TOOLBAR_SECTIONS.viewportActionMenu.topRight]: [ - 'modalityLoadBadge', - 'trackingStatus', - 'navigationComponent', - ], - - [TOOLBAR_SECTIONS.viewportActionMenu.bottomLeft]: ['windowLevelMenu'], - - MeasurementTools: ['Length', 'Bidirectional', 'ArrowAnnotate', 'EllipticalROI'], - - ROIThresholdToolbox: ['SegmentationTools'], - SegmentationTools: ['RectangleROIStartEndThreshold', 'BrushTools'], - BrushTools: ['Brush', 'Eraser', 'Threshold'], -}; - -/** - * Customizations registered (at default scope) by the tmtv extension. The - * TMTV mode reads these by name so `?customization=` modules can extend the - * defaults, and site modes can extend the TMTV mode with different names. - */ -const toolbarCustomization = { - 'tmtv.toolbarButtons': toolbarButtons, - 'tmtv.toolbarSections': toolbarSections, - // Extra tools layered onto the mode's tool groups after creation; entries - // reference tool blocks such as `cornerstone.annotationToolGroupTools`. - 'tmtv.toolGroupAdditions': { - [toolGroupIds.CT]: [], - [toolGroupIds.PT]: [], - [toolGroupIds.Fusion]: [], - [toolGroupIds.MIP]: [], - [toolGroupIds.default]: [], - }, - // Panel lists resolved by the TMTV mode's layout template. - 'tmtv.leftPanels': ['@ohif/extension-default.panelModule.seriesList'], - 'tmtv.rightPanels': [ - '@ohif/extension-tmtv.panelModule.tmtv', - '@ohif/extension-tmtv.panelModule.petSUV', - ], -}; - -export { toolbarButtons }; -export default toolbarCustomization; +export default toolbarButtons; diff --git a/platform/app/public/config/dev.js b/platform/app/public/config/dev.js index 051fb097f..49987d825 100644 --- a/platform/app/public/config/dev.js +++ b/platform/app/public/config/dev.js @@ -3,20 +3,17 @@ // Local development configuration. // // This is the default config for the dev server (`pnpm run dev`, `dev:fast`, -// `start`). It is intentionally kept at parity with config/netlify.js (the -// public demo deploy): every data source is enabled, the `?customization=` URL -// feature is ON via `customizationUrlPrefixes`, and the same startup -// `customizationService` modules are loaded — so the whole app, including -// customizations, can be exercised locally exactly as it runs on the demo. -// The locked-down config/default.js is what a plain production build emits -// instead. +// `start`). Like config/netlify.js it is full-featured — every data source is +// enabled and the `?customization=` URL feature is ON via +// `customizationUrlPrefixes` — so the whole app can be exercised locally. The +// locked-down config/default.js is what a plain production build emits instead. window.config = { name: 'config/dev.js', routerBasename: null, // whiteLabeling: {}, extensions: [], modes: [], - customizationService: ['@ohif/extension-default.customizationModule.theme'], + customizationService: {}, // URL-driven customizations (?customization=). The `default` prefix (no // slashes) is used for values without a leading slash; every other prefix diff --git a/platform/app/public/customizations/segmentationAnnotationTools.jsonc b/platform/app/public/customizations/segmentationAnnotationTools.jsonc deleted file mode 100644 index 458d4f607..000000000 --- a/platform/app/public/customizations/segmentationAnnotationTools.jsonc +++ /dev/null @@ -1,40 +0,0 @@ -// URL-loaded customization: segmentationAnnotationTools -// -// Enables the measurement/annotation tools inside the segmentation mode by -// composing capability blocks the cornerstone extension already exports: -// -// - the annotation buttons are already registered by the mode (its button -// list includes `cornerstone.toolbarButtons`), so the toolbar only needs a -// `MeasurementTools` section added to the primary bar, -// - the annotation tools themselves are added to the mode's tool groups via -// `cornerstone.annotationToolGroupTools`, -// - and the measurement panel is appended so annotations can be reviewed. -// -// Load it with `?customization=segmentationAnnotationTools`. -{ - "global": { - "cornerstone.segmentationModeToolbarSections": { - "primary": { "$unshift": ["MeasurementTools"] }, - "MeasurementTools": { - "$set": [ - "Length", - "Bidirectional", - "ArrowAnnotate", - "EllipticalROI", - "RectangleROI", - "CircleROI", - "PlanarFreehandROI", - "SplineROI", - "LivewireContour" - ] - } - }, - "segmentation.toolGroupAdditions": { - "default": { "$push": ["cornerstone.annotationToolGroupTools"] }, - "mpr": { "$push": ["cornerstone.annotationToolGroupTools"] } - }, - "segmentation.rightPanels": { - "$push": ["@ohif/extension-cornerstone.panelModule.panelMeasurement"] - } - } -} diff --git a/platform/app/public/customizations/segmentationEditing.jsonc b/platform/app/public/customizations/segmentationEditing.jsonc deleted file mode 100644 index 323f68521..000000000 --- a/platform/app/public/customizations/segmentationEditing.jsonc +++ /dev/null @@ -1,49 +0,0 @@ -// URL-loaded customization: segmentationEditing -// -// Adds segmentation editing to the basic and longitudinal (viewer) modes by -// composing the capability blocks the cornerstone extension already exports: -// -// - the segmentation editing toolbar buttons and toolbox section wiring -// (`cornerstone.segmentationToolbarButtons` / `cornerstone.segmentationToolbarSections`), -// - the segmentation editing tools for the modes' tool groups -// (`cornerstone.segmentationToolGroupTools`), -// - the segmentation panels that render the editing toolbox -// (`panelSegmentationWithToolsLabelMap` / `panelSegmentationWithToolsContour`), -// - and enables editing in the segmentation panel. -// -// Both modes read the `basic.*` toolbar/tool-group customizations (longitudinal -// extends the basic mode instance), so a single set of commands covers both; -// only the right-panel lists are mode specific. -// -// Load it with `?customization=segmentationEditing`. -{ - "global": { - "basic.toolbarButtons": { - "$push": ["cornerstone.segmentationToolbarButtons"] - }, - "basic.toolbarSections": { - "$push": ["cornerstone.segmentationToolbarSections"] - }, - "basic.toolGroupAdditions": { - "default": { "$push": ["cornerstone.segmentationToolGroupTools"] }, - "mpr": { "$push": ["cornerstone.segmentationToolGroupTools"] } - }, - "basic.rightPanels": { - "$set": [ - "@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsLabelMap", - "@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsContour", - "@ohif/extension-cornerstone.panelModule.panelMeasurement" - ] - }, - "longitudinal.rightPanels": { - "$set": [ - "@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsLabelMap", - "@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsContour", - "@ohif/extension-measurement-tracking.panelModule.trackedMeasurements" - ] - }, - // The basic/longitudinal modes disable segmentation panel editing only - // when no customization has expressed a preference, so this enables it. - "panelSegmentation.disableEditing": { "$set": false } - } -} diff --git a/platform/docs/docs/migration-guide/3p12-to-3p13/customization-url.md b/platform/docs/docs/migration-guide/3p12-to-3p13/customization-url.md index e9a35db98..4fe02b69e 100644 --- a/platform/docs/docs/migration-guide/3p12-to-3p13/customization-url.md +++ b/platform/docs/docs/migration-guide/3p12-to-3p13/customization-url.md @@ -179,23 +179,13 @@ or the full demo source list, you have two options: ## New `config/dev.js`; dev server no longer uses `default.js` -`config/default.js` is now **only** the default for a full production build. The -dev server gets a full-featured config instead, so `?customization=` and the -complete data-source list are available while developing without editing -`default.js`. - - **`config/dev.js`** (new) is the full-featured local-development config: every data source enabled and `?customization=` turned on. The dev-server scripts - (`pnpm run dev`, `dev:fast`, `start`) now default to `config/dev.js`. It is - kept at **parity with `config/netlify.js`** — including the startup - `customizationService` modules (e.g. the appearance/theme customization) — so - customizations behave locally exactly as they do on the public demo. -- **`config/netlify.js`** is the public demo / Netlify deploy config - (`build:viewer:ci`): the full data source set plus - `customizationUrlPrefixes: { default: './customizations/' }` and the same - `customizationService` modules. -- **`config/default.js`** is the locked-down baseline and is the default **only** - for a real production build (`pnpm run build` with no `APP_CONFIG`). + (`pnpm run dev`, `dev:fast`, `start`) now default to `config/dev.js`. +- **`config/netlify.js`** is the public demo / Netlify deploy config: the full + data source set plus `customizationUrlPrefixes: { default: './customizations/' }`. +- **`config/default.js`** remains the fallback for a real production build + (`pnpm run build` with no `APP_CONFIG`). ### `APP_CONFIG` is honored, not clobbered diff --git a/platform/docs/docs/platform/services/customization-service/specificCustomizations.md b/platform/docs/docs/platform/services/customization-service/specificCustomizations.md index 3b275b4a6..6680b54e0 100644 --- a/platform/docs/docs/platform/services/customization-service/specificCustomizations.md +++ b/platform/docs/docs/platform/services/customization-service/specificCustomizations.md @@ -207,46 +207,6 @@ tool (drag to rotate the image freely, unlike the fixed 90° *Rotate Right*): > module applies at the *global* scope, the `$push` **extends** the built-in buttons rather than > replacing them. The same pattern works for any tool already in the active tool group. -#### 4. Compose whole capability blocks into a mode - -Each shipped mode reads its toolbar, tool-group additions and panel lists through per-mode -customization keys, and the values are **lists that may reference other customizations by name**. -Extensions register both the per-mode defaults and reusable "capability blocks", so a JSON module can -extend a mode by pushing a block's *name* instead of restating its contents. - -Per-mode keys (registered by the cornerstone / measurement-tracking / tmtv extensions): - -| Mode | Buttons | Sections | Tool group additions | Panels | -| --- | --- | --- | --- | --- | -| basic & longitudinal | `basic.toolbarButtons` | `basic.toolbarSections` | `basic.toolGroupAdditions` | `basic.leftPanels` / `basic.rightPanels` (longitudinal: `longitudinal.*`) | -| segmentation | `segmentation.toolbarButtons` | `segmentation.toolbarSections` | `segmentation.toolGroupAdditions` | `segmentation.leftPanels` / `segmentation.rightPanels` | -| tmtv | `tmtv.toolbarButtons` | `tmtv.toolbarSections` | `tmtv.toolGroupAdditions` | `tmtv.leftPanels` / `tmtv.rightPanels` | - -Capability blocks exported by the cornerstone extension: - -- `cornerstone.toolbarButtons` / `cornerstone.toolbarSections` — the general viewer toolbar. -- `cornerstone.segmentationToolbarButtons` / `cornerstone.segmentationToolbarSections` — the - segmentation editing buttons and the toolbox section wiring rendered by the - `panelSegmentationWithTools*` panels. -- `cornerstone.segmentationModeToolbarSections` — the segmentation mode's main toolbar layout. -- `cornerstone.segmentationToolGroupTools` — the segmentation editing tools (brushes, scissors, - contour tools) as a `{ passive: [...] }` block for `toolGroupAdditions`. -- `cornerstone.annotationToolGroupTools` — the measurement/annotation tools as a - `{ passive: [...] }` block for `toolGroupAdditions`. - -Two shipped modules demonstrate the pattern: - -- [`segmentationEditing.jsonc`](https://github.com/OHIF/Viewers/blob/master/platform/app/public/customizations/segmentationEditing.jsonc) - (`?customization=segmentationEditing`) adds segmentation editing to the basic and longitudinal - modes: it pushes the segmentation button/section/tool blocks onto the `basic.*` keys, swaps the - right panels to the segmentation panels with tools, and enables editing via - `panelSegmentation.disableEditing`. -- [`segmentationAnnotationTools.jsonc`](https://github.com/OHIF/Viewers/blob/master/platform/app/public/customizations/segmentationAnnotationTools.jsonc) - (`?customization=segmentationAnnotationTools`) enables the annotation tools inside the - segmentation mode: it adds a `MeasurementTools` section to the primary bar, pushes - `cornerstone.annotationToolGroupTools` onto `segmentation.toolGroupAdditions`, and appends the - measurement panel. - Each payload value uses [immutability-helper](https://github.com/kolodny/immutability-helper) commands (`$set`, `$push`, `$merge`, ...) exactly like `window.config` customizations, so a module can also append to a list or merge into an existing object rather than replacing it wholesale.