3D Volume Viewer: volume cropping tool with command-based tool registration
1. Overview
Cornerstone3D provides interactive volume cropping: users crop an active volume to visualize internal structures (e.g. cropping out the rib cage to see the heart, or abdominal structures to see the kidneys/liver). Implement volume cropping in the Sonador 3D Volume Viewer (extensions/viewer3d-volume):
- A toolbar toggle, placed to the right of the "More" menu, active/visible only when volume rendering is enabled. Clicking toggles the cropping tool between enabled and disabled; the icon shows an Active state when enabled.
- When enabled, the cropping controls (manipulation handles) are visible in the viewport; when disabled they are not.
- An interaction ("Select") tool state so the user can actually grab and drag the crop handles: with cropping enabled, the user moves between three tool states — Default (rotate-led navigation), Pan, and Select (crop-handle interaction) — via the toolbar (§5.6).
- Tool activation/deactivation is implemented as commands in the extension's commands module — not via the viewport-signal pattern — establishing the command-based tool-registration convention for the 3D viewports (see §6, Refactor analysis).
- The tool's active state is stored as a displaySet property; toolbar widgets read it to move between three states: hidden (volume rendering off), cropping active, cropping inactive.
- Every toggle fires a displaySet-service
triggerApiEventcarrying an API code name for the tool, the displaySet UID, and the new state so other tools/components can synchronize.
Design elaborated from the discussion on imaging-development-env#95 (note of 2026-07-14, incl. Figure 1 toolbar placement). The interaction model (§3 FR-7 to FR-9, §5.6) was added after first implementation results showed the toggle working but the handles unreachable — the primary mouse binding was consumed by the navigation tools.
2. Cornerstone3D API (verified against the pinned @cornerstonejs/tools 4.20.0)
Cornerstone3D ships two cooperating tools (both exported from @cornerstonejs/tools in 4.20.0):
-
VolumeCroppingTool(toolName: 'VolumeCropping', extendsBaseTool) — the tool for VOLUME_3D viewports. Renders 6 face spheres + 8 corner spheres + 12 edge lines around a cropping box and applies real-time clipping planes to the volume. Key API:- Registration/activation:
addTool(VolumeCroppingTool),toolGroup.addTool(VolumeCroppingTool.toolName),toolGroup.setToolActive(VolumeCroppingTool.toolName, { bindings }). -
Input dispatch (verified in source; drives the §5.6 design): the tool receives input through
preMouseDownCallbackandmouseDragCallback, which Cornerstone3D dispatches only to the tool holding the pressed mouse-button binding — so grabbing handles requires the tool to be active with the Primary binding; active-without-bindings renders handles/clipping but receives no input. On mouse-down it hit-tests spheres withingrabSpherePixelDistance(default 20 px); if no sphere is under the cursor, its drag callback performs trackball camera rotation itself (with arotateSampleDistanceFactorresolution drop during the drag), and Shift+drag rotates the clipping planes (rotatePlanesOnDrag). Camera navigation therefore remains available while the tool owns Primary. - Configuration:
initialCropFactor(default 0.08),showHandles,showCornerSpheres,showClippingPlanes,sphereColors(per-axis),sphereRadius,grabSpherePixelDistance. - Instance methods:
setHandlesVisible(bool),setClippingPlanesVisible(bool), matching getters. - Events: fires
VOLUMECROPPING_TOOL_CHANGED(detail:originalClippingPlanes,seriesInstanceUID,viewportId) on every handle drag; listens forVOLUME_VIEWPORT_NEW_VOLUMEto reinitialize bounds.
- Registration/activation:
-
VolumeCroppingControlTool(toolName: 'VolumeCroppingControl', extendsAnnotationTool) — the companion for orthographic (2D/MPR) viewports: draggable reference lines that adjust the same clipping planes, synchronized withVolumeCroppingToolviaVOLUMECROPPING_TOOL_CHANGED/VOLUMECROPPINGCONTROL_TOOL_CHANGEDevents (matched on series instance UID; the tools do not need to share a tool group). Its drag callback explicitly no-ops on VOLUME_3D viewports.
The Sonador 3D Volume Viewer is a single VOLUME_3D viewport (Cornerstone3DVolumeViewport, cornerstone3dViewProps.type: ViewportType.VOLUME_3D), so VolumeCroppingTool is the tool this issue integrates. VolumeCroppingControlTool becomes relevant when crop reference lines are wanted in the MPR views — noted as a follow-up in §8; the event contract above means it can be added later without reworking this feature.
3. Functional requirements
-
FR-1 — Toolbar toggle. A cropping toggle button appears in the volume viewer toolbar immediately after the "More" (
CTViewportOptions) group (extensions/viewer3d-volume/src/toolbarModule.js), per Figure 1 on the origin discussion. It uses a "crop" icon — the closest match in the Sonador Viewer SVG icon set (platform/ui/src/elements/Icon/); if no suitable glyph exists, add a crop SVG following the existing icon-registration pattern. -
FR-2 — Three widget states. The button reads displaySet state and renders: hidden when
imageVolumeRenderingEnabledis false; inactive (normal) when volume rendering is on and cropping is off; active (highlighted) when cropping is on. -
FR-3 — Toggle behavior. Clicking runs a command that flips the displaySet property, activates/deactivates
VolumeCroppingToolon the viewer's tool group, and re-publishes the displaySet. Enabled: manipulation spheres/edges appear and clipping planes apply. Disabled: controls disappear and the volume returns to its uncropped rendering. - FR-4 — Volume-rendering dependency. Disabling volume rendering ("3D Volume" in the More menu) while cropping is enabled deactivates the cropping tool and resets the displaySet property to false (the button hides per FR-2). Re-enabling volume rendering returns the button in its inactive state.
-
FR-5 — State event. Every toggle triggers
DisplaySetApi.Instance.displaySetService.triggerApiEventwith an event carrying: an API code name for the tool, thedisplaySetInstanceUID, and the new state (see §5.3). Other components must be able to synchronize from this event alone. -
FR-6 — No regression. Rotate/Pan/Zoom interactions, the More-menu toggles, the rendering menu button, surface segmentation display, and reset behavior (
resetCTVolumeView, which purges and reloads the viewer) are unaffected when cropping is off. After a viewer reset, cropping returns to disabled with the button in its inactive state. -
FR-7 — Select tool state. With cropping enabled, a Select toolbar widget (label proposal: "Adjust"; alternatives: "Select", "Crop Handles") appears alongside Rotate and Pan. Activating it gives
VolumeCroppingToolthe Primary mouse binding so the user can grab and drag face/corner spheres. The widget is hidden whenever cropping is disabled (and therefore whenever volume rendering is off). - FR-8 — Three tool states with cropping enabled. The user can switch between Default (trackball rotate on left button, pan on right, zoom on middle/wheel — the current default), Pan (pan on left button), and Select (crop-handle interaction on left button; empty-space drags fall through to the tool's built-in camera rotation per §2, Shift+drag rotates the crop planes; pan stays on right, zoom on middle/wheel). Default and Pan behave exactly as today; in those states the crop handles remain visible but receive no mouse input.
- FR-9 — Mode transitions. Enabling cropping automatically activates Select (the user enabled the tool to use it); disabling cropping while Select is active reverts to Default. Switching to Default or Pan while cropping is enabled leaves the handles visible. Every mode change fires the state event (§5.3) so widgets stay synchronized.
4. Architectural requirements
-
AR-1 — Commands own tool registration and activation. The methods that register/activate/deactivate the cropping tool are commands in
extensions/viewer3d-volume/src/commandsModule.js. The current volume viewer pattern — commands emit aVOLVIEWER_ACTIVATE_TOOLsignal viatriggerApiEventand the viewport performs registration in its owninitTools()/activateTools()methods (Cornerstone3DVolumeView.jsx/_evtDisplaySetApi) — is not to be extended to the new tool. Follow instead the VTK MPR implementation (initMprTools/activateMprTools/deactivateMprToolsinextensions/vtk/src/commandsModule.js): commands receive the tool group id and a viewport/component reference, create-or-get the tool group viaToolGroupManager, register tools idempotently, set activation modes, and emit the state event. This is the most recent pattern and handles needs (central tool-group management, race-free init, cross-component signaling) the older viewports have not had to contend with. -
AR-2 — DisplaySet as state of record. The cropping state lives on the displaySet (
volumeCroppingEnabled), initializedfalsebyOHIFVtkVolumeViewport.setStateFromProps()alongsideimageVolumeRenderingEnabled/segmentationSurfaceEnabled, cleared on unmount with them, and flipped via thecreateViewportToggleFeatureCommandpattern (republish throughaddDisplaySets). Toolbar widgets read displaySet state (DisplaySetAttributeActiveToolbarButton,extensions/vtk/src/toolbarComponents/), extended for the hidden state (§5.2). -
AR-3 — Lightest-touch viewport refactor. The viewport keeps its current methods (
initTools,activateTools,deactivateTools); their bodies delegate to the new commands where registration/activation is concerned. No method renames, no signature changes, no behavioral changes to existing tools. See §6. -
AR-4 — Event-based synchronization. The state event (§5.3) is the synchronization contract; components must not reach into the tool group to infer state.
VolumeCroppingTool's ownVOLUMECROPPING_TOOL_CHANGEDCornerstone event remains available for fine-grained clipping-plane consumers (e.g. a future MPR control-line integration).
5. Implementation specification
5.1 Commands (extensions/viewer3d-volume/src/commandsModule.js)
New commands, modeled on the MPR trio in extensions/vtk/src/commandsModule.js:
-
initVolumeCroppingTool({ toolGroupId, component })— idempotent registration:c3dAddTool(VolumeCroppingTool); get-or-create the tool group viaC3dToolGroupManager;toolGroup.addTool(VolumeCroppingTool.toolName); apply Sonador defaults viatoolGroup.setToolConfiguration(VolumeCroppingTool.toolName, { initialCropFactor, showCornerSpheres: true, showHandles: true }). Called from the viewport's existinginitTools()(thin delegation, AR-3) so registration happens once the viewport is in the tool group. -
activateVolumeCropping({ toolGroupId, displaySetInstanceUID })—toolGroup.setToolActive(VolumeCroppingTool.toolName)(no bindings yet — handles/clipping render; Primary binding is granted by Select mode per §5.6), trigger the state event (§5.3) with stateactive, then activate Select mode (FR-9). -
deactivateVolumeCropping({ toolGroupId, displaySetInstanceUID })— if the current tool mode isselect, first restoredefaultmode (§5.6); thentoolGroup.setToolDisabled(VolumeCroppingTool.toolName)(removes handles and clipping; the volume returns to uncropped) and trigger the state event with stateinactive. -
toggleVolumeCropping({ viewports })— the toolbar entry point: read the active viewport's displaySet; ifimageVolumeRenderingEnabledis not true, no-op; otherwise flipvolumeCroppingEnabled(via thecreateViewportToggleFeatureCommandhelper pattern), republish the displaySet, and runactivateVolumeCropping/deactivateVolumeCroppingaccordingly.
5.2 Toolbar (extensions/viewer3d-volume/src/toolbarModule.js + src/toolbarComponents/)
- New definition after
CTViewportOptions:{ id: 'CTVolumeCropping', label: 'Crop', icon: '<crop icon>', CustomComponent: ViewerVolumeCroppingToolbarButton, type: 'command', commandName: 'toggleVolumeCropping' }. -
ViewerVolumeCroppingToolbarButton.jsx— wrapper followingViewerImageRenderingEnabledToolbarButton.jsx, based onDisplaySetAttributeActiveToolbarButtonwith one extension: avisibleDisplaySetAttr(hereimageVolumeRenderingEnabled) that hides the button (renders null) when falsy. Implement the extension additively onDisplaySetAttributeActiveToolbarButton(extensions/vtk): new optional prop, no behavior change for existing consumers; the component already subscribes toDISPLAY_SET_CHANGED, so both attributes update from the same subscription. -
Select widget (FR-7):
{ id: 'CTVolumeCropSelect', label: 'Adjust', icon: '<cursor/hand icon>', CustomComponent: ViewerVolumeCropSelectToolbarButton, type: 'command', commandName: 'enableVolumeSelectTool' }, placed after the crop toggle. The wrapper uses the sameDisplaySetAttributeActiveToolbarButtonextension withvisibleDisplaySetAttr: 'volumeCroppingEnabled'(hidden unless cropping is on) and active state fromvolumeCropSelectActive(§5.6).
5.3 State event (extensions/viewer3d-volume/src/enums.js)
- Add
EVENTS.VOLVIEWER_TOOL_STATEand a tool code, e.g.TOOLS.VOLVIEWER_TOOL_CROP = 'VolumeCropping'; add a tool-mode codeTOOLS.VOLVIEWER_TOOL_SELECT = 'select'alongside the existing default/pan modes. - Event payload (fired via
DisplaySetApi.Instance.displaySetService.triggerApiEvent):{ tool: TOOLS.VOLVIEWER_TOOL_CROP, displaySetInstanceUID, state: 'active' | 'inactive' | 'hidden', toolMode?: 'default' | 'pan' | 'select' }. Thehiddenstate is fired when FR-4 force-disables cropping because volume rendering turned off;toolModeis included on every mode change (FR-9).
5.4 Viewport integration (extensions/viewer3d-volume/src/components/Cornerstone3DVolumeView.jsx)
-
initTools()additionally callscommandsManager.runCommand('initVolumeCroppingTool', { toolGroupId, component }, ...)after the existing navigation-tool setup (viewport must already be added to the tool group). The viewport needs acommandsManagerreference — pass it down the existing prop chain (OHIFVtkVolumeViewportalready receivescommandsManager; thread it throughConnectedVTKVolumeViewport). -
activateTools(mode)gains theselectmode (§5.6) alongsidedefaultandpan— implemented in the command per AR-3, with the viewport method delegating. -
componentDidUpdate: whenimageVolumeRenderingEnabledtransitions to false and the displaySet hasvolumeCroppingEnabled === true, rundeactivateVolumeCropping, reset the attribute, republish, and fire the state event withhidden(FR-4). -
componentWillUnmount/_resetVolumeViewerState: ensure the cropping tool is disabled and the attribute cleared before teardown (the existing unmount already clears displaySet attributes inOHIFVtkVolumeViewport; addvolumeCroppingEnabledto that list).
5.5 Attribute lifecycle (extensions/viewer3d-volume/src/ohifComponents/OHIFVtkVolumeViewport.js)
- Initialize
volumeCroppingEnabled = falseinsetStateFromProps()whereimageVolumeRenderingEnabledis published; clear it incomponentWillUnmountwhere the other attributes are cleared.
5.6 Interaction model: tool states with cropping enabled
The navigation tools and the cropping tool contend for the Primary mouse button; only the tool holding the binding receives preMouseDownCallback/mouseDragCallback (§2). Mode switching therefore reassigns Primary while leaving Secondary/Auxiliary/Wheel stable:
| Mode | Primary (left) | Secondary (right) | Auxiliary / Wheel | VolumeCroppingTool state |
|---|---|---|---|---|
| Default | TrackballRotateTool |
PanTool |
SonadorZoomTool |
Active, no bindings (handles visible, no input) |
| Pan | PanTool |
— | SonadorZoomTool |
Active, no bindings |
| Select ("Adjust") |
VolumeCroppingTool — sphere within grabSpherePixelDistance: drag handle; empty space: built-in trackball rotation; Shift+drag: rotate crop planes |
PanTool |
SonadorZoomTool |
Active, Primary binding |
Implementation notes:
- Mode switching extends the existing
activateTools(mode)flow: a newenableVolumeSelectToolcommand (same shape asenableVolumeRotateTool/enableVolumePanTool) triggers modeselect; the activation body (in the command, per AR-1/AR-3) sets the navigation tools passive on Primary andsetToolActive(VolumeCroppingTool.toolName, { bindings: [{ mouseButton: Primary }] }). Leavingselectre-activates VolumeCroppingTool without bindings (handles stay visible) and restores the requested navigation mode. - Track the current mode on the displaySet (
volumeCropSelectActive, or fold into the existingtoolModestate published via the state event) so the Select widget's active state survives re-renders and other components can synchronize (FR-9, §5.3). - If mode changes arrive while cropping is disabled,
selectis not a legal mode — commands guard onvolumeCroppingEnabledand fall back todefault. - Rationale for keeping Select usable for navigation: the tool's built-in empty-space rotation (§2) means users do not need to leave Select to reorient the volume — mode round-trips are only needed for left-button pan.
6. Refactor analysis: command-based tool registration across the 3D viewports
Three tool-registration patterns exist today. The goal is convergence on the command pattern with the lightest possible touch: viewports keep their current methods and call commands to get registration done.
| Viewport / stack | Current pattern | Disposition |
|---|---|---|
VTK MPR (extensions/vtk/src/commandsModule.js + Cornerstone3DSliceView) |
Command-based — initMprTools({ toolGroupId, component }) / activateMprTools / deactivateMprTools own tool-group creation, registration, activation modes, and emit VTK_MPR_ACTIVATE_TOOL for state sync |
Reference implementation. No change. |
3D Volume Viewer (extensions/viewer3d-volume) |
Signal-based — toolbar commands (enableVolumeRotateTool, enableVolumePanTool) only triggerApiEvent(VOLVIEWER_ACTIVATE_TOOL); the viewport's initTools()/activateTools(mode)/deactivateTools() own registration and react to the signal in _evtDisplaySetApi
|
In scope (this issue). Add initVolumeViewerTools({ toolGroupId, component }) / activateVolumeViewerTools({ toolGroupId, toolMode }) / deactivateVolumeViewerTools({ toolGroupId }) commands encapsulating the existing registration/activation bodies; the viewport methods become thin delegates that runCommand (names, signatures, call sites, and the VOLVIEWER_ACTIVATE_TOOL signal path all unchanged — the signal handler now delegates too). The cropping commands (§5.1) and the select mode (§5.6) sit beside them. |
Inspection / enlarged view (extensions/vtk/src/components/Cornerstone3DInspectionView.js) |
Viewport-internal — _registerTools() / initTools() / activateTools(mode) with local React state; no commands |
Follow-up (documented, not in scope). Same treatment: lift activateTools(mode) bodies into initInspectionTools / activateInspectionTools commands in extensions/vtk/src/commandsModule.js; viewport methods delegate. Natural to fold into the #108 toggle-behavior work since both touch activateTools. |
Segmentation Editor 3D layout (extensions/seg-editor/src/components/Cornerstone3DSegmentationViewerLayout.js) |
Viewport-internal — layout-owned imgTools / surfaceTools groups created inline |
Follow-up (documented, not in scope). Delegate registration to commands when editing tools land (#92) — the tool-toggle work there should adopt the command pattern from day one rather than adding another viewport-internal toolset. |
M3D / Three.js viewer (extensions/viewer3d) |
No Cornerstone3D tools (camera-controls based) | Not applicable. |
Command signature convention (normative for this and follow-up work, from the MPR reference): commands accept { toolGroupId, component | viewportId, ...options }; are idempotent (get-or-create tool group; re-entry is a no-op); never assume a viewport is already bound (guard via component._checkViewportActive() as initMprTools does); and emit a displaySet-service API event on activation-state changes so widgets and other viewports can synchronize without polling tool groups.
7. Acceptance criteria
- With volume rendering enabled, a crop toggle appears to the right of the More menu; with volume rendering disabled it is not rendered.
-
Clicking the toggle activates
VolumeCroppingTool: face/corner spheres and edge lines appear and clipping planes apply; the button shows its Active state. - Clicking again deactivates the tool: controls disappear and the volume renders uncropped; the button returns to inactive.
-
volumeCroppingEnabledis stored on the displaySet: initialized false on load, flipped by the toggle command, cleared on unmount, and readable by any component. - With cropping enabled, a Select ("Adjust") widget appears alongside Rotate/Pan; it is hidden whenever cropping is disabled.
- In Select mode, dragging a face/corner sphere crops the volume in real time; dragging empty space rotates the camera; Shift+drag rotates the crop planes; right-button pan and wheel zoom continue to work.
- In Default and Pan modes with cropping enabled, handles remain visible but receive no mouse input, and Rotate/Pan/Zoom behave exactly as with cropping disabled.
-
Enabling cropping auto-activates Select; disabling cropping reverts to Default; mode changes and crop toggles each fire the
VOLVIEWER_TOOL_STATEevent (tool code, displaySet UID, state, toolMode) such that a subscriber receiving only this event tracks the state accurately. - Turning off volume rendering while cropping is active deactivates cropping, hides both widgets, and fires the state event; turning volume rendering back on shows the crop toggle inactive.
-
resetCTVolumeViewreturns the viewer to a clean state with cropping disabled, Default mode active, and no stale sphere/edge actors. -
Registration/activation flows through the new commands; the viewport's
initTools/activateTools/deactivateToolsmethods remain (as delegates) and theVOLVIEWER_ACTIVATE_TOOLsignal path continues to work for Rotate/Pan/Select. -
Existing consumers of
DisplaySetAttributeActiveToolbarButtonrender unchanged.
8. Out of scope
-
VolumeCroppingControlToolreference lines in the 2D/MPR views (the event contract in §2 supports adding this later). - Volume cropping in the Segmentation Editor's 3D viewport (natural follow-up once #121 (closed) lands volume rendering there; the command-based registration from this issue is the prerequisite).
- Persisting crop bounds across sessions or writing them to DICOM.
- Completing the follow-up refactors in §6 (inspection view, seg editor layout) — documented here as the prescribed pattern, executed separately.
9. References
- Discussion/origin: imaging-development-env#95
- Related: oak-tree/medical-imaging/ohif-viewers#108, #121 (closed), oak-tree/medical-imaging/ohif-viewers!58
- Reference implementation files:
extensions/vtk/src/commandsModule.js(MPR command trio — normative pattern),extensions/viewer3d-volume/src/commandsModule.js(createViewportToggleFeatureCommand),extensions/viewer3d-volume/src/toolbarModule.js,extensions/viewer3d-volume/src/components/Cornerstone3DVolumeView.jsx,extensions/viewer3d-volume/src/ohifComponents/OHIFVtkVolumeViewport.js,extensions/vtk/src/toolbarComponents/DisplaySetAttributeActiveToolbarButton.jsx - Cornerstone3D source (v4.20.0, verified):
VolumeCroppingTool.ts(seepreMouseDownCallbacksphere hit-test and_dragCallbackrotation fallback),VolumeCroppingControlTool.ts - Cornerstone3D documentation: Tools introduction / tool groups, ToolGroups
- Wiki: Cornerstone3D Integration Reference, VTK.js <-> Cornerstone3D Integration