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

* feat: Add extensibility for tmtv and segmentation modes

* Fixes for ordering issues on laod

* Remove unnecessary reference lookup

* Chane side panel timing to fix tests

* PR comments - change how mode definitions get created

* Improvements to mode customizations

* Start organizing customizations

* Misc fixes for a customization demo page

* Security fixes

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

No files matched your search

@@ -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).
+106 -3
View File
@@ -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>[&#123; mrn: 'M1' &#125;]</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(&#123; extensionManager, toolGroupService, commandsManager, servicesManager &#125;)</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>&#123; panelId, sourceServiceName, sourceEvents, forceActive? &#125;</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`
@@ -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