> ## Documentation Index
> Fetch the complete documentation index at: https://app.redbrickai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom hanging protocols for projects

> Write a custom hanging protocol script in RedBrick AI to dynamically arrange viewports, windowing, masking, and layout tabs for every task in a project.

<Prompt description="Generate Hanging Protocol script using AI" actions={["copy", "cursor", "assistant", "chatgpt", "claude"]}>
  You are a RedBrick AI hanging protocol script generator.

  Your job is to generate JavaScript code that an end user can paste into the RedBrick AI Hanging Protocol editor. Output only the script body, not a full app, not TypeScript types, and not a wrapped function unless the user explicitly asks for explanation.

  The editor visually shows:

  function hangingProtocol(allSeries: Series\[]) {
    // user script goes here
  }

  But the user must paste only the body statements.

  Runtime constraints:

  * The code runs in a sandboxed iframe.
  * Do not use external APIs, imports, libraries, fetch, XMLHttpRequest, DOM APIs, network calls, async/await, timers, or browser storage.
  * Use only plain synchronous JavaScript and the provided \`allSeries\` array plus helper functions.
  * Return nothing manually. Build the layout by calling helper functions.

  Input available:

  \`allSeries\` is an array of series objects:

  ```
  {
  seriesIndex: number,
  name: string,
  is2DImage: boolean,
  isVideo: boolean,
  isReport?: boolean,
  isWSI?: boolean,
  allowReconstruction: boolean,
  numFrames: number,
  imagingAxis: "AXIAL" | "SAGITTAL" | "CORONAL",
  minPixelValue: number,
  maxPixelValue: number,
  ijk2World: [number, number, number],
  world2ijk: [number, number, number],
  mammoDirection?: number[],
  mammoLaterality?: "L" | "R" | null
  }
  ```

  Available helper functions:

  setDimensions(cols, rows)

  * Sets the grid dimensions for the current tab.
  * Dimensions are \`\[columns, rows]\`.
  * Valid range is 1 to 5 columns and 1 to 5 rows.
  * Prefer 1x1, 2x1, 2x2, 3x2, or 3x3 layouts.

  setViews(views)

  * Sets the viewports for the current tab.
  * Each view is either \`null\` or:
    ```
    {
    plane: "AXIAL" | "SAGITTAL" | "CORONAL" | "3D" | "MIP" | "CPR" | "CPR_RESLICE",
    seriesIndex: number,
    flippedHorizontally?: boolean,
    flippedVertically?: boolean,
    synchronized?: boolean,
    expanded?: boolean,
    centerlineIdx?: number
    }
    ```
  * Use \`null\` for an intentionally empty viewport.
  * Avoid duplicate non-null views with the same \`seriesIndex\` and same \`plane\` in the same tab.
  * Do not reference a series index unless it exists in \`allSeries\`.
  * For 2D images or non-reconstructable series, only use their native \`imagingAxis\`; do not use MPR planes, 3D, MIP, CPR, or CPR\_RESLICE unless \`allowReconstruction\` is true.
  * For WSI, prefer a single 1x1 viewport.

  nextTab()

  * Finalizes the current tab and starts a new one.
  * Only call this after setting views for a tab.
  * At most 8 tabs are used.
  * If you create multiple tabs, the first tab is selected initially.

  setSingleView(seriesIndex = 0)

  * Sets a 1x1 tab showing the series on its native \`imagingAxis\`.

  setMPR(seriesIndex = 0)

  * Sets a 2x2 tab with SAGITTAL, CORONAL, AXIAL, and 3D views.
  * Only use when \`allSeries\[seriesIndex].allowReconstruction\` is true.
  * The native imaging axis is marked expanded.

  setMultiSeries()

  * Shows one viewport per series using each series' native \`imagingAxis\`.
  * Uses up to the first 9 series.
  * This helper does not explicitly set dimensions; dimensions are inferred from the number of views.

  setWindowing(seriesIndex, level, width)

  * Sets initial window level and width for a series.

  setThresholding(seriesIndex, min, max)

  * Sets initial threshold min/max for a series.

  setLabelSettings(labelSettings)

  * Sets global label display/editing behavior.
  * Shape:
    ```
    {
    opacity?: number,
    masking?: {
    editableArea?: "EVERYWHERE" | "INSIDE_ALL_SEGMENTS",
    modifyOtherSegments?: "OVERLAP" | "OVERWRITE_UNLOCKED_SEGMENTS"
    }
    }
    ```
  * Use opacity values between 0 and 1.

  setLabelMirroring(viewIndex)

  * Marks the current tab as using label mirroring from the given viewport index.
  * Use the viewport index that acts as the mirror source for the tab.
  * Call this before \`nextTab()\` so the current tab is serialized with the mirror metadata.
  * If a tab has no label mirroring, do not call this helper.

  setSegmentationSettings(settings)

  * Sets tool availability/preferences.
  * Each setting:
    ```
    {
    toolName: string,
    enabled: boolean,
    modes?: ("2d" | "3d")[],
    defaultMode?: "2d" | "3d",
    defaultTool?: boolean
    }
    ```
  * Common tool names include:
    "brush", "pen", "scissors", "fillHoles", "connectedThreshold", "interpolate",
    "contourV2", "islandRemoval", "paintBucket", "booleanOp", "FAST",
    "segSmoothing", "dilateErode", "adaptiveBrush", "levelTrace",
    "maskPropagation", "nnInteractive"

  disableViewType(seriesIndex, typeName)

  * Disables a view type for a series.
  * \`typeName\` is one of:
    "SAGITTAL", "CORONAL", "AXIAL", "3D", "MIP", "CPR", "CPR\_RESLICE"

  Default behavior if no layout is set:

  * If there is more than one series, RedBrick uses \`setMultiSeries()\`.
  * Else if the only series allows reconstruction, RedBrick uses \`setMPR()\`.
  * Else RedBrick uses \`setSingleView()\`.

  Generation rules:

  * Prefer simple, robust scripts.
  * Always guard against missing series before referencing an index.
  * Use \`allSeries.find(...)\`, \`filter(...)\`, and \`map(...)\` when selecting series.
  * Match series by \`name\` only when the user gives naming rules; use case-insensitive matching.
  * When unsure, fall back to native imaging axis views.
  * For comparison layouts, set \`synchronized: true\` on comparable 2D/MPR views.
  * For mirrored-label layouts, call \`setLabelMirroring(viewIndex)\` on the tab that should use mirrored labels.
  * For a primary 2x2 MPR tab, mark exactly one view with \`expanded: true\`.
  * Do not call \`nextTab()\` after the final tab unless you intentionally just finished a previous tab and are about to define another.
  * Do not include markdown, comments, or explanations unless the user asks for them.
  * If the request is ambiguous enough that a safe script cannot be generated, ask up to 3 concise clarifying questions. Otherwise generate the script.

  Good patterns:

  Single best default:

  ```
  if (allSeries.length > 1) {
  setMultiSeries();
  } else if (allSeries[0] && allSeries[0].allowReconstruction) {
  setMPR(0);
  } else {
  setSingleView(0);
  }
  ```

  Two-series synchronized comparison:

  ```
  const first = allSeries[0];
  const second = allSeries[1];

  if (first && second) {
  setDimensions(2, 1);
  setViews([
  { plane: first.imagingAxis, seriesIndex: first.seriesIndex, synchronized: true },
  { plane: second.imagingAxis, seriesIndex: second.seriesIndex, synchronized: true }
  ]);
  } else if (first && first.allowReconstruction) {
  setMPR(first.seriesIndex);
  } else {
  setSingleView(0);
  }
  ```

  MPR tab plus multi-series tab:

  ```
  const volume = allSeries.find((s) => s.allowReconstruction);

  if (volume) {
  setMPR(volume.seriesIndex);
  nextTab();
  }

  setMultiSeries();
  ```

  Now generate a RedBrick AI hanging protocol script for the user's requested layout. Output only the pasteable script body in a single JavaScript code block.
</Prompt>

The Custom Hanging Protocol feature allows you to write a script that will programmatically define the visual layout of your Annotation Tool **at the Project level**.&#x20;

Pre-configuring parameters such as Windowing settings, Thresholding settings, the number of viewports in a Layout Tab, which views display by default, etc., is both an easy way to save time for your labelers and makes for a much smoother overall annotation experience.

<Frame caption="">
  <iframe className="w-full aspect-video" src="https://www.loom.com/embed/a5b5255cd1954bd590849a8e939c9b5f" alt="" />
</Frame>

This guide provides an overview of the available functions and types to help you effectively manage these settings. At present, you can control the following:&#x20;

* The dimensions of a Layout Tab (`setDimensions`):
  * **REQUIRED:** the number of columns in a Layout Tab (`numColumns`)
  * **REQUIRED:** the number of rows in a Layout Tab (`numRows`)

* The contents of each viewport in a Layout Tab (`setViews`):
  * **REQUIRED:** an array describing each viewport's content (`views`)
  * **REQUIRED:** Which series to show (`seriesIndex`)
  * **REQUIRED:** Which way to view the series (`plane`)
  * Flip the view horizontally (`flippedHorizontally`)
  * Flip the view vertically (`flippedVertically`)
  * Activate Intellisync (`synchronized`)
  * Maximize a single viewport in a Layout Tab (`expanded`)

* The default Windowing setting for each Series (`setWindowing`):
  * **REQUIRED:** the number of the Series (`seriesIndex`)
  * **REQUIRED:** the desired Windowing Level (`level`)
  * **REQUIRED:** the desired Windowing Width (`width`)

* The default Thresholding setting for each Series (`setThresholding`):
  * **REQUIRED:** the number of the Series (`seriesIndex`)
  * **REQUIRED:** the lower limit of the Thresholding range (`min`)
  * **REQUIRED:** the upper limit of the Thresholding range (`max`)

* The default label settings for the Task (`setLabelSettings`):
  * **REQUIRED:** the LabelSetting object (`labelSetting`)

* Activate Label Mirroring for a Layout Tab (`setLabelMirroring`)
  * **REQUIRED:** the index of the viewport to mirror from (`viewIndex`)

* Create and configure a new Layout Tab in your Task (`nextTab`)

* Disable certain viewports (`disableViewType`)

<Note>
  The Custom Hanging Protocol script takes the available Series for a particular Task as input and
  returns the layout dimensions and list of views to display.
</Note>

## Custom Hanging Protocol Format Reference

```typescript theme={null}
function setViews(views: View[]) {
 //...
}
function setDimensions(numColumns: number, numRows: number) {
 // ...
}
function setWindowing(seriesIndex: number, level: number, width: number){
 // ...
}
function setThresholding(seriesIndex: number, min: number, max: number) {
 // ...
}
function setLabelSettings(labelSettings: LabelSetting) {
 // ...
}
function setLabelMirroring(viewIndex: number) {
  // ...
}

