feat: Add extensibility for tmtv and segmentation modes (#6128)
* feat: Add extensibility for tmtv and segmentation modes * Fixes for ordering issues on laod * Remove unnecessary reference lookup * Chane side panel timing to fix tests * PR comments - change how mode definitions get created * Improvements to mode customizations * Start organizing customizations * Misc fixes for a customization demo page * Security fixes * PR requested changes to naming
This commit is contained in:
1 parent
f79055f98e
commit
b266c0a86a
49 files changed
+2653
-1215
No files matched your search
@@ -26,7 +26,7 @@ allowlist.
|
||||
window.config = {
|
||||
customizationUrlPrefixes: {
|
||||
// The `default` prefix (no slashes) handles values with no leading slash.
|
||||
default: './customizations/', // ?customization=ctPresets -> ./customizations/ctPresets.jsonc
|
||||
default: './customizations/', // ?customization=tools/ctPresets -> ./customizations/tools/ctPresets.jsonc
|
||||
// Every other prefix MUST start and end with a slash and is matched against
|
||||
// the leading `/segment/` of the value.
|
||||
'/remote/': 'https://cdn.example.com/ohif-customizations/', // ?customization=/remote/siteA
|
||||
@@ -36,7 +36,7 @@ window.config = {
|
||||
|
||||
Resolution rules:
|
||||
|
||||
- `?customization=ctPresets` → `default` prefix → `./customizations/ctPresets.jsonc`
|
||||
- `?customization=tools/ctPresets` → `default` prefix → `./customizations/tools/ctPresets.jsonc`
|
||||
- `?customization=/remote/siteA` → `/remote/` prefix → `https://cdn.example.com/ohif-customizations/siteA.jsonc`
|
||||
- **A value whose prefix is not configured throws and aborts startup** rather than
|
||||
being silently ignored. With no `customizationUrlPrefixes` configured, *any*
|
||||
@@ -106,7 +106,7 @@ window.config = {
|
||||
// in the global config, which is not itself updatable by any customization.
|
||||
customizationUrlPrefixes: { default: './customizations/' },
|
||||
customizationService: {
|
||||
requires: ['patientBirthDate'], // resolves ./customizations/patientBirthDate.jsonc
|
||||
requires: ['worklist/patientBirthDate'], // resolves ./customizations/worklist/patientBirthDate.jsonc
|
||||
global: [ // mixes string references and inline maps
|
||||
'@ohif/extension-default.customizationModule.datasources',
|
||||
{ 'workList.variant': 'default' },
|
||||
@@ -179,13 +179,23 @@ 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`.
|
||||
- **`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`).
|
||||
(`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`).
|
||||
|
||||
### `APP_CONFIG` is honored, not clobbered
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
---
|
||||
sidebar_position: 11
|
||||
sidebar_label: Mode extensibility
|
||||
title: 'Mode lifecycle regularization (basic / longitudinal / segmentation / tmtv)'
|
||||
---
|
||||
|
||||
# Mode lifecycle regularization
|
||||
|
||||
3.13 makes the `segmentation`, `tmtv` and `basic-test` modes extend the `basic`
|
||||
mode's shared lifecycle (as `longitudinal` already did), and in the process
|
||||
regularizes a few mode-instance properties that 3.12 introduced. If you built a
|
||||
mode on top of `@ohif/mode-basic` (or spread one of the shipped
|
||||
`modeInstance` objects), review the changes below.
|
||||
|
||||
## Lifecycle ordering
|
||||
|
||||
Mode-related customizations follow one deterministic sequence; the final value
|
||||
of every key is decided by scope precedence (global > mode > default) plus
|
||||
application order, with no special cases:
|
||||
|
||||
1. app-config `requires` and `?customization=` chains are resolved up front;
|
||||
2. mode modules load and register the `customizations` maps they carry
|
||||
(Default scope);
|
||||
3. the `bootstrap` phase applies (Global scope) — it can modify what the modes
|
||||
registered;
|
||||
4. extensions register; extension defaults merge; the `global` phase applies;
|
||||
5. mode instances are created (`modeFactory`) — after bootstrap/global, so they
|
||||
see modifications;
|
||||
6. on mode enter, the mode scope is reset, then layered bottom-up: the mode's
|
||||
layout panel lists (seeded as `leftPanels` / `rightPanels`) and its
|
||||
toolbar/tool-group composition (seeded as the plain `toolbarButtons` /
|
||||
`toolbarSections` / `toolGroupAdditions`), the mode's `modeCustomizations`
|
||||
block, the `mode` phase `*` block, and the mode-specific block;
|
||||
7. only then do the sidebars, toolbar, and `onModeEnter` consume the values.
|
||||
|
||||
## `initToolGroups` takes an options object
|
||||
|
||||
All modes now share one tool group setup signature, so an extending mode (or a
|
||||
`modeConfiguration`) can substitute any other mode's implementation via the
|
||||
`initToolGroups` instance property:
|
||||
|
||||
```js
|
||||
// Before (3.12) — basic/segmentation form:
|
||||
function initToolGroups(extensionManager, toolGroupService, commandsManager) { ... }
|
||||
// Before (3.12) — tmtv form:
|
||||
function initToolGroups(toolNames, Enums, toolGroupService, commandsManager) { ... }
|
||||
|
||||
// After (3.13) — every mode:
|
||||
function initToolGroups({ extensionManager, toolGroupService, commandsManager, servicesManager }) {
|
||||
// resolve toolNames/Enums yourself when needed:
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.tools'
|
||||
);
|
||||
const { toolNames, Enums } = utilityModule.exports;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
This only affects functions passed as the `initToolGroups` mode-instance
|
||||
property (invoked by the shared `onModeEnter`); a self-contained mode that
|
||||
calls its own function inside its own `onModeEnter` is unaffected.
|
||||
|
||||
## `enableSegmentationEdit` is replaced by `modeCustomizations`
|
||||
|
||||
Mode-scoped customizations are now declared as data instead of one-off boolean
|
||||
capabilities, and the final value of every key is decided by the customization
|
||||
service's normal resolution alone — scope precedence (global > mode > default)
|
||||
plus application order within the mode scope — with no special-case logic.
|
||||
|
||||
On mode enter the mode route layers the mode scope bottom-up:
|
||||
|
||||
1. the mode's `modeCustomizations` block, applied right after the mode scope is
|
||||
reset (e.g. `basicModeCustomizations` seeds `panelSegmentation.disableEditing: true`);
|
||||
2. the app config / URL `mode` phase blocks (the general `*` block, then the
|
||||
mode-specific block) — e.g. `?customization=segmentation/segmentationEditing` sets
|
||||
`panelSegmentation.disableEditing: false` in its `mode.basic` / `mode.viewer`
|
||||
blocks, which wins over step 1 by application order.
|
||||
|
||||
A `global`-scope customization still overrides the whole mode scope by scope
|
||||
precedence when a value genuinely needs to apply to every mode.
|
||||
|
||||
The block itself is registered with the customization service at default scope
|
||||
by the **mode** when it loads — modes carry a `customizations` map on their
|
||||
definition, registered during app init *before* the bootstrap phase applies —
|
||||
and the mode instance references it by name, so bootstrap and `?customization=`
|
||||
modules can modify the block before it is ever applied:
|
||||
|
||||
```js
|
||||
// Before (3.12)
|
||||
export const modeInstance = {
|
||||
enableSegmentationEdit: false,
|
||||
};
|
||||
|
||||
// After (3.13) — the mode registers the block when it loads (plain
|
||||
// key -> value data; registered customization values never carry `$`
|
||||
// commands — commands are how later customizations modify them):
|
||||
export const customizations = {
|
||||
basicModeCustomizations: {
|
||||
'panelSegmentation.disableEditing': true,
|
||||
},
|
||||
};
|
||||
|
||||
export const mode = {
|
||||
id,
|
||||
modeFactory,
|
||||
modeInstance,
|
||||
extensionDependencies,
|
||||
customizations,
|
||||
};
|
||||
|
||||
// and the mode instance references it:
|
||||
export const modeInstance = {
|
||||
modeCustomizations: 'basicModeCustomizations',
|
||||
};
|
||||
```
|
||||
|
||||
`modeCustomizations` may also be a literal value on the mode instance: an
|
||||
object whose entries are plain values or immutability-helper commands (a
|
||||
command merges with the value registered at default scope — e.g. the
|
||||
`basic-test` mode `$push`es an extra hotkey onto `ohif.hotkeyBindings`), or an
|
||||
array mixing customization module reference strings with such objects.
|
||||
|
||||
## `activatePanelTrigger` is replaced by data-driven `activatePanelTriggers`
|
||||
|
||||
The 3.12 `activatePanelTrigger` boolean (which hardcoded the cornerstone panel
|
||||
ids) is replaced by an `activatePanelTriggers` list the shared `onModeEnter`
|
||||
wires up. Entries are JSON-serializable — event names are looked up in the
|
||||
source service's `EVENTS` map — so a customization or an extending mode can
|
||||
point at its own panels:
|
||||
|
||||
```js
|
||||
export const modeInstance = {
|
||||
activatePanelTriggers: [
|
||||
{
|
||||
panelId: '@ohif/extension-cornerstone.panelModule.panelSegmentation',
|
||||
sourceServiceName: 'segmentationService',
|
||||
sourceEvents: ['SEGMENTATION_ADDED'],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
It is empty by default (matching 3.12 behavior, where nothing set the boolean).
|
||||
The basic mode exports `defaultActivatePanelTriggers` with the historical
|
||||
segmentation/measurement panel triggers.
|
||||
|
||||
Relatedly, subscriptions created during `onModeEnter` are now tracked as
|
||||
unsubscribe **functions** in a single `this._unsubscriptions` array
|
||||
(initialized by the shared `onModeEnter`, cleaned by the shared `onModeExit`).
|
||||
The `_activatePanelTriggersSubscriptions` array of subscription objects is
|
||||
gone; extending modes push plain functions instead:
|
||||
|
||||
```js
|
||||
export function onModeEnter(ctx) {
|
||||
basicOnModeEnter.call(this, ctx);
|
||||
const { unsubscribe } = someService.subscribe(...);
|
||||
this._unsubscriptions.push(unsubscribe);
|
||||
}
|
||||
// No custom onModeExit needed — the shared one cleans up.
|
||||
```
|
||||
|
||||
## Data-driven `isValidMode`
|
||||
|
||||
The shared `isValidMode` gained two properties alongside `modeModalities` /
|
||||
`nonModeModalities`, letting modes declare validity without custom code:
|
||||
|
||||
- `excludedModalities`: the study is invalid when it contains **any** of these
|
||||
(e.g. tmtv rejects `SM`).
|
||||
- `excludedStudies`: a list of `{ attribute: value }` objects; a study matching
|
||||
every attribute of an entry is invalid (e.g. `[{ mrn: 'M1' }]`).
|
||||
|
||||
Also note a 3.12 bug fix: `modeModalities` matching previously used a bare
|
||||
`indexOf` truthiness check, which inverted matches in some cases; it now
|
||||
correctly tests list membership.
|
||||
|
||||
## tmtv now clears measurements on enter
|
||||
|
||||
Because tmtv shares the basic mode's `onModeEnter`, it now calls
|
||||
`measurementService.clearMeasurements()` on mode entry like every other mode.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
sidebar_position: 10
|
||||
sidebar_label: Mode panel lists & customization
|
||||
title: 'Mode panel lists are standard customizations'
|
||||
---
|
||||
|
||||
# Mode panel lists are standard customizations
|
||||
|
||||
3.13 lets a mode's sidebars be modified at runtime through the customization
|
||||
service — for **every** mode, with nothing to opt into. Modes declare their
|
||||
panels the standard way, as literal arrays in the layout:
|
||||
|
||||
```ts
|
||||
props: {
|
||||
leftPanels: ['@ohif/extension-default.panelModule.seriesList'],
|
||||
rightPanels: ['@ohif/extension-cornerstone.panelModule.panelMeasurement'],
|
||||
}
|
||||
```
|
||||
|
||||
On mode enter the mode route layers the mode scope bottom-up and only then
|
||||
resolves the sidebars:
|
||||
|
||||
1. the mode scope is reset;
|
||||
2. the layout's panel arrays are seeded as the standard `leftPanels` /
|
||||
`rightPanels` customizations (the bottom layer of the mode scope);
|
||||
3. the app config / URL `mode` phase blocks apply — the general `*` block, then
|
||||
the block keyed by the entered mode's id / route name;
|
||||
4. the sidebars resolve from the final `leftPanels` / `rightPanels`
|
||||
values (global-scope customizations, as always, win by scope precedence).
|
||||
|
||||
## Customizing a mode's panels
|
||||
|
||||
Because the mode's own list is already in the customization service when the
|
||||
phase blocks apply, a `?customization=` module (or `window.config`
|
||||
customization) targets the standard keys in a `mode` phase block — and
|
||||
immutability-helper commands compose with the mode's own list:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"mode": {
|
||||
// Replace the right sidebar in the longitudinal mode (route name `viewer`)
|
||||
"viewer": {
|
||||
"rightPanels": {
|
||||
"$set": [
|
||||
"@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsLabelMap",
|
||||
"@ohif/extension-measurement-tracking.panelModule.trackedMeasurements"
|
||||
]
|
||||
}
|
||||
},
|
||||
// Append a panel in the segmentation mode
|
||||
"segmentation": {
|
||||
"rightPanels": {
|
||||
"$push": ["@ohif/extension-cornerstone.panelModule.panelMeasurement"]
|
||||
}
|
||||
},
|
||||
// Or change every mode at once with the general block
|
||||
"*": {
|
||||
"leftPanels": { "$push": ["@ohif/extension-example.panelModule.myPanel"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See the shipped
|
||||
[`segmentation/segmentationEditing.jsonc`](https://github.com/OHIF/Viewers/blob/master/platform/app/public/customizations/segmentation/segmentationEditing.jsonc)
|
||||
and
|
||||
[`segmentation/segmentationAnnotationTools.jsonc`](https://github.com/OHIF/Viewers/blob/master/platform/app/public/customizations/segmentation/segmentationAnnotationTools.jsonc)
|
||||
modules for complete worked examples.
|
||||
|
||||
## Migration notes
|
||||
|
||||
- **Existing modes need no changes.** Literal panel arrays are the standard
|
||||
form and are now also the customizable form.
|
||||
- **The per-mode panel-list names from early 3.13 betas are gone.** If you
|
||||
wrote a customization against `basic.leftPanels`, `longitudinal.rightPanels`,
|
||||
`segmentation.rightPanels`, or `tmtv.leftPanels`, move it to the standard
|
||||
`leftPanels` / `rightPanels` keys inside a `mode` phase block keyed
|
||||
by the mode's route name (see above).
|
||||
@@ -225,15 +225,51 @@ export default mode;
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
enableSegmentationEdit
|
||||
excludedModalities
|
||||
</td>
|
||||
<td align="left">Boolean to skip the segmentation edit capabilities</td>
|
||||
<td align="left">The default isValidMode returns false when the modalities list contains ANY of these</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
excludedStudies
|
||||
</td>
|
||||
<td align="left">A list of study attribute objects; the default isValidMode returns false for a study matching every attribute of any entry, e.g. <code>[{ mrn: 'M1' }]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
toolbarSections
|
||||
</td>
|
||||
<td align="left">An object containing toolbar section definitions to register</td>
|
||||
<td align="left">Toolbar section composition: a list of section-layout packs (as <code>$reference</code> markers) and/or literal section objects; seeded onto the Mode scope on enter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
toolbarButtons
|
||||
</td>
|
||||
<td align="left">Toolbar button composition: a list of button packs (as <code>$reference</code> markers) and/or literal button definitions; seeded onto the Mode scope on enter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
toolGroupAdditions
|
||||
</td>
|
||||
<td align="left">Per-tool-group composition: a map of tool-group id to a list of tool packs (as <code>$reference</code> markers) and/or literal tool blocks, layered onto the mode's tool groups after creation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
initToolGroups
|
||||
</td>
|
||||
<td align="left">Tool group setup function called by the shared onModeEnter as <code>initToolGroups({ extensionManager, toolGroupService, commandsManager, servicesManager })</code>; extending modes can substitute their own</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
modeCustomizations
|
||||
</td>
|
||||
<td align="left">The mode's own customizations, applied by the mode route as the bottom layer of the mode scope on enter — before the app config / URL <code>mode</code> phase blocks, and below global-scope customizations, so final values are decided purely by scope precedence and application order. Usually the name of a block the extension registers at default scope (e.g. <code>basicModeCustomizations</code>, which sets <code>panelSegmentation.disableEditing</code>); may also be a literal object of immutability-helper commands or an array mixing those with customization module reference strings</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left">
|
||||
activatePanelTriggers
|
||||
</td>
|
||||
<td align="left">Data-driven ActivatePanel event triggers: a list of <code>{ panelId, sourceServiceName, sourceEvents, forceActive? }</code> entries wired up on mode enter (e.g. activating the segmentation panel when a segmentation is added). Empty by default; see <code>defaultActivatePanelTriggers</code> in the basic mode</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -249,6 +285,73 @@ 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.
|
||||
|
||||
The **`segmentation`** and **`tmtv`** modes now follow this same pattern. Like
|
||||
`longitudinal`, each one exports a `modeInstance` object and reuses the `basic`
|
||||
mode's `modeFactory`, so they are extensible in two complementary ways:
|
||||
|
||||
1. **Build a derived mode** — create a new mode package (for example
|
||||
`mySegmentation`) that imports the shipped mode and overrides only the parts
|
||||
you need, exactly like `longitudinal` builds on `basic`.
|
||||
2. **Customize an existing mode at runtime** — change a shipped mode's toolbar,
|
||||
tools, or panels through per-mode customization keys, with no new package.
|
||||
|
||||
#### Building a derived mode
|
||||
|
||||
A derived mode imports the shipped mode's default export (which carries the
|
||||
`modeFactory`) and its `modeInstance`, then spreads and overrides. Because the
|
||||
default `modeFactory` applies [immutability-helper][immutability-helper]
|
||||
commands from `modeConfiguration` onto `modeInstance`, you can also override
|
||||
via `modeConfiguration` rather than editing the instance directly.
|
||||
|
||||
```js title="modes/my-segmentation/src/index.tsx"
|
||||
import segmentationMode, { modeInstance as segModeInstance } from '@ohif/mode-segmentation';
|
||||
|
||||
const id = 'mySegmentation';
|
||||
|
||||
export const modeInstance = {
|
||||
...segModeInstance,
|
||||
id,
|
||||
routeName: 'mySegmentation',
|
||||
displayName: 'My Segmentation',
|
||||
// Override only what you need. Toolbar buttons/sections and tool group
|
||||
// additions are plain composition arrays on the instance naming the
|
||||
// capability packs the mode uses (see below); panel lists are literal
|
||||
// arrays in the layout. Both are customized at runtime via the `mode` phase.
|
||||
};
|
||||
|
||||
const mode = {
|
||||
...segmentationMode, // carries modeFactory + extensionDependencies
|
||||
id,
|
||||
modeInstance,
|
||||
};
|
||||
|
||||
export default mode;
|
||||
```
|
||||
|
||||
The `tmtv` mode is extended the same way — import `@ohif/mode-tmtv` and its
|
||||
`modeInstance`, then override.
|
||||
|
||||
#### Customizing a mode at runtime
|
||||
|
||||
The `basic`, `longitudinal`, `segmentation`, and `tmtv` modes declare their toolbar
|
||||
buttons, toolbar sections, and tool-group additions as plain composition arrays on
|
||||
the mode instance that name the capability packs the mode uses with `{ $reference }`
|
||||
markers (for example
|
||||
`toolbarButtons: [{ $reference: 'cornerstone.toolbarButtons' }, { $reference: 'cornerstone.segmentationToolbarButtons' }]`).
|
||||
The mode route seeds these onto the Mode customization scope on enter, alongside the
|
||||
`leftPanels` / `rightPanels` panel lists. Because the values are lists whose
|
||||
`{ $reference }` entries the customization service expands at read time, a `window.config`
|
||||
entry or a `?customization=` JSON module can add a whole capability pack (such as the
|
||||
segmentation editing tools), remove a default, or swap the panels — targeting the mode
|
||||
through a `mode` phase block (`mode.basic`, `mode.segmentation`, ...) without building
|
||||
a new mode. See [Compose whole capability blocks into a mode][compose-capability-blocks] in the
|
||||
Customization Service docs for the full key table, the reusable capability
|
||||
blocks, and worked examples (adding segmentation editing to the basic and
|
||||
longitudinal modes, and enabling annotation tools inside the segmentation mode).
|
||||
|
||||
[immutability-helper]: https://github.com/kolodny/immutability-helper
|
||||
[compose-capability-blocks]: ../services/customization-service/specificCustomizations.md#4-compose-whole-capability-blocks-into-a-mode
|
||||
|
||||
### Consuming Extensions
|
||||
|
||||
As mentioned in the [Extensions](../extensions/index.md) section, in `OHIF-v3`
|
||||
|
||||
+97
-7
@@ -107,10 +107,13 @@ A URL-loaded file applies its `global` payload as **global customizations** —
|
||||
`window.config`'s `customizationService` entries, but loaded at runtime from `?customization=`. Any
|
||||
customization key that is read through `customizationService.getCustomization(...)` can therefore be
|
||||
set this way. The examples below are complete files; drop one under `platform/app/public/customizations/`
|
||||
(the `default` prefix) and load it with `?customization=<fileName>`. Because the files are JSONC, you
|
||||
can keep `//` comments and trailing commas in them.
|
||||
(the `default` prefix) and load it with `?customization=<path>`. The shipped examples are grouped into
|
||||
subfolders by area (e.g. `tools/`, `worklist/`, `segmentation/`, `veterinary/`), and a nested path is
|
||||
just part of the name under the `default` prefix — `?customization=tools/ctPresets` resolves to
|
||||
`./customizations/tools/ctPresets.jsonc`. Because the files are JSONC, you can keep `//` comments
|
||||
and trailing commas in them.
|
||||
|
||||
The shipped [`veterinaryOverlay.jsonc`](https://github.com/OHIF/Viewers/blob/master/platform/app/public/customizations/veterinaryOverlay.jsonc)
|
||||
The shipped [`veterinary/veterinaryOverlay.jsonc`](https://github.com/OHIF/Viewers/blob/master/platform/app/public/customizations/veterinary/veterinaryOverlay.jsonc)
|
||||
demonstrates a fourth scenario — replacing the viewport overlay layout via `viewportOverlay.topLeft` /
|
||||
`viewportOverlay.topRight`.
|
||||
|
||||
@@ -120,7 +123,7 @@ Override the CT presets offered in the window-level menu (key: `cornerstone.wind
|
||||
`$merge` replaces only the `CT` entry, so presets for other modalities (PT, etc.) are kept.
|
||||
|
||||
```jsonc
|
||||
// platform/app/public/customizations/ctPresets.jsonc -> ?customization=ctPresets
|
||||
// platform/app/public/customizations/tools/ctPresets.jsonc -> ?customization=tools/ctPresets
|
||||
{
|
||||
"global": {
|
||||
"cornerstone.windowLevelPresets": {
|
||||
@@ -143,7 +146,7 @@ Make the viewer prompt for a label from a fixed list whenever a measurement is c
|
||||
(key: `measurementLabels`).
|
||||
|
||||
```jsonc
|
||||
// platform/app/public/customizations/measurementLabels.jsonc -> ?customization=measurementLabels
|
||||
// platform/app/public/customizations/tools/measurementLabels.jsonc -> ?customization=tools/measurementLabels
|
||||
{
|
||||
"global": {
|
||||
"measurementLabels": {
|
||||
@@ -169,12 +172,12 @@ The basic and longitudinal viewers register their toolbar as customizations
|
||||
layout that maps each section to a list of button ids). A module can therefore add a button by
|
||||
`$push`-ing a definition onto `cornerstone.toolbarButtons` and the button's id onto a section.
|
||||
|
||||
The shipped [`smoothRotate.jsonc`](https://github.com/OHIF/Viewers/blob/master/platform/app/public/customizations/smoothRotate.jsonc)
|
||||
The shipped [`tools/smoothRotate.jsonc`](https://github.com/OHIF/Viewers/blob/master/platform/app/public/customizations/tools/smoothRotate.jsonc)
|
||||
adds a **Smooth Rotate** button to the *More Tools* menu that activates the cornerstone `PlanarRotate`
|
||||
tool (drag to rotate the image freely, unlike the fixed 90° *Rotate Right*):
|
||||
|
||||
```jsonc
|
||||
// platform/app/public/customizations/smoothRotate.jsonc -> ?customization=smoothRotate
|
||||
// platform/app/public/customizations/tools/smoothRotate.jsonc -> ?customization=tools/smoothRotate
|
||||
{
|
||||
"global": {
|
||||
"cornerstone.toolbarButtons": {
|
||||
@@ -207,10 +210,97 @@ 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
|
||||
|
||||
Ownership is split into three layers. **Extensions** export reusable *capability packs* — button
|
||||
definitions, section layouts and tool lists — under their own namespace (`cornerstone.*`,
|
||||
`tmtv.*`); the packs carry no mode identity. **Modes** own *composition*: each mode declares which
|
||||
packs it uses as plain arrays on its instance (`toolbarButtons`, `toolbarSections`,
|
||||
`toolGroupAdditions`), naming each pack with a `{ $reference: '<name>' }` marker, and the mode route
|
||||
seeds those onto the **Mode** customization scope on enter — exactly like it seeds
|
||||
`leftPanels` / `rightPanels` from the layout. **Config** (`?customization=`) re-composes an
|
||||
existing mode through the `mode` phase.
|
||||
|
||||
Because composition is per-mode, a JSON module targets a mode with a `mode` phase block keyed by the
|
||||
mode's id or route name and refines the plain concept keys — pushing a `{ $reference }` to a pack
|
||||
instead of restating its contents:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"mode": {
|
||||
"basic": {
|
||||
"toolbarButtons": { "$push": [{ "$reference": "cornerstone.segmentationToolbarButtons" }] },
|
||||
"toolGroupAdditions": {
|
||||
"default": { "$push": [{ "$reference": "cornerstone.segmentationTools" }] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
There are no `basic.*` / `segmentation.*` / `tmtv.*` keys — `mode.basic` / `mode.viewer` /
|
||||
`mode.segmentation` already select the mode, and the key is the concept (`toolbarButtons`), the same
|
||||
way `rightPanels` works for the sidebars. Reserve the `global` phase for values that truly apply
|
||||
to every mode.
|
||||
|
||||
**`$reference` — composing customizations by name.** A `{ "$reference": "<name>" }` object resolves,
|
||||
when the value is *read*, to the value of the customization `<name>`. References may sit anywhere in a
|
||||
value; inside an array a reference to another array is *flattened* in, so a list composes several
|
||||
packs by name. Because resolution is at read time (not when customizations merge), a later command
|
||||
composes naturally: `$push` adds another `{ $reference }`, and `$set` replaces the whole value —
|
||||
with a different `{ $reference }` **or** a hard-coded list:
|
||||
|
||||
```jsonc
|
||||
{ "mode": { "basic": {
|
||||
// swap the entire toolbar for a different pack …
|
||||
"toolbarButtons": { "$set": [{ "$reference": "myExtension.myToolbarButtons" }] }
|
||||
// … or for a hard-coded list of button definitions
|
||||
// "toolbarButtons": { "$set": [ { "id": "Length", /* … */ } ] }
|
||||
} } }
|
||||
```
|
||||
|
||||
Edits to the referenced pack itself are picked up live, and reference cycles are detected and warned.
|
||||
|
||||
Capability packs 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` — a reusable segmentation-mode main toolbar layout.
|
||||
- `cornerstone.segmentationTools` — the segmentation editing tools (brushes, scissors,
|
||||
contour tools) as a `{ passive: [...] }` block for `toolGroupAdditions`.
|
||||
- `cornerstone.annotationTools` — the measurement/annotation tools as a
|
||||
`{ passive: [...] }` block for `toolGroupAdditions`.
|
||||
|
||||
The tmtv extension exports its TMTV-specific `tmtv.toolbarButtons` / `tmtv.toolbarSections` packs the
|
||||
same way.
|
||||
|
||||
Two shipped modules demonstrate the pattern:
|
||||
|
||||
- [`segmentation/segmentationEditing.jsonc`](https://github.com/OHIF/Viewers/blob/master/platform/app/public/customizations/segmentation/segmentationEditing.jsonc)
|
||||
(`?customization=segmentation/segmentationEditing`) adds segmentation editing to the basic and longitudinal
|
||||
modes: in `mode` phase blocks keyed by each mode's route name (`basic`, `viewer`) it `$push`es the
|
||||
segmentation button/section/tool packs onto that mode's `toolbarButtons` / `toolbarSections` /
|
||||
`toolGroupAdditions`, swaps the right panels via `rightPanels`, and enables editing via
|
||||
`panelSegmentation.disableEditing`.
|
||||
- [`segmentation/segmentationAnnotationTools.jsonc`](https://github.com/OHIF/Viewers/blob/master/platform/app/public/customizations/segmentation/segmentationAnnotationTools.jsonc)
|
||||
(`?customization=segmentation/segmentationAnnotationTools`) enables the annotation tools inside the
|
||||
segmentation mode: in the `mode.segmentation` block it adds a `MeasurementTools` section to the
|
||||
primary bar, `$push`es a `{ $reference }` to `cornerstone.annotationTools` onto
|
||||
`toolGroupAdditions`, and `$push`es the measurement panel onto `rightPanels`.
|
||||
|
||||
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.
|
||||
|
||||
> **Every mode's panels are customizable with no opt-in.** A mode's layout declares
|
||||
> `leftPanels` / `rightPanels` as ordinary **arrays of panel ids** — the standard setup. On mode
|
||||
> enter the mode route seeds those arrays into the `leftPanels` / `rightPanels`
|
||||
> customizations at the bottom of the mode scope, then applies the `mode` phase blocks, then
|
||||
> resolves the sidebars from the final values — so commands compose with the mode's own list and
|
||||
> global-scope values win by scope precedence.
|
||||
|
||||
### URL modules, bootstrap, and client-side navigation (intended behavior)
|
||||
|
||||
Modules referenced from `?customization=` are loaded when the app applies URL customizations from
|
||||
|
||||
Reference in new issue
Block a user