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 triggerApiEvent carrying 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', extends BaseTool) — 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 preMouseDownCallback and mouseDragCallback, 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 within grabSpherePixelDistance (default 20 px); if no sphere is under the cursor, its drag callback performs trackball camera rotation itself (with a rotateSampleDistanceFactor resolution 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 for VOLUME_VIEWPORT_NEW_VOLUME to reinitialize bounds.
  • VolumeCroppingControlTool (toolName: 'VolumeCroppingControl', extends AnnotationTool) — the companion for orthographic (2D/MPR) viewports: draggable reference lines that adjust the same clipping planes, synchronized with VolumeCroppingTool via VOLUMECROPPING_TOOL_CHANGED / VOLUMECROPPINGCONTROL_TOOL_CHANGED events (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 imageVolumeRenderingEnabled is 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 VolumeCroppingTool on 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.triggerApiEvent with an event carrying: an API code name for the tool, the displaySetInstanceUID, 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 VolumeCroppingTool the 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 a VOLVIEWER_ACTIVATE_TOOL signal via triggerApiEvent and the viewport performs registration in its own initTools()/activateTools() methods (Cornerstone3DVolumeView.jsx / _evtDisplaySetApi) — is not to be extended to the new tool. Follow instead the VTK MPR implementation (initMprTools / activateMprTools / deactivateMprTools in extensions/vtk/src/commandsModule.js): commands receive the tool group id and a viewport/component reference, create-or-get the tool group via ToolGroupManager, 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), initialized false by OHIFVtkVolumeViewport.setStateFromProps() alongside imageVolumeRenderingEnabled / segmentationSurfaceEnabled, cleared on unmount with them, and flipped via the createViewportToggleFeatureCommand pattern (republish through addDisplaySets). 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 own VOLUMECROPPING_TOOL_CHANGED Cornerstone 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 via C3dToolGroupManager; toolGroup.addTool(VolumeCroppingTool.toolName); apply Sonador defaults via toolGroup.setToolConfiguration(VolumeCroppingTool.toolName, { initialCropFactor, showCornerSpheres: true, showHandles: true }). Called from the viewport's existing initTools() (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 state active, then activate Select mode (FR-9).
  • deactivateVolumeCropping({ toolGroupId, displaySetInstanceUID }) — if the current tool mode is select, first restore default mode (§5.6); then toolGroup.setToolDisabled(VolumeCroppingTool.toolName) (removes handles and clipping; the volume returns to uncropped) and trigger the state event with state inactive.
  • toggleVolumeCropping({ viewports }) — the toolbar entry point: read the active viewport's displaySet; if imageVolumeRenderingEnabled is not true, no-op; otherwise flip volumeCroppingEnabled (via the createViewportToggleFeatureCommand helper pattern), republish the displaySet, and run activateVolumeCropping / deactivateVolumeCropping accordingly.

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 following ViewerImageRenderingEnabledToolbarButton.jsx, based on DisplaySetAttributeActiveToolbarButton with one extension: a visibleDisplaySetAttr (here imageVolumeRenderingEnabled) that hides the button (renders null) when falsy. Implement the extension additively on DisplaySetAttributeActiveToolbarButton (extensions/vtk): new optional prop, no behavior change for existing consumers; the component already subscribes to DISPLAY_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 same DisplaySetAttributeActiveToolbarButton extension with visibleDisplaySetAttr: 'volumeCroppingEnabled' (hidden unless cropping is on) and active state from volumeCropSelectActive (§5.6).

5.3 State event (extensions/viewer3d-volume/src/enums.js)

  • Add EVENTS.VOLVIEWER_TOOL_STATE and a tool code, e.g. TOOLS.VOLVIEWER_TOOL_CROP = 'VolumeCropping'; add a tool-mode code TOOLS.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' }. The hidden state is fired when FR-4 force-disables cropping because volume rendering turned off; toolMode is included on every mode change (FR-9).

5.4 Viewport integration (extensions/viewer3d-volume/src/components/Cornerstone3DVolumeView.jsx)

  • initTools() additionally calls commandsManager.runCommand('initVolumeCroppingTool', { toolGroupId, component }, ...) after the existing navigation-tool setup (viewport must already be added to the tool group). The viewport needs a commandsManager reference — pass it down the existing prop chain (OHIFVtkVolumeViewport already receives commandsManager; thread it through ConnectedVTKVolumeViewport).
  • activateTools(mode) gains the select mode (§5.6) alongside default and pan — implemented in the command per AR-3, with the viewport method delegating.
  • componentDidUpdate: when imageVolumeRenderingEnabled transitions to false and the displaySet has volumeCroppingEnabled === true, run deactivateVolumeCropping, reset the attribute, republish, and fire the state event with hidden (FR-4).
  • componentWillUnmount / _resetVolumeViewerState: ensure the cropping tool is disabled and the attribute cleared before teardown (the existing unmount already clears displaySet attributes in OHIFVtkVolumeViewport; add volumeCroppingEnabled to that list).

5.5 Attribute lifecycle (extensions/viewer3d-volume/src/ohifComponents/OHIFVtkVolumeViewport.js)

  • Initialize volumeCroppingEnabled = false in setStateFromProps() where imageVolumeRenderingEnabled is published; clear it in componentWillUnmount where 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 new enableVolumeSelectTool command (same shape as enableVolumeRotateTool / enableVolumePanTool) triggers mode select; the activation body (in the command, per AR-1/AR-3) sets the navigation tools passive on Primary and setToolActive(VolumeCroppingTool.toolName, { bindings: [{ mouseButton: Primary }] }). Leaving select re-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 existing toolMode state 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, select is not a legal mode — commands guard on volumeCroppingEnabled and fall back to default.
  • 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-basedinitMprTools({ 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.
  • volumeCroppingEnabled is 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_STATE event (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.
  • resetCTVolumeView returns 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/deactivateTools methods remain (as delegates) and the VOLVIEWER_ACTIVATE_TOOL signal path continues to work for Rotate/Pan/Select.
  • Existing consumers of DisplaySetAttributeActiveToolbarButton render unchanged.

8. Out of scope

  • VolumeCroppingControlTool reference 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

Edited by Sonador Claude