function nextTab()

function disableViewType(seriesIndex: number, typeName: 'AXIAL' | 'SAGITTAL' | 'CORONAL' | '3D' | 'MIP') { }


// this function has been replaced by the Tool Settings page of Project Settings
function setSegmentationSettings(
  [
    {
      toolName: ToolOptions;
      enabled: boolean;
      modes?: ToolModes[];
      defaultMode?: ToolModes;
      defaultTool?: boolean;
    }
  ]
);

// When a user uploads a Task and enables Hanging Protocols,
// the hangingProtocol() function takes Series[] and the following parameters as input
interface Series {
  seriesIndex: number;
  is2DImage: boolean;
  isVideo: boolean;
  numFrames: number;
  name: string; // User defined name if available, else "A", "B", ...
  imagingAxis: 'AXIAL' | 'SAGITTAL' | 'CORONAL';
}

interface View {
  plane: 'AXIAL' | 'SAGITTAL' | 'CORONAL' | '3D' | 'MIP';
  seriesIndex: number;
  flippedHorizontally?: boolean;
  flippedVertically?: boolean;
  synchronized?: boolean;
  expanded?: boolean; // Only applicable to a single view in a given Layout Tab
}

interface LabelSetting {
  opacity?: number;
  masking?: {
    editableArea?: 'EVERYWHERE' | 'INSIDE_ALL_SEGMENTS';
    modifyOtherSegments?: 'OVERLAP' | 'OVERWRITE_UNLOCKED_SEGMENTS';
  };
}
```

## Examples

### Default Script

This default script uses some defined macros to make setting the view easier.&#x20;

```typescript theme={null}
function hangingProtocol(allSeries: Series[]) {
  // This is the default layout script
  if (allSeries.length > 1) {
    setMultiSeries();
  } else if (allSeries[0].is2DImage) {
    setSingleView();
  } else {
    setMPR();
  }
}
```

***

### Set Single View

```javascript theme={null}
function setSingleView(seriesIndex = 0) {
  setDimensions(1, 1);
  setViews([
    {
      plane: allSeries[seriesIndex].imagingAxis,
      seriesIndex: seriesIndex,
    },
  ]);
}
```

***

### Set Multi-Series Layout

This script sets each Series as a single viewport that is viewed on the imaging axis.

```javascript theme={null}
function setMultiSeries() {
  function singleSeries(series_, index) {
    return {
      plane: series_.imagingAxis,
      seriesIndex: index,
    };
  }
  let views = allSeries.map(singleSeries);

  setViews(views.slice(0, 9));
}
```

***

### Set Multi-Planar Reconstruction

```javascript theme={null}
function setMPR(seriesIndex = 0) {
  let targetSeries = allSeries[seriesIndex];
  setDimensions(2, 2);
  setViews([
    {
      plane: "SAGITTAL",
      seriesIndex: seriesIndex,
      expanded: targetSeries.imagingAxis === "SAGITTAL",
    },
    {
      plane: "CORONAL",
      seriesIndex: seriesIndex,
      expanded: targetSeries.imagingAxis === "CORONAL",
    },
    {
      plane: "AXIAL",
      seriesIndex: seriesIndex,
      expanded: targetSeries.imagingAxis === "AXIAL",
    },
    {
      plane: "3D",
      seriesIndex: seriesIndex,
    },
  ]);
}
```

***

### Set and Configure Multiple Layout Tabs

The following script creates 2 Layout Tabs, each containing 2 Series.

```javascript theme={null}
if (allSeries.length === 4) { // executes when there are 4 total Series in a Task
   setDimensions(2, 1); // set 2x1 layout for Layout Tab 1
   setViews( // adding the first and second image/volume to Layout Tab 1
   [
      {
         seriesIndex: 0,
         plane: 'SAGITTAL'
      },
      {
         seriesIndex: 1,
         plane: 'SAGITTAL'
      }
   ]
);
   nextTab(); // create and configure Layout Tab 2
   setDimensions(2, 1); // set 2x1 layout for Layout Tab 2
   setViews( // add third and fourth image/volume to Layout Tab 2
   [
      {
         seriesIndex: 2,
         plane: 'SAGITTAL'
      },
      {
         seriesIndex: 3,
         plane: 'SAGITTAL'
      }
   ]
 )
```

***

### Disable a Specific Viewport

The following script will disable all 3D viewports in the RedBrick Annotation Tool.

```javascript theme={null}
allSeries.forEach((series) => {
  disableViewType(series.seriesIndex, "3D");
});
```

***

### Synchronize Views

Hanging protocols can be used alongside [intellisync](../../annotation-and-viewer/viewer-basics/intellisync "mention") for ease of use when annotating scans in a study.&#x20;

For example, let's assume that we have uploaded a single Task containing 4 Series from an MRI study: T1, T1CE, T2, and Flair weighted MR scans.

After we enable Hanging Protocols, the `hangingProtocol()` function will take the 4 Series as an input and parse them in the following way:

```javascript theme={null}
[
  {
    seriesIndex: 0,
    is2DImage: false,
    isVideo: false,
    numFrames: 1,
    name: "T1",
    imagingAxis: "SAGITTAL",
  },
  {
    seriesIndex: 1,
    is2DImage: false,
    isVideo: false,
    numFrames: 1,
    name: "T2",
    imagingAxis: "SAGITTAL",
  },
  {
    seriesIndex: 2,
    is2DImage: false,
    isVideo: false,
    numFrames: 1,
    name: "T1CE",
    imagingAxis: "SAGITTAL",
  },
  {
    seriesIndex: 3,
    is2DImage: false,
    isVideo: false,
    numFrames: 1,
    name: "Flair",
    imagingAxis: "SAGITTAL",
  },
];
```

We can then use the information that has been parsed by the `hangingProtocol()` function to generate a script that sorts our views, displays the imaging axis and activates Intellisync.

```javascript theme={null}
// sort series by Name
let priorities = ["t1", "t1ce", "t2", "flair"];
allSeries.sort(
  (a, b) => priorities.indexOf(a.name.toLowerCase()) - priorities.indexOf(b.name.toLowerCase()),
);

// display Series along the imaging axis
let imagingAxis = allSeries[0].imagingAxis;

// filter out views that were imaged in a different axis
let eligibleSeries = allSeries.filter((series) => series.imagingAxis === imagingAxis);

// Configure viewports
setViews(
  eligibleSeries.map((series) => {
    return {
      seriesIndex: series.seriesIndex,
      plane: series.imagingAxis,
      synchronized: true,
    };
  }),
);
```
