From 80151603e6f0d8aba96a6533925c77e559753ee8 Mon Sep 17 00:00:00 2001 From: Alireza Date: Fri, 27 Oct 2023 10:05:58 -0400 Subject: [PATCH] fix(scroll): take into account the slab thickness for scrolling (#849) * fix(scroll): take into account the slab thickness for scrolling * apply review comments * apply review comments --- common/reviews/api/core.api.md | 2 + .../src/RenderingEngine/BaseVolumeViewport.ts | 7 +- .../src/RenderingEngine/VolumeViewport.ts | 8 + .../getTargetVolumeAndSpacingInNormalDir.ts | 30 +- .../tools/examples/volumeSlabScroll/index.ts | 272 ++++++++++++++++++ utils/ExampleRunner/example-info.json | 4 + utils/demo/helpers/addDropdownToToolbar.ts | 18 +- 7 files changed, 325 insertions(+), 16 deletions(-) create mode 100644 packages/tools/examples/volumeSlabScroll/index.ts diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 9e8c14368..edb31b952 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -148,6 +148,8 @@ export abstract class BaseVolumeViewport extends Viewport implements IVolumeView // (undocumented) static get useCustomRenderingPipeline(): boolean; // (undocumented) + protected viewportProperties: VolumeViewportProperties; + // (undocumented) worldToCanvas: (worldPos: Point3) => Point2; } diff --git a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts index 5e62cc302..bd3cf60dc 100644 --- a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts +++ b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts @@ -31,7 +31,6 @@ import type { Point2, Point3, VOIRange, - ViewportProperties, VolumeViewportProperties, } from '../types'; import { VoiModifiedEventDetail } from '../types/EventTypes'; @@ -79,7 +78,7 @@ abstract class BaseVolumeViewport extends Viewport implements IVolumeViewport { VolumeViewportProperties >(); - private viewportProperties: VolumeViewportProperties = {}; + protected viewportProperties: VolumeViewportProperties = {}; constructor(props: ViewportInput) { super(props); @@ -606,9 +605,7 @@ abstract class BaseVolumeViewport extends Viewport implements IVolumeViewport { if (properties.slabThickness !== undefined) { this.setSlabThickness(properties.slabThickness); - //We need to set the current slabthickness here since setSlabThickness is define in VolumeViewport - // this.currentViewportProperties.get(volumeId).slabThickness = - // properties.slabThickness; + //We need to set the current slabThickness here since setSlabThickness is define in VolumeViewport this.viewportProperties.slabThickness = properties.slabThickness; } diff --git a/packages/core/src/RenderingEngine/VolumeViewport.ts b/packages/core/src/RenderingEngine/VolumeViewport.ts index c67e623a9..efab56776 100644 --- a/packages/core/src/RenderingEngine/VolumeViewport.ts +++ b/packages/core/src/RenderingEngine/VolumeViewport.ts @@ -302,6 +302,7 @@ class VolumeViewport extends BaseVolumeViewport { const currentCamera = this.getCamera(); this.updateClippingPlanesForActors(currentCamera); this.triggerCameraModifiedEventIfNecessary(currentCamera, currentCamera); + this.viewportProperties.slabThickness = slabThickness; } /** @@ -387,6 +388,13 @@ class VolumeViewport extends BaseVolumeViewport { throw new Error(`No actor found for the given volumeId: ${volumeId}`); } + // if a custom slabThickness was set, we need to reset it + if (volumeActor.slabThickness) { + volumeActor.slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS; + this.viewportProperties.slabThickness = undefined; + this.updateClippingPlanesForActors(this.getCamera()); + } + const imageVolume = cache.getVolume(volumeActor.uid); if (!imageVolume) { throw new Error( diff --git a/packages/core/src/utilities/getTargetVolumeAndSpacingInNormalDir.ts b/packages/core/src/utilities/getTargetVolumeAndSpacingInNormalDir.ts index 175815c71..79dacca43 100644 --- a/packages/core/src/utilities/getTargetVolumeAndSpacingInNormalDir.ts +++ b/packages/core/src/utilities/getTargetVolumeAndSpacingInNormalDir.ts @@ -1,7 +1,7 @@ import cache from '../cache/cache'; import { EPSILON } from '../constants'; // import type { VolumeViewport } from '../RenderingEngine' -import { ICamera, IImageVolume, IVolumeViewport } from '../types'; +import { ICamera, IImageVolume, IVolumeViewport, Point3 } from '../types'; import getSpacingInNormalDirection from './getSpacingInNormalDirection'; import { getVolumeLoaderSchemes } from '../loaders/volumeLoader'; @@ -71,9 +71,11 @@ export default function getTargetVolumeAndSpacingInNormalDir( const imageVolume = imageVolumes[imageVolumeIndex]; const { uid: actorUID } = volumeActors[imageVolumeIndex]; - const spacingInNormalDirection = getSpacingInNormalDirection( + + const spacingInNormalDirection = getSpacingInNormal( imageVolume, - viewPlaneNormal + viewPlaneNormal, + viewport ); return { imageVolume, spacingInNormalDirection, actorUID }; @@ -104,9 +106,10 @@ export default function getTargetVolumeAndSpacingInNormalDir( continue; } - const spacingInNormalDirection = getSpacingInNormalDirection( + const spacingInNormalDirection = getSpacingInNormal( imageVolume, - viewPlaneNormal + viewPlaneNormal, + viewport ); // Allow for EPSILON part larger requirement to prefer earlier volumes @@ -124,3 +127,20 @@ export default function getTargetVolumeAndSpacingInNormalDir( return smallest; } + +function getSpacingInNormal( + imageVolume: IImageVolume, + viewPlaneNormal: Point3, + viewport: IVolumeViewport +): number { + const { slabThickness } = viewport.getProperties(); + let spacingInNormalDirection = slabThickness; + if (!slabThickness) { + spacingInNormalDirection = getSpacingInNormalDirection( + imageVolume, + viewPlaneNormal + ); + } + + return spacingInNormalDirection; +} diff --git a/packages/tools/examples/volumeSlabScroll/index.ts b/packages/tools/examples/volumeSlabScroll/index.ts new file mode 100644 index 000000000..46c82af6a --- /dev/null +++ b/packages/tools/examples/volumeSlabScroll/index.ts @@ -0,0 +1,272 @@ +import { + RenderingEngine, + Types, + Enums, + setVolumesForViewports, + volumeLoader, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + addButtonToToolbar, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import addDropDownToToolbar from '../../../../utils/demo/helpers/addDropdownToToolbar'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { + ToolGroupManager, + StackScrollMouseWheelTool, + ZoomTool, + ReferenceLines, + PanTool, + Enums: csToolsEnums, + WindowLevelTool, +} = cornerstoneTools; + +const { ViewportType } = Enums; +const { MouseBindings } = csToolsEnums; + +// Define a unique id for the volume +const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix +const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use +const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id + +// ======== Set up page ======== // +setTitleAndDescription( + 'Volume Slab Scroll', + 'Here we demonstrate how you can programmatically change the slab thickness of volume for rendering and view them in 3D.' +); + +const size = '500px'; +const content = document.getElementById('content'); +const viewportGrid = document.createElement('div'); + +viewportGrid.style.display = 'flex'; +viewportGrid.style.flexDirection = 'row'; + +const element1 = document.createElement('div'); +const element2 = document.createElement('div'); +const element3 = document.createElement('div'); +element1.oncontextmenu = () => false; +element2.oncontextmenu = () => false; +element3.oncontextmenu = () => false; + +element1.style.width = size; +element1.style.height = size; +element2.style.width = size; +element2.style.height = size; +element3.style.width = size; +element3.style.height = size; + +viewportGrid.appendChild(element1); +viewportGrid.appendChild(element2); +viewportGrid.appendChild(element3); + +content.appendChild(viewportGrid); + +const instructions = document.createElement('p'); +instructions.innerText = + 'Choose the level of thickness you want to view the volume in 3D.'; + +content.append(instructions); +// ============================= // + +let renderingEngine; + +const viewportIds = ['CT_AXIAL', 'CT_SAGITTAL', 'CT_OBLIQUE']; +let activeViewportId = viewportIds[0]; +let targetSlabThickness = 1; +let toolGroup; + +addDropDownToToolbar({ + id: 'viewportIdSelector', + options: { + defaultValue: activeViewportId, + values: viewportIds, + }, + labelText: 'Active Viewport to Change Slab Thickness', + onSelectedValueChange: (value) => { + activeViewportId = value as string; + toolGroup.setToolDisabled(ReferenceLines.toolName); + toolGroup.setToolConfiguration(ReferenceLines.toolName, { + sourceViewportId: activeViewportId, + }); + toolGroup.setToolEnabled(ReferenceLines.toolName); + renderingEngine.render(); + }, +}); + +/** + * - add button to change slab thickness dropdown + * - test on the other orientations + */ + +addDropDownToToolbar({ + id: 'slabThickness', + options: { + defaultValue: targetSlabThickness, + values: [1, 2.5, 3, 4.5, 5, 20], + }, + labelText: 'Slab Thickness', + onSelectedValueChange: (value) => { + targetSlabThickness = value as number; + }, +}); + +addButtonToToolbar({ + id: 'slabChange', + title: 'Apply', + onClick: () => { + const viewport = renderingEngine.getViewport(activeViewportId); + + viewport.setProperties({ slabThickness: targetSlabThickness }); + + // Todo: i think we should move this to set properties as well + viewport.setBlendMode(Enums.BlendModes.AVERAGE_INTENSITY_BLEND); + viewport.render(); + }, +}); + +addButtonToToolbar({ + title: 'Reset', + onClick: () => { + const viewport = renderingEngine.getViewport(activeViewportId); + + viewport.resetProperties(); + viewport.render(); + }, +}); + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + const toolGroupId = 'STACK_TOOL_GROUP_ID'; + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(WindowLevelTool); + cornerstoneTools.addTool(ReferenceLines); + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(ZoomTool); + cornerstoneTools.addTool(StackScrollMouseWheelTool); + + // Define a tool group, which defines how mouse events map to tool commands for + // Any viewport using the group + toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + // Add the tools to the tool group and specify which volume they are pointing at + toolGroup.addTool(WindowLevelTool.toolName, { volumeId }); + toolGroup.addTool(ReferenceLines.toolName, { volumeId }); + toolGroup.addTool(PanTool.toolName, { volumeId }); + toolGroup.addTool(ZoomTool.toolName, { volumeId }); + toolGroup.addTool(StackScrollMouseWheelTool.toolName); + + // Set the initial state of the tools, here we set one tool active on left click. + // This means left click will draw that tool. + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + toolGroup.setToolActive(PanTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, // Left Click + }, + ], + }); + + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Secondary, // Right Click + }, + ], + }); + toolGroup.setToolEnabled(ReferenceLines.toolName); + toolGroup.setToolConfiguration(ReferenceLines.toolName, { + sourceViewportId: 'CT_AXIAL', + }); + + // As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback` + // hook instead of mouse buttons, it does not need to assign any mouse button. + toolGroup.setToolActive(StackScrollMouseWheelTool.toolName); + + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + }); + + // Instantiate a rendering engine + const renderingEngineId = 'myRenderingEngine'; + renderingEngine = new RenderingEngine(renderingEngineId); + + // Create the viewports + + const viewportInputArray = [ + { + viewportId: viewportIds[0], + type: ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + background: [0.2, 0, 0.2], + }, + }, + { + viewportId: viewportIds[1], + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + background: [0.2, 0, 0.2], + }, + }, + { + viewportId: viewportIds[2], + type: ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + background: [0.2, 0, 0.2], + }, + }, + ]; + + renderingEngine.setViewports(viewportInputArray); + + // Set the tool group on the viewports + viewportIds.forEach((viewportId) => + toolGroup.addViewport(viewportId, renderingEngineId) + ); + + // Define a volume in memory + const volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds, + }); + + // Set the volume to load + volume.load(); + + setVolumesForViewports(renderingEngine, [{ volumeId }], viewportIds); + + // Render the image + renderingEngine.renderViewports(viewportIds); +} + +run(); diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 9f4c8c28e..5180950b5 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -122,6 +122,10 @@ "stackProperties": { "name": "Stack viewport default properties", "description": "Demonstrates how you can set per image properties for a stack viewport that acts as default values for that specific image" + }, + "volumeSlabScroll": { + "name": "Volume Slab Scroll", + "description": "Demonstrates how to use the slab scroll tool to scroll through a volume" } }, "tools-basic": { diff --git a/utils/demo/helpers/addDropdownToToolbar.ts b/utils/demo/helpers/addDropdownToToolbar.ts index b4d30da26..deb3aeb3a 100644 --- a/utils/demo/helpers/addDropdownToToolbar.ts +++ b/utils/demo/helpers/addDropdownToToolbar.ts @@ -4,16 +4,27 @@ export default function addDropDownToToolbar({ container, style, onSelectedValueChange, + labelText, }: { id?: string; options: { values: number[] | string[]; defaultValue: number | string }; container?: HTMLElement; style?: Record; onSelectedValueChange: (value: number | string) => void; + labelText?: string; }) { const { values, defaultValue } = options; - const select = document.createElement('select'); + container = container ?? document.getElementById('demo-toolbar'); + // Create label element if labelText is provided + if (labelText) { + const label = document.createElement('label'); + label.htmlFor = id; + label.innerText = labelText; + container.append(label); + } + + const select = document.createElement('select'); select.id = id; if (style) { @@ -22,25 +33,20 @@ export default function addDropDownToToolbar({ values.forEach((value) => { const optionElement = document.createElement('option'); - optionElement.value = String(value); optionElement.innerText = String(value); - if (value === defaultValue) { optionElement.selected = true; } - select.append(optionElement); }); select.onchange = (evt) => { const selectElement = evt.target; - if (selectElement) { onSelectedValueChange(selectElement.value); } }; - container = container ?? document.getElementById('demo-toolbar'); container.append(select); }