feat: Add inherit from basic/longitudinal modes (#5435)
Co-authored-by: Ibrahim <ibrahim.mdev@gmail.com>
This commit is contained in:
1 parent
71d21ee184
commit
9ad0d7fc8c
28 files changed
+1153
-743
No files matched your search
@@ -71,6 +71,9 @@
|
||||
{
|
||||
"packageName": "@ohif/mode-longitudinal"
|
||||
},
|
||||
{
|
||||
"packageName": "@ohif/mode-basic"
|
||||
},
|
||||
{
|
||||
"packageName": "@ohif/mode-segmentation"
|
||||
},
|
||||
|
||||
@@ -27,6 +27,16 @@ window.config = {
|
||||
// Uses the ohif datasource as the default - this requires that KHEOPS be
|
||||
// configured with an OHIF path to .../viewer/dicomwebproxy
|
||||
defaultDataSourceName: 'ohif',
|
||||
// Show basic as 'Basic' and hide the longiutdinal mode for kheops
|
||||
modesConfiguration: {
|
||||
'@ohif/mode-basic': {
|
||||
hide: { $set: false },
|
||||
displayName: { $set: 'Basic' },
|
||||
},
|
||||
'@ohif/mode-longitudinal': {
|
||||
hide: { $set: true },
|
||||
},
|
||||
},
|
||||
/* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */
|
||||
// dangerouslyUseDynamicConfig: {
|
||||
// enabled: true,
|
||||
|
||||
@@ -391,12 +391,21 @@ function WorkList({
|
||||
})
|
||||
: appConfig.loadedModes
|
||||
).map((mode, i) => {
|
||||
if (mode.hide) {
|
||||
// Hide this mode from display
|
||||
return null;
|
||||
}
|
||||
const modalitiesToCheck = modalities.replaceAll('/', '\\');
|
||||
|
||||
const { valid: isValidMode, description: invalidModeDescription } = mode.isValidMode({
|
||||
modalities: modalitiesToCheck,
|
||||
study,
|
||||
});
|
||||
if (isValidMode === null) {
|
||||
// Hide this as a computed result.
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Modes need a default/target route? We mostly support a single one for now.
|
||||
// We should also be using the route path, but currently are not
|
||||
// mode.routeName
|
||||
@@ -428,7 +437,7 @@ function WorkList({
|
||||
{/* TODO revisit the completely rounded style of buttons used for launching a mode from the worklist later */}
|
||||
<Button
|
||||
type={ButtonEnums.type.primary}
|
||||
size={ButtonEnums.size.medium}
|
||||
size={ButtonEnums.size.smallTall}
|
||||
disabled={!isValidMode}
|
||||
startIconTooltip={
|
||||
!isValidMode ? (
|
||||
@@ -446,7 +455,7 @@ function WorkList({
|
||||
}
|
||||
onClick={() => {}}
|
||||
dataCY={`mode-${mode.routeName}-${studyInstanceUid}`}
|
||||
className={isValidMode ? 'text-[13px]' : 'bg-[#222d44] text-[13px]'}
|
||||
className={!isValidMode && 'bg-[#222d44]'}
|
||||
>
|
||||
{mode.displayName}
|
||||
</Button>
|
||||
|
||||
@@ -1,174 +1,22 @@
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { initToolGroups, toolbarButtons } from '@ohif/mode-longitudinal';
|
||||
import { id } from './id';
|
||||
import { longitudinalMode, longitudinalModeInstance, longitudinalRoute } from '@ohif/mode-longitudinal';
|
||||
|
||||
const ohif = {
|
||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||
hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default',
|
||||
leftPanel: '@ohif/extension-default.panelModule.seriesList',
|
||||
rightPanel: '@ohif/extension-cornerstone.panelModule.panelMeasurement',
|
||||
};
|
||||
|
||||
const cornerstone = {
|
||||
viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone',
|
||||
};
|
||||
|
||||
/**
|
||||
* Just two dependencies to be able to render a viewport with panels in order
|
||||
* to make sure that the mode is working.
|
||||
*/
|
||||
const extensionDependencies = {
|
||||
'@ohif/extension-default': '^3.0.0',
|
||||
'@ohif/extension-cornerstone': '^3.0.0',
|
||||
};
|
||||
|
||||
function modeFactory({ modeConfiguration }) {
|
||||
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.
|
||||
*/
|
||||
export const modeInstance = {
|
||||
...longitudinalModeInstance,
|
||||
// TODO: We're using this as a route segment
|
||||
// We should not be.
|
||||
id,
|
||||
routeName: 'template',
|
||||
/**
|
||||
* Mode name, which is displayed in the viewer's UI in the workList, for the
|
||||
* user to select the mode.
|
||||
*/
|
||||
displayName: 'Template Mode',
|
||||
/**
|
||||
* 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 } = servicesManager.services;
|
||||
|
||||
measurementService.clearMeasurements();
|
||||
|
||||
// Init Default and SR ToolGroups
|
||||
initToolGroups(extensionManager, toolGroupService, commandsManager);
|
||||
|
||||
toolbarService.register([...toolbarButtons]);
|
||||
toolbarService.updateSection('primary', [
|
||||
'MeasurementTools',
|
||||
'Zoom',
|
||||
'Pan',
|
||||
'TrackballRotate',
|
||||
'WindowLevel',
|
||||
'Capture',
|
||||
'Layout',
|
||||
'Crosshairs',
|
||||
'MoreTools',
|
||||
]);
|
||||
|
||||
toolbarService.updateSection('MeasurementTools', [
|
||||
'Length',
|
||||
'Bidirectional',
|
||||
'ArrowAnnotate',
|
||||
'EllipticalROI',
|
||||
'RectangleROI',
|
||||
'CircleROI',
|
||||
'PlanarFreehandROI',
|
||||
'SplineROI',
|
||||
'LivewireContour',
|
||||
]);
|
||||
|
||||
toolbarService.updateSection('MoreTools', [
|
||||
'Reset',
|
||||
'rotate-right',
|
||||
'flipHorizontal',
|
||||
'ImageSliceSync',
|
||||
'ReferenceLines',
|
||||
'ImageOverlayViewer',
|
||||
'StackScroll',
|
||||
'invert',
|
||||
'Probe',
|
||||
'Cine',
|
||||
'Angle',
|
||||
'CobbAngle',
|
||||
'Magnify',
|
||||
'CalibrationLine',
|
||||
'TagBrowser',
|
||||
'AdvancedMagnify',
|
||||
'UltrasoundDirectionalTool',
|
||||
'WindowLevelRegion',
|
||||
]);
|
||||
},
|
||||
onModeExit: ({ servicesManager }: withAppTypes) => {
|
||||
const {
|
||||
toolGroupService,
|
||||
syncGroupService,
|
||||
segmentationService,
|
||||
cornerstoneViewportService,
|
||||
uiDialogService,
|
||||
uiModalService,
|
||||
} = servicesManager.services;
|
||||
|
||||
uiDialogService.hideAll();
|
||||
uiModalService.hide();
|
||||
toolGroupService.destroy();
|
||||
syncGroupService.destroy();
|
||||
segmentationService.destroy();
|
||||
cornerstoneViewportService.destroy();
|
||||
},
|
||||
/** */
|
||||
validationTags: {
|
||||
study: [],
|
||||
series: [],
|
||||
},
|
||||
/**
|
||||
* A boolean return value that indicates whether the mode is valid for the
|
||||
* modalities of the selected studies. For instance a PET/CT mode should be
|
||||
*/
|
||||
isValidMode: ({ modalities }) => {
|
||||
return { valid: true };
|
||||
},
|
||||
/**
|
||||
* 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.leftPanel],
|
||||
rightPanels: [ohif.rightPanel],
|
||||
viewports: [
|
||||
{
|
||||
namespace: cornerstone.viewport,
|
||||
displaySetsToDisplay: [ohif.sopClassHandler],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
longitudinalRoute
|
||||
],
|
||||
/** List of extensions that are used by the mode */
|
||||
extensions: extensionDependencies,
|
||||
/** HangingProtocol used by the mode */
|
||||
// hangingProtocol: [''],
|
||||
/** SopClassHandlers used by the mode */
|
||||
sopClassHandlers: [ohif.sopClassHandler],
|
||||
/** hotkeys for mode */
|
||||
};
|
||||
}
|
||||
|
||||
const mode = {
|
||||
export const mode = {
|
||||
...longitudinalMode,
|
||||
id,
|
||||
modeFactory,
|
||||
extensionDependencies,
|
||||
modeInstance,
|
||||
};
|
||||
|
||||
export default mode;
|
||||
@@ -63,6 +63,8 @@ export default class ToolbarService extends PubSubService {
|
||||
},
|
||||
};
|
||||
|
||||
public static TOOLBAR_SECTIONS = TOOLBAR_SECTIONS;
|
||||
|
||||
/**
|
||||
* Access to predefined toolbar sections for autocomplete support
|
||||
*/
|
||||
|
||||
@@ -45,6 +45,7 @@ import * as MeasurementFilters from './measurementFilters';
|
||||
import getClosestOrientationFromIOP from './getClosestOrientationFromIOP';
|
||||
import calculateScanAxisNormal from './calculateScanAxisNormal';
|
||||
import areAllImageOrientationsEqual from './areAllImageOrientationsEqual';
|
||||
import { structuredCloneWithFunctions } from './structuredCloneWithFunctions';
|
||||
// Commented out unused functionality.
|
||||
// Need to implement new mechanism for derived displaySets using the displaySetManager.
|
||||
|
||||
@@ -71,6 +72,7 @@ const utils = {
|
||||
//loadAndCacheDerivedDisplaySets,
|
||||
makeDeferred,
|
||||
makeCancelable,
|
||||
structuredCloneWithFunctions,
|
||||
hotkeys,
|
||||
Queue,
|
||||
isDicomUid,
|
||||
@@ -109,6 +111,7 @@ export {
|
||||
//loadAndCacheDerivedDisplaySets,
|
||||
makeDeferred,
|
||||
makeCancelable,
|
||||
structuredCloneWithFunctions,
|
||||
hotkeys,
|
||||
Queue,
|
||||
isDicomUid,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Clones the object, incorporating functions as functions in the result.
|
||||
*/
|
||||
export function structuredCloneWithFunctions(obj, seen = new WeakMap()) {
|
||||
// Handle null, primitives, and functions
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
if (typeof obj === 'function') {
|
||||
return obj; // copy function by reference
|
||||
}
|
||||
|
||||
// Handle circular references
|
||||
if (seen.has(obj)) {
|
||||
return seen.get(obj);
|
||||
}
|
||||
|
||||
// Handle Date
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime());
|
||||
}
|
||||
|
||||
// Handle Array
|
||||
if (Array.isArray(obj)) {
|
||||
const arrCopy = [];
|
||||
seen.set(obj, arrCopy);
|
||||
for (const item of obj) {
|
||||
arrCopy.push(structuredCloneWithFunctions(item, seen));
|
||||
}
|
||||
return arrCopy;
|
||||
}
|
||||
|
||||
// Handle Object
|
||||
const copy = {};
|
||||
seen.set(obj, copy);
|
||||
for (const key of Object.keys(obj)) {
|
||||
copy[key] = structuredCloneWithFunctions(obj[key], seen);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
@@ -6,3 +6,21 @@ sidebar_label: 3.11 -> 3.12 beta
|
||||
# Migration Guide
|
||||
|
||||
This guide provides information about migrating from OHIF version 3.11 to version 3.12 beta
|
||||
|
||||
## Optional: Migrate modes to extend `modes/basic`
|
||||
|
||||
There is a lot of support for the basic mode definition contained in the
|
||||
`modes/basic` module. Using this framework will allow your mode to avoid
|
||||
creating a lot of boilerplate code that may not upgrade very well.
|
||||
|
||||
This is an OPTIONAL change - your existing mode definitions will continue to work,
|
||||
but using the new basic mode as a basis will reduce the amount of effort when
|
||||
there are changes unrelated to your custom mode.
|
||||
|
||||
|
||||
## ui button with text size
|
||||
|
||||
Using the class text size with the ui-button is inconsistent as to whether
|
||||
it will apply or not. Instead, create a new size value to assign the desired size.
|
||||
To support this, a new size enum is created, smallTall, which is used in the worklist
|
||||
for an over-ride.
|
||||
@@ -52,9 +52,12 @@ The mode configuration specifies which `extensions` the mode requires, which
|
||||
template this defines which `side panels` will be available, as well as what
|
||||
`viewports` and which `displaySets` they may hang.
|
||||
|
||||
Mode's config is composed of three elements:
|
||||
Mode's config is composed of these elements:
|
||||
- `id`: the mode `id`
|
||||
- `modeFactory`: the function that returns the mode specific configuration
|
||||
- `modeInstance`: An optional configuration used by the default modeFactory
|
||||
exported by the basic mode. This allows specifying or updating the
|
||||
default mode values.
|
||||
- `extensionDependencies`: the list of extensions that the mode requires
|
||||
|
||||
|
||||
@@ -62,10 +65,18 @@ that return a config object with certain
|
||||
properties, the high-level view of this config object is:
|
||||
|
||||
```js title="modes/example/src/index.js"
|
||||
function modeFactory() {
|
||||
return {
|
||||
function modeFactory({modeConfiguration}) {
|
||||
return { ...this.modeInstance, ...modeConfiguration };
|
||||
|
||||
}
|
||||
|
||||
const mode = {
|
||||
id,
|
||||
modeFactory,
|
||||
modeInstance: {
|
||||
id: '',
|
||||
version: '',
|
||||
hide: true,
|
||||
displayName: '',
|
||||
onModeEnter: () => {},
|
||||
onModeExit: () => {},
|
||||
@@ -75,19 +86,27 @@ function modeFactory() {
|
||||
{
|
||||
path: '',
|
||||
init: () => {},
|
||||
layoutTemplate: () => {},
|
||||
layoutInstance: {
|
||||
id,
|
||||
props: {
|
||||
leftPanels: [],
|
||||
leftPanelResizable: true,
|
||||
rightPanels: [],
|
||||
rightPanelClosed: true,
|
||||
rightPanelResizable: true,
|
||||
viewports: [],
|
||||
},
|
||||
},
|
||||
layoutTemplate: function() { return this.layoutInstance },
|
||||
},
|
||||
],
|
||||
extensions: extensionDependencies,
|
||||
hangingProtocol: [],
|
||||
sopClassHandlers: [],
|
||||
hotkeys: []
|
||||
};
|
||||
}
|
||||
|
||||
const mode = {
|
||||
id,
|
||||
modeFactory,
|
||||
hotkeys: [],
|
||||
nonModeModalities: [],
|
||||
modeModalities: [],
|
||||
},
|
||||
extensionDependencies,
|
||||
};
|
||||
|
||||
@@ -108,6 +127,12 @@ export default mode;
|
||||
</td>
|
||||
<td align="left">unique mode id used to refer to the mode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
hide
|
||||
</td>
|
||||
<td align="left">Set to true to hide this mode on the worklist, but allow it in the path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
displayName
|
||||
@@ -168,7 +193,7 @@ export default mode;
|
||||
hanging protocol
|
||||
</a>
|
||||
</td>
|
||||
<td align="left">list of hanging protocols that the mode should have access to</td>
|
||||
<td align="left">list of hanging protocols that the mode applies initially, choosing the highest scoring match</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
@@ -186,10 +211,44 @@ export default mode;
|
||||
</td>
|
||||
<td align="left">hotkeys</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
modeModalities
|
||||
</td>
|
||||
<td align="left">If non-empty, then the default isValidMode will only return true when the modalities list has all of the elements of one of the mode modalities. Eg `[` [CT,PT], [MR,PT] ]` would mean that the mode supports a CT AND a PT, OR an MR and a PT</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
nonModeModalities
|
||||
</td>
|
||||
<td align="left">Enable the mode if the modalities list contains a modality OTHER than those in the array</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
enableSegmentationEdit
|
||||
</td>
|
||||
<td align="left">Boolean to skip the segmentation edit capabilities</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
toolbarSections
|
||||
</td>
|
||||
<td align="left">An object containing toolbar section definitions to register</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Extending Modes
|
||||
|
||||
The `basic` mode provides support for creating mode extensions without having
|
||||
to redeclare the entire mode. See `longitudinal/src/index.ts` for an example
|
||||
mode that builds on top of the basic mode. Also see `basic/src/index.tsx` for
|
||||
some default functions which can be used to create your own modes. Doing a mode
|
||||
this way makes the definition of new modes based on your existing mode much easier,
|
||||
and the upgrade to new versions of modes tends to be more consistent.
|
||||
|
||||
### Consuming Extensions
|
||||
|
||||
As mentioned in the [Extensions](../extensions/index.md) section, in `OHIF-v3`
|
||||
@@ -345,7 +404,6 @@ const myHotkeys = [
|
||||
|
||||
function modeFactory() {
|
||||
return {
|
||||
id: '',
|
||||
id: '',
|
||||
displayName: '',
|
||||
/*
|
||||
@@ -413,3 +471,50 @@ rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.m
|
||||
This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other.
|
||||
|
||||
:::
|
||||
|
||||
## APP Configuration of Modes
|
||||
|
||||
Modes based on the `basic` mode allow for customization using the `immutability-helper`
|
||||
api within the `app-config.js` file as specified by the build process. For example,
|
||||
to list the `basic` mode by default, and hide the `longitudinal` mode, the following
|
||||
configuration from `config/kheops.js` can be used:
|
||||
|
||||
```
|
||||
... app config file
|
||||
modesConfiguration: {
|
||||
'@ohif/mode-basic': {
|
||||
hide: { $set: false },
|
||||
displayName: { $set: 'Basic' },
|
||||
},
|
||||
'@ohif/mode-longitudinal': {
|
||||
hide: { $set: true },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
## Default Modes
|
||||
|
||||
There are a number of modes provided in a default OHIF installation. These
|
||||
are described here, along with some amount of information about extending/configuration
|
||||
of those modes.
|
||||
|
||||
Modes which are loaded by default, but which are hidden can be activated by
|
||||
using a direct URL launch. For example, to show a study in the `basic` mode,
|
||||
use the URL for the `longitudinal` mode, and replace the `/viewer` with `/basic`
|
||||
|
||||
### Basic (NOT `Basic Viewer`, which got assigned to `longitudinal`)
|
||||
|
||||
The basic mode is a mode that demonstrates the base capabilities of the OHIF
|
||||
system, without including features such as longitudinal tracking, segmentation editing
|
||||
or other custom capabilities. The left hand panel uses the study browser thumbnails
|
||||
without tracking, and the right hand panel uses the basic segmentation panel and the
|
||||
measurements without tracking (longitudinal) layouts. This makes it a good overall
|
||||
base for using when the tracking behaviour of longitudinal mode is not desired.
|
||||
|
||||
It can be used in a default install by direct URL launch to the `/basic` endpoint
|
||||
instead of the `/viewer` endpoint.
|
||||
|
||||
### Longitudinal (The `Basic Viewer` label in OHIF)
|
||||
|
||||
The longitudinal mode adds the tracking for measurements in the study browser
|
||||
and in the measurements panel, and is otherwise identical to the `basic` mode.
|
||||
@@ -11,6 +11,8 @@ summary: Documentation for OHIF Mode validity checks, which determine when speci
|
||||
There are two mechanism for checking the validity of a mode for a study.
|
||||
|
||||
- `isValidMode`: which is called on a selected study in the workList.
|
||||
- The basic mode exports an `isValidMode` function which selects
|
||||
validity based on the modalities in the study.
|
||||
- `validTags`
|
||||
|
||||
|
||||
@@ -22,15 +24,16 @@ validity of the mode based on `StudyInstanceUID` and `modalities` that are in th
|
||||
For instance, for pet-ct mode, both `PT` and 'CT' modalities should be available inside the study.
|
||||
|
||||
```js
|
||||
import { isValidMode } from '@ohif/mode-basic';
|
||||
|
||||
function modeFactory() {
|
||||
return {
|
||||
id: '',
|
||||
displayName: '',
|
||||
isValidMode: ({ modalities, StudyInstanceUID }) => {
|
||||
const modalities_list = modalities.split('\\');
|
||||
const validMode = ['CT', 'PT'].every(modality => modalities_list.includes(modality));
|
||||
return validMode;
|
||||
},
|
||||
// Select either `CT & PT | MR & PT`
|
||||
modeModalities: [ ['CT', 'PT'], ['MR', 'PT'] ],
|
||||
// Just re-use the existing function
|
||||
isValidMode,
|
||||
/*
|
||||
...
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import Tooltip from '../Tooltip/Tooltip';
|
||||
|
||||
const sizeClasses = {
|
||||
[ButtonEnums.size.small]: 'h-[26px] text-[13px]',
|
||||
[ButtonEnums.size.smallTall]: 'h-[32px] text-[13px]',
|
||||
[ButtonEnums.size.medium]: 'h-[32px] text-[14px]',
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ enum type {
|
||||
}
|
||||
enum size {
|
||||
medium = 'medium',
|
||||
smallTall = 'smallTall',
|
||||
small = 'small',
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user