diff --git a/.gitignore b/.gitignore index ce920dc87..874edf8fe 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ build .env.development.local .env.test.local .env.production.local +.yarn npm-debug.log* yarn-debug.log* diff --git a/packages/cornerstone-render/src/RenderingEngine/Viewport.ts b/packages/cornerstone-render/src/RenderingEngine/Viewport.ts index ac3219f3f..95ae9b6db 100644 --- a/packages/cornerstone-render/src/RenderingEngine/Viewport.ts +++ b/packages/cornerstone-render/src/RenderingEngine/Viewport.ts @@ -90,6 +90,10 @@ class Viewport { public getRenderer() { const renderingEngine = this.getRenderingEngine() + if (!renderingEngine || renderingEngine.hasBeenDestroyed) { + throw new Error('Rendering engine has been destroyed') + } + return renderingEngine.offscreenMultiRenderWindow.getRenderer(this.uid) } @@ -318,6 +322,15 @@ class Viewport { public addActor(actorEntry: ActorEntry): void { const { uid: actorUID, volumeActor } = actorEntry + const renderingEngine = this.getRenderingEngine() + + if (!renderingEngine || renderingEngine.hasBeenDestroyed) { + console.warn( + `Cannot add actor UID of ${actorUID} Rendering Engine has been destroyed` + ) + return + } + if (!actorUID || !volumeActor) { throw new Error('Actors should have uid and vtk volumeActor properties') } diff --git a/packages/cornerstone-render/src/RenderingEngine/VolumeViewport.ts b/packages/cornerstone-render/src/RenderingEngine/VolumeViewport.ts index 7bde4dd3a..20e406140 100644 --- a/packages/cornerstone-render/src/RenderingEngine/VolumeViewport.ts +++ b/packages/cornerstone-render/src/RenderingEngine/VolumeViewport.ts @@ -148,14 +148,20 @@ class VolumeViewport extends Viewport { // One actor per volume for (let i = 0; i < volumeInputArray.length; i++) { - const { volumeUID, visibility } = volumeInputArray[i] + const { volumeUID, visibility, actorUID } = volumeInputArray[i] const volumeActor = await createVolumeActor(volumeInputArray[i]) if (visibility === false) { volumeActor.setVisibility(false) } - volumeActors.push({ uid: volumeUID, volumeActor }) + // We cannot use only volumeUID since then we cannot have for instance more + // than one representation of the same volume (since actors would have the + // same name, and we don't allow that) AND We cannot use only any uid, since + // we rely on the volume in the cache for mapper. So we prefer actorUID if + // it is defined, otherwise we use volumeUID for the actor name. + const uid = actorUID || volumeUID + volumeActors.push({ uid, volumeActor }) } this.addActors(volumeActors) @@ -168,11 +174,13 @@ class VolumeViewport extends Viewport { /** * It removes the volume actor from the Viewport. If the volume actor is not in * the viewport, it does nothing. - * @param volumeUIDs Array of volume UIDs to remove + * @param actorUIDs Array of actor UIDs to remove. In case of simple volume it will + * be the volume UID, but in caase of Segmentation it will be `{volumeUID}-{representationType}` + * since the same volume can be rendered in multiple representations. * @param immediate If true, the Viewport will be rendered immediately */ - public removeVolumes(volumeUIDs: Array, immediate = false): void { - this.removeActors(volumeUIDs) + public removeVolumeActors(actorUIDs: Array, immediate = false): void { + this.removeActors(actorUIDs) if (immediate) { this.render() diff --git a/packages/cornerstone-render/src/RenderingEngine/getRenderingEngine.ts b/packages/cornerstone-render/src/RenderingEngine/getRenderingEngine.ts index 52a874ccf..940a98ca6 100644 --- a/packages/cornerstone-render/src/RenderingEngine/getRenderingEngine.ts +++ b/packages/cornerstone-render/src/RenderingEngine/getRenderingEngine.ts @@ -1,5 +1,6 @@ import renderingEngineCache from './renderingEngineCache' import RenderingEngine from './RenderingEngine' +import { IViewport } from '../types' /** * Method to retrieve a RenderingEngine by its unique identifier. @@ -20,10 +21,6 @@ import RenderingEngine from './RenderingEngine' * @public */ export function getRenderingEngine(uid: string): RenderingEngine | undefined { - // if (!uid) { - // return renderingEngineCache.getAll() - // } - return renderingEngineCache.get(uid) } diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeMapper.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeMapper.ts index 7c755682f..8fb6bb4f8 100644 --- a/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeMapper.ts +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeMapper.ts @@ -1,5 +1,3 @@ -import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper' - import { vtkSharedVolumeMapper } from '../vtkClasses' export default function createVolumeMapper( diff --git a/packages/cornerstone-render/src/getEnabledElement.ts b/packages/cornerstone-render/src/getEnabledElement.ts index dba6cc3d5..d82c7d4ae 100644 --- a/packages/cornerstone-render/src/getEnabledElement.ts +++ b/packages/cornerstone-render/src/getEnabledElement.ts @@ -36,9 +36,15 @@ export default function getEnabledElement( return } - const { viewportUid: viewportUID, renderingEngineUid: renderingEngineUID } = - element.dataset + const { viewportUid, renderingEngineUid } = element.dataset + return getEnabledElementByUIDs(renderingEngineUid, viewportUid) +} + +export function getEnabledElementByUIDs( + renderingEngineUID: string, + viewportUID: string +): IEnabledElement { if (!renderingEngineUID || !viewportUID) { return } diff --git a/packages/cornerstone-render/src/index.ts b/packages/cornerstone-render/src/index.ts index 4171c1d4d..55605a7f3 100644 --- a/packages/cornerstone-render/src/index.ts +++ b/packages/cornerstone-render/src/index.ts @@ -47,7 +47,7 @@ import { registerVolumeLoader, registerUnknownVolumeLoader, } from './volumeLoader' -import getEnabledElement from './getEnabledElement' +import getEnabledElement, { getEnabledElementByUIDs } from './getEnabledElement' import configuration from './configuration' import metaData from './metaData' import { @@ -114,6 +114,7 @@ export { cache, Cache, getEnabledElement, + getEnabledElementByUIDs, renderToCanvas, // eventTarget, diff --git a/packages/cornerstone-render/src/types/CustomEventType.ts b/packages/cornerstone-render/src/types/CustomEventType.ts new file mode 100644 index 000000000..f1605b593 --- /dev/null +++ b/packages/cornerstone-render/src/types/CustomEventType.ts @@ -0,0 +1,14 @@ +interface CustomEvent extends Event { + /** + * Returns any custom data event was created with. Typically used for synthetic events. + */ + readonly detail: T + initCustomEvent( + typeArg: string, + canBubbleArg: boolean, + cancelableArg: boolean, + detailArg: T + ): void +} + +export default CustomEvent diff --git a/packages/cornerstone-render/src/types/IViewport.ts b/packages/cornerstone-render/src/types/IViewport.ts index d7aec4b53..3e20de529 100644 --- a/packages/cornerstone-render/src/types/IViewport.ts +++ b/packages/cornerstone-render/src/types/IViewport.ts @@ -9,6 +9,7 @@ interface IViewport { renderingEngineUID: string type: string canvas: HTMLCanvasElement + element: HTMLElement sx: number sy: number sWidth: number diff --git a/packages/cornerstone-render/src/types/IVolumeInput.ts b/packages/cornerstone-render/src/types/IVolumeInput.ts index 81b46d379..47952a0e0 100644 --- a/packages/cornerstone-render/src/types/IVolumeInput.ts +++ b/packages/cornerstone-render/src/types/IVolumeInput.ts @@ -7,6 +7,9 @@ type VolumeInputCallback = (params: { type IVolumeInput = { volumeUID: string + // actorUID for segmentations, since two segmentations with the same volumeUID + // can have different represetnations + actorUID?: string visibility?: boolean callback?: VolumeInputCallback blendMode?: string diff --git a/packages/cornerstone-render/src/types/index.ts b/packages/cornerstone-render/src/types/index.ts index 8e8531710..975881717 100644 --- a/packages/cornerstone-render/src/types/index.ts +++ b/packages/cornerstone-render/src/types/index.ts @@ -10,6 +10,7 @@ import type IImageVolume from './IImageVolume' import type VolumeLoaderFn from './VolumeLoaderFn' import type IRegisterImageLoader from './IRegisterImageLoader' import type IStreamingVolume from './IStreamingVolume' +import type CustomEventType from './CustomEventType' import type { IViewport, ViewportInput, @@ -106,4 +107,6 @@ export type { CPUFallbackLookupTable, CPUFallbackLUT, CPUFallbackRenderingTools, + // + CustomEventType, } diff --git a/packages/cornerstone-render/src/utilities/testUtils.js b/packages/cornerstone-render/src/utilities/testUtils.js index f058bb7de..0d39b1343 100644 --- a/packages/cornerstone-render/src/utilities/testUtils.js +++ b/packages/cornerstone-render/src/utilities/testUtils.js @@ -1,8 +1,8 @@ import resemble from 'resemblejs' -import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData' -import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray' -import { ImageVolume } from '../index' -import { getOrCreateCanvas } from '../RenderingEngine' + +import { fakeImageLoader, fakeMetaDataProvider } from './testUtilsImageLoader' +import { fakeVolumeLoader } from './testUtilsVolumeLoader' +import { createNormalizedMouseEvent } from './testUtilsMouseEvents' // 10 slice, 10 colors const colors = [ @@ -20,241 +20,6 @@ const colors = [ Object.freeze(colors) -const imageIds = [ - 'fakeSharedBufferImageLoader:imageId1', - 'fakeSharedBufferImageLoader:imageId2', - 'fakeSharedBufferImageLoader:imageId3', - 'fakeSharedBufferImageLoader:imageId4', - 'fakeSharedBufferImageLoader:imageId5', -] - -function makeTestImage1(rows, columns, barStart, barWidth) { - const pixelData = new Uint8Array(rows * columns) - - for (let i = 0; i < rows; i++) { - for (let j = barStart; j < barStart + barWidth; j++) { - pixelData[i * columns + j] = 255 - } - } - - return pixelData -} - -function makeTestRGB(rows, columns, barStart, barWidth) { - let start = barStart - - const pixelData = new Uint8Array(rows * columns * 3) - - colors.forEach((color) => { - for (let i = 0; i < rows; i++) { - for (let j = start; j < start + barWidth; j++) { - pixelData[(i * columns + j) * 3] = color[0] - pixelData[(i * columns + j) * 3 + 1] = color[1] - pixelData[(i * columns + j) * 3 + 2] = color[2] - } - } - - start += barWidth - }) - - return pixelData -} - -/** - * It creates an image based on the imageId name. It splits the imageId - * based on "_" and deciphers each field of rows, columns, barStart, barWidth, x_spacing, y_spacing, rgb - * fakeLoader: myImage_64_64_10_20_1_1_0 will create a grayscale test image of size 64 by - * 64 and with a vertical bar which starts at 10th pixel and span 20 pixels - * width, with pixel spacing of 1 mm and 1 mm in x and y direction. - * @param {imageId} imageId - * @returns - */ -const fakeImageLoader = (imageId) => { - const imageURI = imageId.split(':')[1] - const [_, rows, columns, barStart, barWidth, x_spacing, y_spacing, rgb, PT] = - imageURI.split('_').map((v) => parseFloat(v)) - - let pixelData - - if (rgb) { - pixelData = makeTestRGB(rows, columns, barStart, barWidth) - } else { - pixelData = makeTestImage1(rows, columns, barStart, barWidth) - } - - // Todo: separated fakeImageLoader for cpu and gpu - const image = { - rows, - columns, - width: columns, - height: rows, - imageId, - intercept: 0, - slope: 1, - invert: false, - windowCenter: 40, - windowWidth: 400, - maxPixelValue: 255, - minPixelValue: 0, - rowPixelSpacing: 1, - columnPixelSpacing: 1, - getPixelData: () => pixelData, - sizeInBytes: rows * columns * 1, // 1 byte for now - FrameOfReferenceUID: 'Stack_Frame_Of_Reference', - } - - return { - promise: Promise.resolve(image), - } -} - -function fakeMetaDataProvider(type, imageId) { - const imageURI = imageId.split(':')[1] - const [_, rows, columns, barStart, barWidth, x_spacing, y_spacing, rgb, PT] = - imageURI.split('_').map((v) => parseFloat(v)) - - const modality = PT ? 'PT' : 'MR' - const photometricInterpretation = rgb ? 'RGB' : 'MONOCHROME2' - if (type === 'imagePixelModule') { - const imagePixelModule = { - photometricInterpretation, - rows, - columns, - samplesPerPixel: rgb ? 3 : 1, - bitsAllocated: rgb ? 24 : 8, - bitsStored: rgb ? 24 : 8, - highBit: rgb ? 24 : 8, - pixelRepresentation: 0, - } - - return imagePixelModule - } else if (type === 'generalSeriesModule') { - const generalSeriesModule = { - modality: modality, - } - return generalSeriesModule - } else if (type === 'scalingModule') { - const scalingModule = { - suvbw: 100, - suvlbm: 100, - suvbsa: 100, - } - return scalingModule - } else if (type === 'imagePlaneModule') { - const imagePlaneModule = { - rows, - columns, - width: rows, - heigth: columns, - imageOrientationPatient: [1, 0, 0, 0, 1, 0], - rowCosines: [1, 0, 0], - columnCosines: [0, 1, 0], - imagePositionPatient: [0, 0, 0], - pixelSpacing: [x_spacing, y_spacing], - rowPixelSpacing: x_spacing, - columnPixelSpacing: y_spacing, - } - - return imagePlaneModule - } else if (type === 'voiLutModule') { - return { - windowWidth: undefined, - windowCenter: undefined, - } - } else if (type === 'modalityLutModule') { - return { - rescaleSlope: undefined, - rescaleIntercept: undefined, - } - } -} - -const fakeVolumeLoader = (volumeId) => { - const volumeURI = volumeId.split(':')[1] - const [_, rows, columns, slices, x_spacing, y_spacing, z_spacing, rgb] = - volumeURI.split('_').map((v) => parseFloat(v)) - - const dimensions = [rows, columns, slices] - - const photometricInterpretation = rgb ? 'RGB' : 'MONOCHROME2' - - const volumeMetadata = { - BitsAllocated: rgb ? 24 : 8, - BitsStored: rgb ? 24 : 8, - SamplesPerPixel: rgb ? 3 : 1, - HighBit: rgb ? 24 : 8, - PixelRepresentation: 0, - PhotometricInterpretation: photometricInterpretation, - FrameOfReferenceUID: 'Volume_Frame_Of_Reference', - ImageOrientationPatient: [1, 0, 0, 0, 1, 0], - PixelSpacing: [x_spacing, y_spacing, z_spacing], - Columns: columns, - Rows: rows, - } - - const yMultiple = rows - const zMultiple = rows * columns - - let barStart = 0 - const barWidth = Math.floor(rows / slices) - let pixelData, index - - if (!rgb) { - pixelData = new Uint8Array(rows * columns * slices) - for (let z = 0; z < slices; z++) { - for (let i = 0; i < rows; i++) { - for (let j = barStart; j < barStart + barWidth; j++) { - pixelData[z * zMultiple + i * yMultiple + j] = 255 - } - } - barStart += barWidth - } - } else { - pixelData = new Uint8Array(rows * columns * slices * 3) - - for (let z = 0; z < slices; z++) { - for (let i = 0; i < rows; i++) { - for (let j = 0; j < columns; j++) { - index = z * zMultiple + i * yMultiple + j - pixelData[index * 3] = colors[z][0] - pixelData[index * 3 + 1] = colors[z][1] - pixelData[index * 3 + 2] = colors[z][2] - } - } - } - } - - const scalarArray = vtkDataArray.newInstance({ - name: 'Pixels', - numberOfComponents: rgb ? 3 : 1, - values: pixelData, - }) - - const imageData = vtkImageData.newInstance() - imageData.setDimensions(dimensions) - imageData.setSpacing([1, 1, 1]) - imageData.setDirection([1, 0, 0, 0, 1, 0, 0, 0, 1]) - imageData.setOrigin([0, 0, 0]) - imageData.getPointData().setScalars(scalarArray) - - const imageVolume = new ImageVolume({ - uid: volumeId, - metadata: volumeMetadata, - dimensions: dimensions, - spacing: [1, 1, 1], - origin: [0, 0, 0], - direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], - scalarData: pixelData, - sizeInBytes: pixelData.byteLength, - imageData: imageData, - imageIds: [], - }) - - return { - promise: Promise.resolve(imageVolume), - } -} - function downloadURI(uri, name) { const link = document.createElement('a') @@ -287,7 +52,9 @@ function compareImages(imageDataURL, baseline, outputName) { // If the error is greater than 1%, fail the test // and download the difference image if (mismatch > 1) { - console.log(mismatch) + console.log(imageDataURL) + + console.log('mismatch of ' + mismatch + '%') const diff = data.getImageDataUrl() //downloadURI(diff, outputName) @@ -301,53 +68,16 @@ function compareImages(imageDataURL, baseline, outputName) { }) } -function canvasPointsToPagePoints(DomCanvasElement, canvasPoint) { - const rect = DomCanvasElement.getBoundingClientRect() - return [ - canvasPoint[0] + rect.left + window.pageXOffset, - canvasPoint[1] + rect.top + window.pageYOffset, - ] -} - -/** - * This function uses the imageData being displayed on the viewport and - * an index (IJK) on the image to normalize the mouse event details. - * It should be noted that the normalization is required since client and page XY - * cannot accept a double. Therefore, for the requested index, canvas coordinate - * will get calculated and normalized (rounded) to enable normalized client/page XY - * - * @param {vtkImageData} imageData - * @param {[number, number,number]} index IJK index of the point to click - * @param {HTMLCanvasElement} canvas the canvas to be clicked on - * @param {StackViewport|VolumeViewport} viewport - * @returns pageX, pageY, clientX, clientY, worldCoordinate - */ -function createNormalizedMouseEvent(imageData, index, element, viewport) { - const canvas = getOrCreateCanvas(element) - const tempWorld1 = imageData.indexToWorld(index) - const tempCanvasPoint1 = viewport.worldToCanvas(tempWorld1) - const canvasPoint1 = tempCanvasPoint1.map((p) => Math.round(p)) - const [pageX, pageY] = canvasPointsToPagePoints(canvas, canvasPoint1) - const worldCoord = viewport.canvasToWorld(canvasPoint1) - - return { - pageX, - pageY, - clientX: pageX, - clientY: pageY, - worldCoord, - } -} - const testUtils = { - makeTestImage1, fakeImageLoader, - fakeVolumeLoader, fakeMetaDataProvider, + fakeVolumeLoader, compareImages, - downloadURI, createNormalizedMouseEvent, - canvasPointsToPagePoints, + // utils + downloadURI, + colors, } export default testUtils +export { colors } diff --git a/packages/cornerstone-render/src/utilities/testUtilsImageLoader.js b/packages/cornerstone-render/src/utilities/testUtilsImageLoader.js new file mode 100644 index 000000000..acd773e78 --- /dev/null +++ b/packages/cornerstone-render/src/utilities/testUtilsImageLoader.js @@ -0,0 +1,115 @@ +import { + getVerticalBarImage, + getVerticalBarRGBImage, +} from './testUtilsPixelData' + +/** + * It creates an image based on the imageId name. It splits the imageId + * based on "_" and deciphers each field of rows, columns, barStart, barWidth, x_spacing, y_spacing, rgb + * fakeLoader: myImage_64_64_10_20_1_1_0 will create a grayscale test image of size 64 by + * 64 and with a vertical bar which starts at 10th pixel and span 20 pixels + * width, with pixel spacing of 1 mm and 1 mm in x and y direction. + * @param {imageId} imageId + * @returns + */ +const fakeImageLoader = (imageId) => { + const imageURI = imageId.split(':')[1] + const [_, rows, columns, barStart, barWidth, x_spacing, y_spacing, rgb, PT] = + imageURI.split('_').map((v) => parseFloat(v)) + + let pixelData + + if (rgb) { + pixelData = getVerticalBarRGBImage(rows, columns, barStart, barWidth) + } else { + pixelData = getVerticalBarImage(rows, columns, barStart, barWidth) + } + + // Todo: separated fakeImageLoader for cpu and gpu + const image = { + rows, + columns, + width: columns, + height: rows, + imageId, + intercept: 0, + slope: 1, + invert: false, + windowCenter: 40, + windowWidth: 400, + maxPixelValue: 255, + minPixelValue: 0, + rowPixelSpacing: 1, + columnPixelSpacing: 1, + getPixelData: () => pixelData, + sizeInBytes: rows * columns * 1, // 1 byte for now + FrameOfReferenceUID: 'Stack_Frame_Of_Reference', + } + + return { + promise: Promise.resolve(image), + } +} + +function fakeMetaDataProvider(type, imageId) { + const imageURI = imageId.split(':')[1] + const [_, rows, columns, barStart, barWidth, x_spacing, y_spacing, rgb, PT] = + imageURI.split('_').map((v) => parseFloat(v)) + + const modality = PT ? 'PT' : 'MR' + const photometricInterpretation = rgb ? 'RGB' : 'MONOCHROME2' + if (type === 'imagePixelModule') { + const imagePixelModule = { + photometricInterpretation, + rows, + columns, + samplesPerPixel: rgb ? 3 : 1, + bitsAllocated: rgb ? 24 : 8, + bitsStored: rgb ? 24 : 8, + highBit: rgb ? 24 : 8, + pixelRepresentation: 0, + } + + return imagePixelModule + } else if (type === 'generalSeriesModule') { + const generalSeriesModule = { + modality: modality, + } + return generalSeriesModule + } else if (type === 'scalingModule') { + const scalingModule = { + suvbw: 100, + suvlbm: 100, + suvbsa: 100, + } + return scalingModule + } else if (type === 'imagePlaneModule') { + const imagePlaneModule = { + rows, + columns, + width: rows, + height: columns, + imageOrientationPatient: [1, 0, 0, 0, 1, 0], + rowCosines: [1, 0, 0], + columnCosines: [0, 1, 0], + imagePositionPatient: [0, 0, 0], + pixelSpacing: [x_spacing, y_spacing], + rowPixelSpacing: x_spacing, + columnPixelSpacing: y_spacing, + } + + return imagePlaneModule + } else if (type === 'voiLutModule') { + return { + windowWidth: undefined, + windowCenter: undefined, + } + } else if (type === 'modalityLutModule') { + return { + rescaleSlope: undefined, + rescaleIntercept: undefined, + } + } +} + +export { fakeImageLoader, fakeMetaDataProvider } diff --git a/packages/cornerstone-render/src/utilities/testUtilsMouseEvents.js b/packages/cornerstone-render/src/utilities/testUtilsMouseEvents.js new file mode 100644 index 000000000..2da57e773 --- /dev/null +++ b/packages/cornerstone-render/src/utilities/testUtilsMouseEvents.js @@ -0,0 +1,41 @@ +import { getOrCreateCanvas } from '../RenderingEngine' + +function canvasPointsToPagePoints(DomCanvasElement, canvasPoint) { + const rect = DomCanvasElement.getBoundingClientRect() + return [ + canvasPoint[0] + rect.left + window.pageXOffset, + canvasPoint[1] + rect.top + window.pageYOffset, + ] +} + +/** + * This function uses the imageData being displayed on the viewport and + * an index (IJK) on the image to normalize the mouse event details. + * It should be noted that the normalization is required since client and page XY + * cannot accept a double. Therefore, for the requested index, canvas coordinate + * will get calculated and normalized (rounded) to enable normalized client/page XY + * + * @param {vtkImageData} imageData + * @param {[number, number,number]} index IJK index of the point to click + * @param {HTMLCanvasElement} canvas the canvas to be clicked on + * @param {StackViewport|VolumeViewport} viewport + * @returns pageX, pageY, clientX, clientY, worldCoordinate + */ +function createNormalizedMouseEvent(imageData, index, element, viewport) { + const canvas = getOrCreateCanvas(element) + const tempWorld1 = imageData.indexToWorld(index) + const tempCanvasPoint1 = viewport.worldToCanvas(tempWorld1) + const canvasPoint1 = tempCanvasPoint1.map((p) => Math.round(p)) + const [pageX, pageY] = canvasPointsToPagePoints(canvas, canvasPoint1) + const worldCoord = viewport.canvasToWorld(canvasPoint1) + + return { + pageX, + pageY, + clientX: pageX, + clientY: pageY, + worldCoord, + } +} + +export { createNormalizedMouseEvent } diff --git a/packages/cornerstone-render/src/utilities/testUtilsPixelData.js b/packages/cornerstone-render/src/utilities/testUtilsPixelData.js new file mode 100644 index 000000000..01afe3028 --- /dev/null +++ b/packages/cornerstone-render/src/utilities/testUtilsPixelData.js @@ -0,0 +1,124 @@ +import { colors } from './testUtils' + +function getVerticalBarImage(rows, columns, barStart, barWidth) { + const pixelData = new Uint8Array(rows * columns) + + for (let i = 0; i < rows; i++) { + for (let j = barStart; j < barStart + barWidth; j++) { + pixelData[i * columns + j] = 255 + } + } + + return pixelData +} + +function getVerticalBarRGBImage(rows, columns, barStart, barWidth) { + let start = barStart + + const pixelData = new Uint8Array(rows * columns * 3) + + colors.forEach((color) => { + for (let i = 0; i < rows; i++) { + for (let j = start; j < start + barWidth; j++) { + pixelData[(i * columns + j) * 3] = color[0] + pixelData[(i * columns + j) * 3 + 1] = color[1] + pixelData[(i * columns + j) * 3 + 2] = color[2] + } + } + + start += barWidth + }) + + return pixelData +} + +function getExactRegionVolume( + rows, + columns, + slices, + start_X, + start_Y, + start_Z, + end_X, + end_Y, + end_Z, + valueForSegmentIndex +) { + let value = valueForSegmentIndex + + if (!value) { + value = 1 + } + + const yMultiple = rows + const zMultiple = rows * columns + + // from [start_x, start_y, start_z] to [end_x, end_y, end_z] + // create all the indices that are in the region of interest + const indices = [] + for (let z = start_Z; z < end_Z; z++) { + for (let y = start_Y; y < end_Y; y++) { + for (let x = start_X; x < end_X; x++) { + indices.push([x, y, z]) + } + } + } + + let pixelData + pixelData = new Uint8Array(rows * columns * slices) + + for (const index of indices) { + const [x, y, z] = index + pixelData[z * zMultiple + y * yMultiple + x] = value + } + + return pixelData +} + +function getVerticalBarVolume(rows, columns, slices) { + const yMultiple = rows + const zMultiple = rows * columns + + let barStart = 0 + const barWidth = Math.floor(rows / slices) + + let pixelData + pixelData = new Uint8Array(rows * columns * slices) + for (let z = 0; z < slices; z++) { + for (let i = 0; i < rows; i++) { + for (let j = barStart; j < barStart + barWidth; j++) { + pixelData[z * zMultiple + i * yMultiple + j] = 255 + } + } + barStart += barWidth + } + + return pixelData +} + +function getVerticalBarRGBVolume(rows, columns, slices) { + let index, pixelData + const yMultiple = rows + const zMultiple = rows * columns + pixelData = new Uint8Array(rows * columns * slices * 3) + for (let z = 0; z < slices; z++) { + for (let i = 0; i < rows; i++) { + for (let j = 0; j < columns; j++) { + index = z * zMultiple + i * yMultiple + j + pixelData[index * 3] = colors[z][0] + pixelData[index * 3 + 1] = colors[z][1] + pixelData[index * 3 + 2] = colors[z][2] + } + } + } + + return pixelData +} + +export { + getVerticalBarImage, + getVerticalBarVolume, + getVerticalBarRGBImage, + getVerticalBarRGBVolume, + getExactRegionVolume, +} diff --git a/packages/cornerstone-render/src/utilities/testUtilsVolumeLoader.js b/packages/cornerstone-render/src/utilities/testUtilsVolumeLoader.js new file mode 100644 index 000000000..ca62025f7 --- /dev/null +++ b/packages/cornerstone-render/src/utilities/testUtilsVolumeLoader.js @@ -0,0 +1,107 @@ +import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray' +import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData' +import { ImageVolume } from '../index' +import { + getVerticalBarRGBVolume, + getVerticalBarVolume, + getExactRegionVolume, +} from './testUtilsPixelData' + +const fakeVolumeLoader = (volumeId) => { + const volumeURI = volumeId.split(':')[1] + const uriName = volumeURI.split('_')[0] + const [ + _, + rows, + columns, + slices, + x_spacing, + y_spacing, + z_spacing, + rgb, + startX, + startY, + startZ, + endX, + endY, + endZ, + valueForSegmentIndex, + ] = volumeURI.split('_').map((v) => parseFloat(v)) + + // If uri name is volumeURIExact, it means that the metadata provided + // has the start and end indices of the region of interest. + let useExactRegion = false + if (uriName === 'volumeURIExact') { + useExactRegion = true + } + + const dimensions = [rows, columns, slices] + + const photometricInterpretation = rgb ? 'RGB' : 'MONOCHROME2' + + const volumeMetadata = { + BitsAllocated: rgb ? 24 : 8, + BitsStored: rgb ? 24 : 8, + SamplesPerPixel: rgb ? 3 : 1, + HighBit: rgb ? 24 : 8, + PixelRepresentation: 0, + PhotometricInterpretation: photometricInterpretation, + FrameOfReferenceUID: 'Volume_Frame_Of_Reference', + ImageOrientationPatient: [1, 0, 0, 0, 1, 0], + PixelSpacing: [x_spacing, y_spacing, z_spacing], + Columns: columns, + Rows: rows, + } + + let pixelData + if (rgb) { + pixelData = getVerticalBarRGBVolume(rows, columns, slices) + } else if (useExactRegion) { + pixelData = getExactRegionVolume( + rows, + columns, + slices, + startX, + startY, + startZ, + endX, + endY, + endZ, + valueForSegmentIndex + ) + } else { + pixelData = getVerticalBarVolume(rows, columns, slices) + } + + const scalarArray = vtkDataArray.newInstance({ + name: 'Pixels', + numberOfComponents: rgb ? 3 : 1, + values: pixelData, + }) + + const imageData = vtkImageData.newInstance() + imageData.setDimensions(dimensions) + imageData.setSpacing([1, 1, 1]) + imageData.setDirection([1, 0, 0, 0, 1, 0, 0, 0, 1]) + imageData.setOrigin([0, 0, 0]) + imageData.getPointData().setScalars(scalarArray) + + const imageVolume = new ImageVolume({ + uid: volumeId, + metadata: volumeMetadata, + dimensions: dimensions, + spacing: [1, 1, 1], + origin: [0, 0, 0], + direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], + scalarData: pixelData, + sizeInBytes: pixelData.byteLength, + imageData: imageData, + imageIds: [], + }) + + return { + promise: Promise.resolve(imageVolume), + } +} + +export { fakeVolumeLoader } diff --git a/packages/cornerstone-render/src/volumeLoader.ts b/packages/cornerstone-render/src/volumeLoader.ts index 5193d5ad5..56e748933 100644 --- a/packages/cornerstone-render/src/volumeLoader.ts +++ b/packages/cornerstone-render/src/volumeLoader.ts @@ -163,7 +163,7 @@ export function loadVolume( * @returns {Types.VolumeLoadObject} Volume Loader Object * @category VolumeLoader */ -export function createAndCacheVolume( +export async function createAndCacheVolume( volumeId: string, options: VolumeLoaderOptions ): Promise> { diff --git a/packages/cornerstone-tools/src/cursors/SVGCursorDescriptor.ts b/packages/cornerstone-tools/src/cursors/SVGCursorDescriptor.ts index 044708be9..f2ea1aff8 100644 --- a/packages/cornerstone-tools/src/cursors/SVGCursorDescriptor.ts +++ b/packages/cornerstone-tools/src/cursors/SVGCursorDescriptor.ts @@ -380,47 +380,47 @@ const DefinedDescriptorsMap = { }), // Default Rectangle Scissors - RectangleScissors: extend(BASE, { + RectangleScissor: extend(BASE, { iconContent: `${RECTANGLE_ICON} ${PLUS_RECT}`, viewBox: SEGMENTATION_CURSOR_BOUNDARIES, }), - 'RectangleScissors.FILL_INSIDE': extend(BASE, { + 'RectangleScissor.FILL_INSIDE': extend(BASE, { iconContent: `${RECTANGLE_ICON} ${PLUS_RECT}`, viewBox: SEGMENTATION_CURSOR_BOUNDARIES, }), - 'RectangleScissors.FILL_OUTSIDE': extend(BASE, { + 'RectangleScissor.FILL_OUTSIDE': extend(BASE, { iconContent: `${RECTANGLE_ICON} ${PLUS_RECT}`, viewBox: SEGMENTATION_CURSOR_BOUNDARIES, }), - 'RectangleScissors.ERASE_OUTSIDE': extend(BASE, { + 'RectangleScissor.ERASE_OUTSIDE': extend(BASE, { iconContent: `${RECTANGLE_ICON} ${MINUS_RECT}`, viewBox: SEGMENTATION_CURSOR_BOUNDARIES, }), - 'RectangleScissors.ERASE_INSIDE': extend(BASE, { + 'RectangleScissor.ERASE_INSIDE': extend(BASE, { iconContent: `${RECTANGLE_ICON} ${MINUS_RECT}`, viewBox: SEGMENTATION_CURSOR_BOUNDARIES, }), - CircleScissors: extend(BASE, { + CircleScissor: extend(BASE, { iconContent: `${CIRCLE_ICON} ${PLUS_RECT}`, viewBox: SEGMENTATION_CURSOR_BOUNDARIES, }), - 'CircleScissors.FILL_INSIDE': extend(BASE, { + 'CircleScissor.FILL_INSIDE': extend(BASE, { iconContent: `${CIRCLE_ICON} ${PLUS_RECT}`, viewBox: SEGMENTATION_CURSOR_BOUNDARIES, }), - 'CircleScissors.ERASE_OUTSIDE': extend(BASE, { + 'CircleScissor.ERASE_OUTSIDE': extend(BASE, { iconContent: `${CIRCLE_ICON} ${MINUS_RECT}`, viewBox: SEGMENTATION_CURSOR_BOUNDARIES, }), - 'CircleScissors.FILL_OUTSIDE': extend(BASE, { + 'CircleScissor.FILL_OUTSIDE': extend(BASE, { iconContent: `${CIRCLE_ICON} ${PLUS_RECT}`, viewBox: SEGMENTATION_CURSOR_BOUNDARIES, }), diff --git a/packages/cornerstone-tools/src/cursors/SVGMouseCursor.ts b/packages/cornerstone-tools/src/cursors/SVGMouseCursor.ts index ec3781551..3dccadee3 100644 --- a/packages/cornerstone-tools/src/cursors/SVGMouseCursor.ts +++ b/packages/cornerstone-tools/src/cursors/SVGMouseCursor.ts @@ -1,5 +1,5 @@ import { ToolModes, ToolDataStates } from '../enums' -import { getDefaultStyleProperty } from '../stateManagement/toolStyle' +import { getDefaultStyleProperty } from '../stateManagement/annotation/toolStyle' import MouseCursor from './MouseCursor' import ImageMouseCursor from './ImageMouseCursor' import { getDefinedSVGCursorDescriptor } from './SVGCursorDescriptor' diff --git a/packages/cornerstone-tools/src/drawingSvg/getSvgDrawingHelper.ts b/packages/cornerstone-tools/src/drawingSvg/getSvgDrawingHelper.ts index 2f543dfa4..d5b99004d 100644 --- a/packages/cornerstone-tools/src/drawingSvg/getSvgDrawingHelper.ts +++ b/packages/cornerstone-tools/src/drawingSvg/getSvgDrawingHelper.ts @@ -88,7 +88,6 @@ function clearUntouched(svgLayerElement, canvasHash) { const cacheEntry = state.svgNodeCache[canvasHash][cacheKey] if (!cacheEntry.touched && cacheEntry.domRef) { - // console.log(`Removing: ${svgNodeHash}`) svgLayerElement.removeChild(cacheEntry.domRef) delete state.svgNodeCache[canvasHash][cacheKey] } diff --git a/packages/cornerstone-tools/src/enums/CornerstoneTools3DEvents.ts b/packages/cornerstone-tools/src/enums/CornerstoneTools3DEvents.ts index ad7f46e1b..5b2c8ed19 100644 --- a/packages/cornerstone-tools/src/enums/CornerstoneTools3DEvents.ts +++ b/packages/cornerstone-tools/src/enums/CornerstoneTools3DEvents.ts @@ -15,14 +15,24 @@ * @enum {String} * @readonly */ +// Todo: add documentation. I'll add it after we finalize the API. enum CornerstoneTools3DEvents { // - // Labelmaps + // segmentation display // - LABELMAP_STATE_UPDATED = 'cornerstonetoolslabelmapstateupdated', - - // SEGMENTATION - LABELMAP_REMOVED = 'cornerstonetoolslabelmapremoved', + SEGMENTATION_RENDERED = 'cornerstonetoolssegmentationrendered', + // + // segmentation state + // + SEGMENTATION_STATE_MODIFIED = 'cornerstonetoolssegmentationstatemodified', + // + // segmentation global state + // + SEGMENTATION_GLOBAL_STATE_MODIFIED = 'cornerstonetoolssegmentationglobalstatemodified', + // + // segmentation data modified + // + SEGMENTATION_DATA_MODIFIED = 'cornerstonetoolssegmentationdatamodified', // // MOUSE diff --git a/packages/cornerstone-tools/src/enums/SegmentationRepresentations.ts b/packages/cornerstone-tools/src/enums/SegmentationRepresentations.ts new file mode 100644 index 000000000..1e24978a2 --- /dev/null +++ b/packages/cornerstone-tools/src/enums/SegmentationRepresentations.ts @@ -0,0 +1,11 @@ +/** + * Segmentations on viewports can be visualized in different ways. This enum + * defines the different ways of visualizing segmentations. Currently, only + * labelmap is supported. + */ +enum SegmentationRepresentations { + Labelmap = 'LABELMAP', + // Todo: add more representations +} + +export default SegmentationRepresentations diff --git a/packages/cornerstone-tools/src/enums/index.js b/packages/cornerstone-tools/src/enums/index.js index 76b9b4295..c4c929f70 100644 --- a/packages/cornerstone-tools/src/enums/index.js +++ b/packages/cornerstone-tools/src/enums/index.js @@ -2,5 +2,12 @@ import ToolBindings from './ToolBindings' import ToolModes from './ToolModes' import ToolDataStates from './ToolDataStates' import CornerstoneTools3DEvents from './CornerstoneTools3DEvents' +import SegmentationRepresentations from './SegmentationRepresentations' -export { ToolBindings, ToolModes, ToolDataStates, CornerstoneTools3DEvents } +export { + ToolBindings, + ToolModes, + ToolDataStates, + CornerstoneTools3DEvents, + SegmentationRepresentations, +} diff --git a/packages/cornerstone-tools/src/eventDispatchers/keyboardEventHandlers/keyDown.ts b/packages/cornerstone-tools/src/eventDispatchers/keyboardEventHandlers/keyDown.ts index 145a42693..4044a0022 100644 --- a/packages/cornerstone-tools/src/eventDispatchers/keyboardEventHandlers/keyDown.ts +++ b/packages/cornerstone-tools/src/eventDispatchers/keyboardEventHandlers/keyDown.ts @@ -11,14 +11,12 @@ export default function keyDown(evt) { const { renderingEngineUID, viewportUID } = evt.detail - const toolGroups = ToolGroupManager.getToolGroups( + const toolGroup = ToolGroupManager.getToolGroup( renderingEngineUID, viewportUID ) - toolGroups.forEach((toolGroup) => { - if (Object.keys(toolGroup.toolOptions).includes(activeTool.name)) { - toolGroup.resetViewportsCursor({ name: activeTool.name }) - } - }) + if (Object.keys(toolGroup.toolOptions).includes(activeTool.name)) { + toolGroup.resetViewportsCursor({ name: activeTool.name }) + } } diff --git a/packages/cornerstone-tools/src/eventDispatchers/keyboardEventHandlers/keyUp.ts b/packages/cornerstone-tools/src/eventDispatchers/keyboardEventHandlers/keyUp.ts index c730569ca..18c4623cd 100644 --- a/packages/cornerstone-tools/src/eventDispatchers/keyboardEventHandlers/keyUp.ts +++ b/packages/cornerstone-tools/src/eventDispatchers/keyboardEventHandlers/keyUp.ts @@ -8,7 +8,7 @@ export default function keyUp(evt) { const { renderingEngineUID, viewportUID } = evt.detail - const toolGroups = ToolGroupManager.getToolGroups( + const toolGroup = ToolGroupManager.getToolGroup( renderingEngineUID, viewportUID ) @@ -16,9 +16,7 @@ export default function keyUp(evt) { // Reset the modifier key resetModifierKey() - toolGroups.forEach((toolGroup) => { - if (Object.keys(toolGroup.toolOptions).includes(activeTool.name)) { - toolGroup.resetViewportsCursor({ name: activeTool.name }) - } - }) + if (Object.keys(toolGroup.toolOptions).includes(activeTool.name)) { + toolGroup.resetViewportsCursor({ name: activeTool.name }) + } } diff --git a/packages/cornerstone-tools/src/eventDispatchers/mouseEventHandlers/mouseDown.ts b/packages/cornerstone-tools/src/eventDispatchers/mouseEventHandlers/mouseDown.ts index 9a66e81ef..d32841740 100644 --- a/packages/cornerstone-tools/src/eventDispatchers/mouseEventHandlers/mouseDown.ts +++ b/packages/cornerstone-tools/src/eventDispatchers/mouseEventHandlers/mouseDown.ts @@ -11,9 +11,9 @@ import { selectToolData, deselectToolData, isToolDataSelected, -} from '../../stateManagement/toolDataSelection' +} from '../../stateManagement/annotation/toolDataSelection' -import { isToolDataLocked } from '../../stateManagement/toolDataLocking' +import { isToolDataLocked } from '../../stateManagement/annotation/toolDataLocking' // // Util import getToolsWithMoveableHandles from '../../store/getToolsWithMoveableHandles' diff --git a/packages/cornerstone-tools/src/eventDispatchers/mouseEventHandlers/mouseDownActivate.ts b/packages/cornerstone-tools/src/eventDispatchers/mouseEventHandlers/mouseDownActivate.ts index a9bbd3772..f8d174d59 100644 --- a/packages/cornerstone-tools/src/eventDispatchers/mouseEventHandlers/mouseDownActivate.ts +++ b/packages/cornerstone-tools/src/eventDispatchers/mouseEventHandlers/mouseDownActivate.ts @@ -1,6 +1,6 @@ import { state } from '../../store' import getActiveToolForMouseEvent from '../shared/getActiveToolForMouseEvent' -import { selectToolData } from '../../stateManagement/toolDataSelection' +import { selectToolData } from '../../stateManagement/annotation/toolDataSelection' /** * @function mouseDownActivate - If the `mouseDown` handler does not consume an event, diff --git a/packages/cornerstone-tools/src/eventDispatchers/shared/customCallbackHandler.ts b/packages/cornerstone-tools/src/eventDispatchers/shared/customCallbackHandler.ts index 02876efd4..9a634a51f 100644 --- a/packages/cornerstone-tools/src/eventDispatchers/shared/customCallbackHandler.ts +++ b/packages/cornerstone-tools/src/eventDispatchers/shared/customCallbackHandler.ts @@ -28,11 +28,15 @@ export default function customCallbackHandler( } const { renderingEngineUID, viewportUID } = evt.detail - const toolGroups = ToolGroupManager.getToolGroups( + const toolGroup = ToolGroupManager.getToolGroup( renderingEngineUID, viewportUID ) + if (!toolGroup) { + return false + } + // TODO: Filter tools by interaction type? /** * Iterate tool group tools until we find a tool that is: @@ -41,31 +45,22 @@ export default function customCallbackHandler( * */ let activeTool - for (let i = 0; i < toolGroups.length; i++) { - const toolGroup = toolGroups[i] - const toolGroupToolNames = Object.keys(toolGroup.toolOptions) + const toolGroupToolNames = Object.keys(toolGroup.toolOptions) - for (let j = 0; j < toolGroupToolNames.length; j++) { - const toolName = toolGroupToolNames[j] - const tool = toolGroup.toolOptions[toolName] - // TODO: Should be getter - const toolInstance = toolGroup._toolInstances[toolName] - - if ( - // TODO: Should be enum? - tool.mode === Active && - // TODO: Should be implements interface? - // Weird that we need concrete instance. Other options to filter / get callback? - typeof toolInstance[customFunction] === 'function' - ) { - // This should be behind some API. Too much knowledge of ToolGroup - // inner workings leaking out - activeTool = toolGroup._toolInstances[toolName] - break - } - } + for (let j = 0; j < toolGroupToolNames.length; j++) { + const toolName = toolGroupToolNames[j] + const tool = toolGroup.toolOptions[toolName] + // TODO: Should be getter + const toolInstance = toolGroup.getToolInstance(toolName) - if (activeTool) { + if ( + // TODO: Should be enum? + tool.mode === Active && + // TODO: Should be implements interface? + // Weird that we need concrete instance. Other options to filter / get callback? + typeof toolInstance[customFunction] === 'function' + ) { + activeTool = toolGroup.getToolInstance(toolName) break } } diff --git a/packages/cornerstone-tools/src/eventDispatchers/shared/getActiveToolForMouseEvent.ts b/packages/cornerstone-tools/src/eventDispatchers/shared/getActiveToolForMouseEvent.ts index a7a3190df..4052f2198 100644 --- a/packages/cornerstone-tools/src/eventDispatchers/shared/getActiveToolForMouseEvent.ts +++ b/packages/cornerstone-tools/src/eventDispatchers/shared/getActiveToolForMouseEvent.ts @@ -17,34 +17,33 @@ export default function getActiveToolForMouseEvent(evt) { const mouseEvent = evt.detail.event const modifierKey = keyEventListener.getModifierKey() - const toolGroups = ToolGroupManager.getToolGroups( + const toolGroup = ToolGroupManager.getToolGroup( renderingEngineUID, viewportUID ) - for (let i = 0; i < toolGroups.length; i++) { - const toolGroup = toolGroups[i] - const toolGroupToolNames = Object.keys(toolGroup.toolOptions) - - for (let j = 0; j < toolGroupToolNames.length; j++) { - const toolName = toolGroupToolNames[j] - const tool = toolGroup.toolOptions[toolName] - - // tool has binding that matches the mouse button, if mouseEvent is undefined - // it uses the primary button - const correctBinding = - tool.bindings.length && - tool.bindings.some( - (binding) => - binding.mouseButton === (mouseEvent ? mouseEvent.buttons : 1) && - binding.modifierKey === modifierKey - ) - - if (tool.mode === Active && correctBinding) { - // This should be behind some API. Too much knowledge of ToolGroup - // inner workings leaking out - return toolGroup._toolInstances[toolName] - } + if (!toolGroup) { + return null + } + + const toolGroupToolNames = Object.keys(toolGroup.toolOptions) + + for (let j = 0; j < toolGroupToolNames.length; j++) { + const toolName = toolGroupToolNames[j] + const tool = toolGroup.toolOptions[toolName] + + // tool has binding that matches the mouse button, if mouseEvent is undefined + // it uses the primary button + const correctBinding = + tool.bindings.length && + tool.bindings.some( + (binding) => + binding.mouseButton === (mouseEvent ? mouseEvent.buttons : 1) && + binding.modifierKey === modifierKey + ) + + if (tool.mode === Active && correctBinding) { + return toolGroup.getToolInstance(toolName) } } } diff --git a/packages/cornerstone-tools/src/eventDispatchers/shared/getToolsWithModesForMouseEvent.ts b/packages/cornerstone-tools/src/eventDispatchers/shared/getToolsWithModesForMouseEvent.ts index e0bc3294e..3c4ebe821 100644 --- a/packages/cornerstone-tools/src/eventDispatchers/shared/getToolsWithModesForMouseEvent.ts +++ b/packages/cornerstone-tools/src/eventDispatchers/shared/getToolsWithModesForMouseEvent.ts @@ -18,40 +18,41 @@ export default function getToolsWithModesForMouseEvent( const modifierKey = keyEventListener.getModifierKey() const { renderingEngineUID, viewportUID } = evt.detail - const toolGroups = ToolGroupManager.getToolGroups( + const toolGroup = ToolGroupManager.getToolGroup( renderingEngineUID, viewportUID ) + if (!toolGroup) { + return [] + } + const enabledTools = [] - for (let i = 0; i < toolGroups.length; i++) { - const toolGroup = toolGroups[i] - const toolGroupToolNames = Object.keys(toolGroup.toolOptions) - - for (let j = 0; j < toolGroupToolNames.length; j++) { - const toolName = toolGroupToolNames[j] - const tool = toolGroup.toolOptions[toolName] - - // tool has binding that matches the mouse button - const correctBinding = - evtButton != null && // not null or undefined - tool.bindings.length && - tool.bindings.some( - (binding) => - binding.mouseButton === evtButton && - binding.modifierKey === modifierKey - ) - - if ( - modesFilter.includes(tool.mode) && - // Should not filter by event's button - // or should, and the tool binding includes the event's button - (!evtButton || correctBinding) - ) { - const toolInstance = toolGroup._toolInstances[toolName] - enabledTools.push(toolInstance) - } + const toolGroupToolNames = Object.keys(toolGroup.toolOptions) + + for (let j = 0; j < toolGroupToolNames.length; j++) { + const toolName = toolGroupToolNames[j] + const tool = toolGroup.toolOptions[toolName] + + // tool has binding that matches the mouse button + const correctBinding = + evtButton != null && // not null or undefined + tool.bindings.length && + tool.bindings.some( + (binding) => + binding.mouseButton === evtButton && + binding.modifierKey === modifierKey + ) + + if ( + modesFilter.includes(tool.mode) && + // Should not filter by event's button + // or should, and the tool binding includes the event's button + (!evtButton || correctBinding) + ) { + const toolInstance = toolGroup.getToolInstance(toolName) + enabledTools.push(toolInstance) } } diff --git a/packages/cornerstone-tools/src/eventListeners/index.ts b/packages/cornerstone-tools/src/eventListeners/index.ts index 9e2e5d18d..27c6ce01e 100644 --- a/packages/cornerstone-tools/src/eventListeners/index.ts +++ b/packages/cornerstone-tools/src/eventListeners/index.ts @@ -1,7 +1,10 @@ import mouseEventListeners from './mouse' import wheelEventListener from './wheel' import keyEventListener from './keyboard' -import labelmapStateEventListener from './labelmap' +import { + segmentationDataModifiedEventListener, + segmentationStateModifiedEventListener, +} from './segmentation' import { measurementSelectionListener } from './toolStyles' //import touchEventListeners from './touchEventListeners'; @@ -9,6 +12,7 @@ export { mouseEventListeners, wheelEventListener, keyEventListener, - labelmapStateEventListener, + segmentationStateModifiedEventListener, + segmentationDataModifiedEventListener, measurementSelectionListener, } diff --git a/packages/cornerstone-tools/src/eventListeners/labelmap/index.ts b/packages/cornerstone-tools/src/eventListeners/labelmap/index.ts deleted file mode 100644 index 9430bf67e..000000000 --- a/packages/cornerstone-tools/src/eventListeners/labelmap/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import labelmapStateListener from './labelmapStateListener' - -export default labelmapStateListener diff --git a/packages/cornerstone-tools/src/eventListeners/labelmap/labelmapStateListener.ts b/packages/cornerstone-tools/src/eventListeners/labelmap/labelmapStateListener.ts deleted file mode 100644 index 9a573036f..000000000 --- a/packages/cornerstone-tools/src/eventListeners/labelmap/labelmapStateListener.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - getEnabledElement, - VolumeViewport, -} from '@precisionmetrics/cornerstone-render' -import state from '../../store/SegmentationModule/state' -import setLabelmapColorAndOpacity from '../../store/SegmentationModule/setLabelmapColorAndOpacity' -import csToolsEvents from '../../enums/CornerstoneTools3DEvents' - -function updateLabelmapProperties( - viewport: VolumeViewport, - activeLabelmapIndex: number -): void { - // Render only active labelmaps from the viewport state - const viewportLabelmapsState = state.volumeViewports[viewport.uid].labelmaps - const { volumeUID: activeLabelmapUID } = - viewportLabelmapsState[activeLabelmapIndex] - - viewportLabelmapsState.forEach((labelmapState) => { - const { - volumeUID: labelmapUID, - colorLUTIndex, - cfun, - ofun, - labelmapConfig, - } = labelmapState - const { volumeActor } = viewport.getActor(labelmapUID) - - const isActiveLabelmap = activeLabelmapUID === labelmapUID - - setLabelmapColorAndOpacity( - volumeActor, - cfun, - ofun, - colorLUTIndex, - labelmapConfig, - isActiveLabelmap - ) - }) -} - -const onLabelmapStateUpdated = function (evt) { - const { viewport, viewportUID, renderingEngine } = getEnabledElement( - evt.detail.element - ) - - if (!(viewport instanceof VolumeViewport)) { - throw new Error('Segmentation for stack viewports not implemented yet') - } - - const { activeLabelmapIndex } = state.volumeViewports[viewportUID] - - updateLabelmapProperties(viewport, activeLabelmapIndex) - - renderingEngine.renderViewport(viewportUID) -} - -const enable = function (element: HTMLElement) { - element.addEventListener( - csToolsEvents.LABELMAP_STATE_UPDATED, - onLabelmapStateUpdated - ) -} - -const disable = function (element: HTMLElement) { - element.removeEventListener( - csToolsEvents.LABELMAP_STATE_UPDATED, - onLabelmapStateUpdated - ) -} - -export default { - enable, - disable, -} diff --git a/packages/cornerstone-tools/src/eventListeners/segmentation/index.ts b/packages/cornerstone-tools/src/eventListeners/segmentation/index.ts new file mode 100644 index 000000000..4f1878c28 --- /dev/null +++ b/packages/cornerstone-tools/src/eventListeners/segmentation/index.ts @@ -0,0 +1,7 @@ +import segmentationStateModifiedEventListener from './segmentationStateModifiedEventListener' +import segmentationDataModifiedEventListener from './segmentationDataModifiedEventListener' + +export { + segmentationStateModifiedEventListener, + segmentationDataModifiedEventListener, +} diff --git a/packages/cornerstone-tools/src/eventListeners/segmentation/segmentationDataModifiedEventListener.ts b/packages/cornerstone-tools/src/eventListeners/segmentation/segmentationDataModifiedEventListener.ts new file mode 100644 index 000000000..fb2f184b9 --- /dev/null +++ b/packages/cornerstone-tools/src/eventListeners/segmentation/segmentationDataModifiedEventListener.ts @@ -0,0 +1,67 @@ +import { cache } from '@precisionmetrics/cornerstone-render' + +import triggerSegmentationRender from '../../util/triggerSegmentationRender' +import SegmentationRepresentations from '../../enums/SegmentationRepresentations' +import * as SegmentationState from '../../stateManagement/segmentation/segmentationState' +import { SegmentationDataModifiedEvent } from '../../types/SegmentationEventTypes' + +/** A callback function that is called when the segmentation data is modified which + * often is as a result of tool interactions e.g., scissors, eraser, etc. + */ +const onSegmentationDataModified = function ( + evt: SegmentationDataModifiedEvent +): void { + const { toolGroupUID, segmentationDataUID } = evt.detail + + const segmentationData = SegmentationState.getSegmentationDataByUID( + toolGroupUID, + segmentationDataUID + ) + + if (!segmentationData) { + console.warn( + `onSegmentationDataModified: segmentationDataUID ${segmentationDataUID} not found in toolGroupUID ${toolGroupUID}` + ) + return + } + + const { + representation: { type }, + } = segmentationData + + let toolGroupUIDs + if (type === SegmentationRepresentations.Labelmap) { + // get the volume from cache, we need the openGLTexture to be updated to GPU + const { volumeUID } = segmentationData + const segmentation = cache.getVolume(volumeUID) + + if (!segmentation) { + console.warn('segmentation not found in cache') + return + } + const { imageData, vtkOpenGLTexture, uid } = segmentation + + // Todo: this can be optimized to not use the full texture from all slices + const numSlices = imageData.getDimensions()[2] + const modifiedSlicesToUse = [...Array(numSlices).keys()] + + // Update the texture for the volume in the GPU + modifiedSlicesToUse.forEach((i) => { + vtkOpenGLTexture.setUpdatedFrame(i) + }) + + // Trigger modified on the imageData to update the image + imageData.modified() + toolGroupUIDs = SegmentationState.getToolGroupsWithSegmentation(uid) + } else { + throw new Error( + `onSegmentationDataModified: representationType ${type} not supported yet` + ) + } + + toolGroupUIDs.forEach((toolGroupUID) => { + triggerSegmentationRender(toolGroupUID) + }) +} + +export default onSegmentationDataModified diff --git a/packages/cornerstone-tools/src/eventListeners/segmentation/segmentationStateModifiedEventListener.ts b/packages/cornerstone-tools/src/eventListeners/segmentation/segmentationStateModifiedEventListener.ts new file mode 100644 index 000000000..dac7285fb --- /dev/null +++ b/packages/cornerstone-tools/src/eventListeners/segmentation/segmentationStateModifiedEventListener.ts @@ -0,0 +1,16 @@ +import triggerSegmentationRender from '../../util/triggerSegmentationRender' + +import { SegmentationStateModifiedEvent } from '../../types/SegmentationEventTypes' + +/** A function that listens to the `segmentationStateModified` event and triggers + * the `triggerSegmentationRender` function. This function is called when the + * segmentation state or config is modified. + */ +const segmentationStateModifiedEventListener = function ( + evt: SegmentationStateModifiedEvent +): void { + const { toolGroupUID } = evt.detail + triggerSegmentationRender(toolGroupUID) +} + +export default segmentationStateModifiedEventListener diff --git a/packages/cornerstone-tools/src/index.ts b/packages/cornerstone-tools/src/index.ts index d91196dfc..abc5c060b 100644 --- a/packages/cornerstone-tools/src/index.ts +++ b/packages/cornerstone-tools/src/index.ts @@ -15,6 +15,15 @@ import { getDefaultToolStateManager, getViewportSpecificStateManager, getToolDataByToolDataUID, + // segmentations + addSegmentationsForToolGroup, + removeSegmentationsForToolGroup, + getGlobalSegmentationState, + getGlobalSegmentationDataByUID, + getSegmentationState, + getColorLut, + getSegmentationDataByUID, + SegmentationState, } from './stateManagement' import { init, destroy } from './init' @@ -57,13 +66,22 @@ import { RectangleRoiThreshold, RectangleRoiStartEndThreshold, SUVPeakTool, + SegmentationDisplayTool, } from './tools' -import { ToolBindings, ToolModes, CornerstoneTools3DEvents } from './enums' +import { + ToolBindings, + ToolModes, + CornerstoneTools3DEvents, + SegmentationRepresentations, +} from './enums' -const lockedSegmentController = SegmentationModule.lockedSegmentController -const activeLabelmapController = SegmentationModule.activeLabelmapController -const segmentIndexController = SegmentationModule.segmentIndexController -const hideSegmentController = SegmentationModule.hideSegmentController +const { + lockedSegmentController, + activeSegmentationController, + segmentationConfigController, + segmentIndexController, + segmentationVisibilityController, +} = SegmentationModule export { // LifeCycle @@ -97,6 +115,7 @@ export { RectangleRoiStartEndThreshold, // PET annotation SUVPeakTool, + SegmentationDisplayTool, // Synchronizers synchronizers, Synchronizer, @@ -105,14 +124,17 @@ export { SynchronizerManager, // Modules SegmentationModule, + // Segmentation Controllers lockedSegmentController, - activeLabelmapController, + activeSegmentationController, segmentIndexController, - hideSegmentController, + segmentationVisibilityController, + segmentationConfigController, // Enums ToolBindings, ToolModes, CornerstoneTools3DEvents, + SegmentationRepresentations, // ToolState Managers FrameOfReferenceSpecificToolStateManager, defaultFrameOfReferenceSpecificToolStateManager, @@ -139,4 +161,13 @@ export { getDefaultToolStateManager, getViewportSpecificStateManager, getToolDataByToolDataUID, + // Segmentations + addSegmentationsForToolGroup, + SegmentationState, + getGlobalSegmentationDataByUID, + getSegmentationState, + getColorLut, + getSegmentationDataByUID, + removeSegmentationsForToolGroup, + getGlobalSegmentationState, } diff --git a/packages/cornerstone-tools/src/init.ts b/packages/cornerstone-tools/src/init.ts index 803bc56a4..3ad56c689 100644 --- a/packages/cornerstone-tools/src/init.ts +++ b/packages/cornerstone-tools/src/init.ts @@ -2,11 +2,18 @@ import { eventTarget, EVENTS as RENDERING_EVENTS, } from '@precisionmetrics/cornerstone-render' -import { getDefaultToolStateManager } from './stateManagement/toolState' -import { CornerstoneTools3DEvents } from './enums' +import { getDefaultToolStateManager } from './stateManagement/annotation/toolState' +import { getDefaultSegmentationStateManager } from './stateManagement/segmentation/segmentationState' +import { CornerstoneTools3DEvents as TOOLS_EVENTS } from './enums' import { addEnabledElement, removeEnabledElement } from './store' import { resetCornerstoneToolsState } from './store/state' -import { measurementSelectionListener } from './eventListeners' +import { + measurementSelectionListener, + segmentationDataModifiedEventListener, + segmentationStateModifiedEventListener, +} from './eventListeners' + +import ToolGroupManager from './store/ToolGroupManager' let csToolsInitialized = false @@ -25,12 +32,19 @@ export function destroy() { _removeCornerstoneEventListeners() _removeCornerstoneToolsEventListeners() + // Impportant: destroy ToolGroups first, in order for cleanup to work correctly for the + // added tools. + ToolGroupManager.destroy() + // Remove all tools resetCornerstoneToolsState() // remove all toolData const toolStateManager = getDefaultToolStateManager() + const segmentationStateManager = getDefaultSegmentationStateManager() + toolStateManager.restoreToolState({}) + segmentationStateManager.resetState() csToolsInitialized = false } @@ -77,18 +91,38 @@ function _addCornerstoneToolsEventListeners() { // Clear any listeners that may already be set _removeCornerstoneToolsEventListeners() - const selectionEvent = CornerstoneTools3DEvents.MEASUREMENT_SELECTION_CHANGE + const selectionEvent = TOOLS_EVENTS.MEASUREMENT_SELECTION_CHANGE + const segmentationDataModified = TOOLS_EVENTS.SEGMENTATION_DATA_MODIFIED + const segmentationStateModified = TOOLS_EVENTS.SEGMENTATION_STATE_MODIFIED eventTarget.addEventListener(selectionEvent, measurementSelectionListener) + eventTarget.addEventListener( + segmentationDataModified, + segmentationDataModifiedEventListener + ) + eventTarget.addEventListener( + segmentationStateModified, + segmentationStateModifiedEventListener + ) } /** * Remove the event listener for the selection event */ function _removeCornerstoneToolsEventListeners() { - const selectionEvent = CornerstoneTools3DEvents.MEASUREMENT_SELECTION_CHANGE + const selectionEvent = TOOLS_EVENTS.MEASUREMENT_SELECTION_CHANGE + const segmentationDataModified = TOOLS_EVENTS.SEGMENTATION_DATA_MODIFIED + const segmentationStateModified = TOOLS_EVENTS.SEGMENTATION_STATE_MODIFIED eventTarget.removeEventListener(selectionEvent, measurementSelectionListener) + eventTarget.removeEventListener( + segmentationDataModified, + segmentationDataModifiedEventListener + ) + eventTarget.removeEventListener( + segmentationStateModified, + segmentationStateModifiedEventListener + ) } export default init diff --git a/packages/cornerstone-tools/src/stateManagement/FrameOfReferenceSpecificToolStateManager.ts b/packages/cornerstone-tools/src/stateManagement/annotation/FrameOfReferenceSpecificToolStateManager.ts similarity index 99% rename from packages/cornerstone-tools/src/stateManagement/FrameOfReferenceSpecificToolStateManager.ts rename to packages/cornerstone-tools/src/stateManagement/annotation/FrameOfReferenceSpecificToolStateManager.ts index b9b7b977f..d5e60981e 100644 --- a/packages/cornerstone-tools/src/stateManagement/FrameOfReferenceSpecificToolStateManager.ts +++ b/packages/cornerstone-tools/src/stateManagement/annotation/FrameOfReferenceSpecificToolStateManager.ts @@ -1,10 +1,10 @@ -import uuidv4 from '../util/uuidv4' +import uuidv4 from '../../util/uuidv4' import { ToolSpecificToolData, ToolSpecificToolState, FrameOfReferenceSpecificToolState, ToolState, -} from '../types/toolStateTypes' +} from '../../types/toolStateTypes' import cloneDeep from 'lodash.clonedeep' import { diff --git a/packages/cornerstone-tools/src/stateManagement/getStyle.ts b/packages/cornerstone-tools/src/stateManagement/annotation/getStyle.ts similarity index 92% rename from packages/cornerstone-tools/src/stateManagement/getStyle.ts rename to packages/cornerstone-tools/src/stateManagement/annotation/getStyle.ts index ed68e4d3e..4376ebfa4 100644 --- a/packages/cornerstone-tools/src/stateManagement/getStyle.ts +++ b/packages/cornerstone-tools/src/stateManagement/annotation/getStyle.ts @@ -1,5 +1,5 @@ import { Settings } from '@precisionmetrics/cornerstone-render' -import state from '../store/state' +import state from '../../store/state' export default function getStyle( toolName?: string, diff --git a/packages/cornerstone-tools/src/stateManagement/setGlobalStyle.ts b/packages/cornerstone-tools/src/stateManagement/annotation/setGlobalStyle.ts similarity index 100% rename from packages/cornerstone-tools/src/stateManagement/setGlobalStyle.ts rename to packages/cornerstone-tools/src/stateManagement/annotation/setGlobalStyle.ts diff --git a/packages/cornerstone-tools/src/stateManagement/setToolDataStyle.ts b/packages/cornerstone-tools/src/stateManagement/annotation/setToolDataStyle.ts similarity index 91% rename from packages/cornerstone-tools/src/stateManagement/setToolDataStyle.ts rename to packages/cornerstone-tools/src/stateManagement/annotation/setToolDataStyle.ts index 2869ffbd2..3b1c020f4 100644 --- a/packages/cornerstone-tools/src/stateManagement/setToolDataStyle.ts +++ b/packages/cornerstone-tools/src/stateManagement/annotation/setToolDataStyle.ts @@ -1,5 +1,5 @@ import { Settings } from '@precisionmetrics/cornerstone-render' -import state from '../store/state' +import state from '../../store/state' export default function setToolDataStyle( toolName: string, diff --git a/packages/cornerstone-tools/src/stateManagement/setToolStyle.ts b/packages/cornerstone-tools/src/stateManagement/annotation/setToolStyle.ts similarity index 90% rename from packages/cornerstone-tools/src/stateManagement/setToolStyle.ts rename to packages/cornerstone-tools/src/stateManagement/annotation/setToolStyle.ts index 72ad0bad8..6588809da 100644 --- a/packages/cornerstone-tools/src/stateManagement/setToolStyle.ts +++ b/packages/cornerstone-tools/src/stateManagement/annotation/setToolStyle.ts @@ -1,5 +1,5 @@ import { Settings } from '@precisionmetrics/cornerstone-render' -import state from '../store/state' +import state from '../../store/state' export default function setToolStyle( toolName: string, diff --git a/packages/cornerstone-tools/src/stateManagement/toolDataLocking.ts b/packages/cornerstone-tools/src/stateManagement/annotation/toolDataLocking.ts similarity index 97% rename from packages/cornerstone-tools/src/stateManagement/toolDataLocking.ts rename to packages/cornerstone-tools/src/stateManagement/annotation/toolDataLocking.ts index c5ec20bfc..4926e0288 100644 --- a/packages/cornerstone-tools/src/stateManagement/toolDataLocking.ts +++ b/packages/cornerstone-tools/src/stateManagement/annotation/toolDataLocking.ts @@ -1,6 +1,6 @@ import { eventTarget, triggerEvent } from '@precisionmetrics/cornerstone-render' -import { CornerstoneTools3DEvents } from '../enums' -import { ToolSpecificToolData } from '../types' +import { CornerstoneTools3DEvents } from '../../enums' +import { ToolSpecificToolData } from '../../types' /* * Types diff --git a/packages/cornerstone-tools/src/stateManagement/toolDataSelection.ts b/packages/cornerstone-tools/src/stateManagement/annotation/toolDataSelection.ts similarity index 97% rename from packages/cornerstone-tools/src/stateManagement/toolDataSelection.ts rename to packages/cornerstone-tools/src/stateManagement/annotation/toolDataSelection.ts index 0fe993754..bd60ed5c0 100644 --- a/packages/cornerstone-tools/src/stateManagement/toolDataSelection.ts +++ b/packages/cornerstone-tools/src/stateManagement/annotation/toolDataSelection.ts @@ -1,6 +1,6 @@ import { eventTarget, triggerEvent } from '@precisionmetrics/cornerstone-render' -import { CornerstoneTools3DEvents } from '../enums' -import { ToolSpecificToolData } from '../types' +import { CornerstoneTools3DEvents } from '../../enums' +import { ToolSpecificToolData } from '../../types' /* * Types diff --git a/packages/cornerstone-tools/src/stateManagement/toolState.ts b/packages/cornerstone-tools/src/stateManagement/annotation/toolState.ts similarity index 96% rename from packages/cornerstone-tools/src/stateManagement/toolState.ts rename to packages/cornerstone-tools/src/stateManagement/annotation/toolState.ts index ac1d90719..c5961f883 100644 --- a/packages/cornerstone-tools/src/stateManagement/toolState.ts +++ b/packages/cornerstone-tools/src/stateManagement/annotation/toolState.ts @@ -3,14 +3,14 @@ import { triggerEvent, eventTarget, } from '@precisionmetrics/cornerstone-render' -import { CornerstoneTools3DEvents as EVENTS } from '../enums' +import { CornerstoneTools3DEvents as EVENTS } from '../../enums' import { Types } from '@precisionmetrics/cornerstone-render' import { defaultFrameOfReferenceSpecificToolStateManager } from './FrameOfReferenceSpecificToolStateManager' -import { uuidv4 } from '../util' +import { uuidv4 } from '../../util' import { ToolSpecificToolState, ToolSpecificToolData, -} from '../types/toolStateTypes' +} from '../../types/toolStateTypes' function getDefaultToolStateManager() { return defaultFrameOfReferenceSpecificToolStateManager diff --git a/packages/cornerstone-tools/src/stateManagement/toolStyle.ts b/packages/cornerstone-tools/src/stateManagement/annotation/toolStyle.ts similarity index 98% rename from packages/cornerstone-tools/src/stateManagement/toolStyle.ts rename to packages/cornerstone-tools/src/stateManagement/annotation/toolStyle.ts index 0e82f80b2..c76c82890 100644 --- a/packages/cornerstone-tools/src/stateManagement/toolStyle.ts +++ b/packages/cornerstone-tools/src/stateManagement/annotation/toolStyle.ts @@ -1,5 +1,5 @@ import { Settings } from '@precisionmetrics/cornerstone-render' -import { ToolModes, ToolDataStates } from '../enums' +import { ToolModes, ToolDataStates } from '../../enums' /* * Initialization diff --git a/packages/cornerstone-tools/src/stateManagement/index.js b/packages/cornerstone-tools/src/stateManagement/index.js index 43682e675..7687f50de 100644 --- a/packages/cornerstone-tools/src/stateManagement/index.js +++ b/packages/cornerstone-tools/src/stateManagement/index.js @@ -1,13 +1,13 @@ import FrameOfReferenceSpecificToolStateManager, { defaultFrameOfReferenceSpecificToolStateManager, -} from './FrameOfReferenceSpecificToolStateManager' -import * as toolStyle from './toolStyle' -import getStyle from './getStyle' -import setGlobalStyle from './setGlobalStyle' -import setToolStyle from './setToolStyle' -import setToolDataStyle from './setToolDataStyle' -import * as toolDataLocking from './toolDataLocking' -import * as toolDataSelection from './toolDataSelection' +} from './annotation/FrameOfReferenceSpecificToolStateManager' +import * as toolStyle from './annotation/toolStyle' +import getStyle from './annotation/getStyle' +import setGlobalStyle from './annotation/setGlobalStyle' +import setToolStyle from './annotation/setToolStyle' +import setToolDataStyle from './annotation/setToolDataStyle' +import * as toolDataLocking from './annotation/toolDataLocking' +import * as toolDataSelection from './annotation/toolDataSelection' import { getToolState, addToolState, @@ -16,9 +16,22 @@ import { getDefaultToolStateManager, getViewportSpecificStateManager, getToolDataByToolDataUID, -} from './toolState' +} from './annotation/toolState' + +import { + getGlobalSegmentationDataByUID, + getSegmentationState, + getColorLut, + addSegmentationsForToolGroup, + removeSegmentationsForToolGroup, + getGlobalSegmentationState, + getSegmentationDataByUID, + getToolGroupsWithSegmentation, + SegmentationState, +} from './segmentation' export { + // annotations FrameOfReferenceSpecificToolStateManager, defaultFrameOfReferenceSpecificToolStateManager, toolDataLocking, @@ -35,4 +48,14 @@ export { getDefaultToolStateManager, getViewportSpecificStateManager, getToolDataByToolDataUID, + // segmentations + addSegmentationsForToolGroup, + getGlobalSegmentationDataByUID, + getSegmentationState, + getColorLut, + removeSegmentationsForToolGroup, + getGlobalSegmentationState, + getToolGroupsWithSegmentation, + getSegmentationDataByUID, + SegmentationState, } diff --git a/packages/cornerstone-tools/src/stateManagement/segmentation/SegmentationStateManager.ts b/packages/cornerstone-tools/src/stateManagement/segmentation/SegmentationStateManager.ts new file mode 100644 index 000000000..c76a77c03 --- /dev/null +++ b/packages/cornerstone-tools/src/stateManagement/segmentation/SegmentationStateManager.ts @@ -0,0 +1,464 @@ +import cloneDeep from 'lodash.clonedeep' +import uuidv4 from '../../util/uuidv4' +import { addColorLUT } from './colorLUT' + +import { + SegmentationState, + GlobalSegmentationState, + GlobalSegmentationData, + ColorLUT, + ToolGroupSpecificSegmentationData, + ToolGroupSpecificSegmentationState, + SegmentationConfig, +} from '../../types/SegmentationStateTypes' + +/* A default initial state for the segmentation manager. */ +const initialDefaultState = { + colorLutTables: [], + global: { + segmentations: [], + config: { + renderInactiveSegmentations: true, + representations: {}, + }, + }, + toolGroups: {}, +} + +export default class SegmentationStateManager { + private state: SegmentationState + public readonly uid: string + + constructor(uid?: string) { + if (!uid) { + uid = uuidv4() + } + this.state = cloneDeep(initialDefaultState) + this.uid = uid + } + + /** + * It returns a copy of the current state of the segmentation + * @returns A deep copy of the state. + */ + getState(): SegmentationState { + return cloneDeep(this.state) + } + + /** + * It returns an array of toolGroupUIDs currently in the segmentation state. + * @returns An array of strings. + */ + getToolGroups(): string[] { + return Object.keys(this.state.toolGroups) + } + + /** + * It returns the colorLut at the specified index. + * @param {number} lutIndex - The index of the color LUT to retrieve. + * @returns A ColorLUT object. + */ + getColorLut(lutIndex: number): ColorLUT | undefined { + return this.state.colorLutTables[lutIndex] + } + + /** + * Reset the state to the default state + */ + resetState(): void { + this.state = cloneDeep(initialDefaultState) + } + + /** + * Given a segmentation UID, return the global segmentation data for that + * segmentation + * @param {string} segmentationUID - The UID of the segmentation to get the + * global data for. + * @returns {GlobalSegmentationData} - The global segmentation data for the + * segmentation with the given UID. + */ + getGlobalSegmentationData( + segmentationUID: string + ): GlobalSegmentationData | undefined { + return this.state.global.segmentations?.find( + (segmentationState) => segmentationState.volumeUID === segmentationUID + ) + } + + /** + * Get the global segmentation state for all the segmentations in the + * segmentation state manager. + * @returns An array of GlobalSegmentationData. + */ + getGlobalSegmentationState(): GlobalSegmentationState | [] { + return this.state.global.segmentations + } + + /** + * Get the global config containing both representation config + * and render inactive segmentations config + * @returns The global config object. + */ + getGlobalSegmentationConfig(): SegmentationConfig { + return this.state.global.config + } + + /** + * It sets the global segmentation config including both representation config + * and render inactive segmentations config + * @param {SegmentationConfig} config - The global configuration for the segmentations. + */ + setGlobalSegmentationConfig(config: SegmentationConfig): void { + this.state.global.config = config + } + + /** + * Given a segmentation UID, return a list of tool group UIDs that have that + * segmentation in their segmentation state (segmentation has been added + * to the tool group). + * @param {string} segmentationUID - The UID of the segmentation volume. + * @returns An array of toolGroupUIDs. + */ + getToolGroupsWithSegmentation(segmentationUID: string): string[] { + const toolGroupUIDs = Object.keys(this.state.toolGroups) + + const foundToolGroupUIDs = [] + toolGroupUIDs.forEach((toolGroupUID) => { + const toolGroupSegmentationState = this.getSegmentationState( + toolGroupUID + ) as ToolGroupSpecificSegmentationState + + const segmentationData = toolGroupSegmentationState.find( + (segmentationData) => segmentationData.volumeUID === segmentationUID + ) + + if (segmentationData) { + foundToolGroupUIDs.push(toolGroupUID) + } + }) + + return foundToolGroupUIDs + } + + /** + * Get the segmentation state for the toolGroup containing array of + * segmentation data objects. + * + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * belongs to. + * @returns An array of objects, each of which contains the data for a single + * segmentation data + */ + getSegmentationState( + toolGroupUID: string + ): ToolGroupSpecificSegmentationState | [] { + const toolGroupSegmentationState = this.state.toolGroups[toolGroupUID] + + if (!toolGroupSegmentationState) { + return [] + } + + return this.state.toolGroups[toolGroupUID].segmentations + } + + /** + * Given a tool group UID and a representation type, return toolGroup specifc + * config for that representation type. + * + * @param {string} toolGroupUID - The UID of the tool group + * @param {string} representationType - The type of representation, currently only Labelmap + * @returns A SegmentationConfig object. + */ + getSegmentationConfig(toolGroupUID: string): SegmentationConfig | undefined { + const toolGroupStateWithConfig = this.state.toolGroups[toolGroupUID] + + if (!toolGroupStateWithConfig) { + return + } + + return toolGroupStateWithConfig.config + } + + /** + * Set the segmentation config for a given tool group. It will create a new + * tool group specific config if one does not exist. + * + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * belongs to. + * @param {SegmentationConfig} config - SegmentationConfig + */ + setSegmentationConfig( + toolGroupUID: string, + config: SegmentationConfig + ): void { + let toolGroupStateWithConfig = this.state.toolGroups[toolGroupUID] + + if (!toolGroupStateWithConfig) { + this.state.toolGroups[toolGroupUID] = { + segmentations: [], + config: { + renderInactiveSegmentations: true, + representations: {}, + }, + } + + toolGroupStateWithConfig = this.state.toolGroups[toolGroupUID] + } + + toolGroupStateWithConfig.config = { + ...toolGroupStateWithConfig.config, + ...config, + } + } + + /** + * Given a toolGroupUID and a segmentationDataUID, return the segmentation data for that tool group. + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * data belongs to. + * @param {string} segmentationDataUID - string + * @returns A ToolGroupSpecificSegmentationData object. + */ + getSegmentationDataByUID( + toolGroupUID: string, + segmentationDataUID: string + ): ToolGroupSpecificSegmentationData | undefined { + const toolGroupSegState = this.getSegmentationState( + toolGroupUID + ) as ToolGroupSpecificSegmentationState + + const segmentationData = toolGroupSegState.find( + (segData) => segData.segmentationDataUID === segmentationDataUID + ) + + return segmentationData + } + + /** + * Get the active segmentation data for a tool group + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * data belongs to. + * @returns A ToolGroupSpecificSegmentationData object. + */ + getActiveSegmentationData( + toolGroupUID: string + ): ToolGroupSpecificSegmentationData | undefined { + const toolGroupSegState = this.getSegmentationState( + toolGroupUID + ) as ToolGroupSpecificSegmentationState + + return toolGroupSegState.find((segmentationData) => segmentationData.active) + } + + /** + * It adds a color LUT to the state. + * @param {ColorLUT} colorLut - ColorLUT + * @param {number} lutIndex - The index of the color LUT table to add. + */ + addColorLut(colorLut: ColorLUT, lutIndex: number): void { + if (this.state.colorLutTables[lutIndex]) { + console.log('Color LUT table already exists, overwriting') + } + + this.state.colorLutTables[lutIndex] = colorLut + } + + /** + * It adds a new segmentation to the global segmentation state. It will merge + * the segmentation data with the existing global segmentation data if it exists. + * @param {GlobalSegmentationData} segmentationData - GlobalSegmentationData + */ + addGlobalSegmentationData(segmentationData: GlobalSegmentationData): void { + const { volumeUID } = segmentationData + + // Creating the default color LUT if not created yet + this._initDefaultColorLutIfNecessary() + + // Don't allow overwriting existing labelmapState with the same labelmapUID + const existingGlobalSegmentationData = + this.getGlobalSegmentationData(volumeUID) + + // merge the new state with the existing state + const updatedState = { + ...existingGlobalSegmentationData, + ...segmentationData, + } + + // Is there any existing state? + if (!existingGlobalSegmentationData) { + this.state.global.segmentations.push({ + volumeUID, + label: segmentationData.label, + referenceVolumeUID: segmentationData.referenceVolumeUID, + cachedStats: segmentationData.cachedStats, + referenceImageId: segmentationData.referenceImageId, + activeSegmentIndex: segmentationData.activeSegmentIndex, + segmentsLocked: segmentationData.segmentsLocked, + }) + + return + } + + // If there is an existing state, replace it + const index = this.state.global.segmentations.findIndex( + (segmentationState) => segmentationState.volumeUID === volumeUID + ) + this.state.global.segmentations[index] = updatedState + } + + /** + * Add a new segmentation data to the toolGroup's segmentation state + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * belongs to. + * @param {ToolGroupSpecificSegmentationData} segmentationData - + * ToolGroupSpecificSegmentationData + */ + addSegmentationData( + toolGroupUID: string, + segmentationData: ToolGroupSpecificSegmentationData + ): void { + // Initialize the default toolGroup state if not created yet + if (!this.state.toolGroups[toolGroupUID]) { + this.state.toolGroups[toolGroupUID] = { + segmentations: [], + config: {} as SegmentationConfig, + } + } + + // local toolGroupSpecificSegmentationState + this.state.toolGroups[toolGroupUID].segmentations.push(segmentationData) + this._handleActiveSegmentation(toolGroupUID, segmentationData) + } + + /** + * Set the active segmentation data for a tool group + * @param {string} toolGroupUID - The UID of the tool group that owns the + * segmentation data. + * @param {string} segmentationDataUID - string + */ + setActiveSegmentationData( + toolGroupUID: string, + segmentationDataUID: string + ): void { + const toolGroupSegmentations = this.getSegmentationState(toolGroupUID) + + if (!toolGroupSegmentations || !toolGroupSegmentations.length) { + throw new Error( + `No segmentation data found for toolGroupUID: ${toolGroupUID}` + ) + } + + const segmentationData = toolGroupSegmentations.find( + (segmentationData) => + segmentationData.segmentationDataUID === segmentationDataUID + ) + + if (!segmentationData) { + throw new Error( + `No segmentation data found for segmentation data UID ${segmentationDataUID}` + ) + } + + segmentationData.active = true + this._handleActiveSegmentation(toolGroupUID, segmentationData) + } + + /** + * Remove a segmentation data from the toolGroup specific segmentation state + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * data is associated with. + * @param {string} segmentationDataUID - string + */ + removeSegmentationData( + toolGroupUID: string, + segmentationDataUID: string + ): void { + const toolGroupSegmentations = this.getSegmentationState(toolGroupUID) + + if (!toolGroupSegmentations || !toolGroupSegmentations.length) { + throw new Error( + `No viewport specific segmentation state found for viewport ${toolGroupUID}` + ) + } + + const state = toolGroupSegmentations as ToolGroupSpecificSegmentationState + const index = state.findIndex( + (segData) => segData.segmentationDataUID === segmentationDataUID + ) + + if (index === -1) { + console.warn( + `No viewport specific segmentation state data found for viewport ${toolGroupUID} and segmentation data UID ${segmentationDataUID}` + ) + } + + const removedSegmentationData = toolGroupSegmentations[index] + toolGroupSegmentations.splice(index, 1) + this._handleActiveSegmentation(toolGroupUID, removedSegmentationData) + } + + /** + * It handles the active segmentation data based on the active status of the + * segmentation data that was added or removed. + * + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * data belongs to. + * @param {ToolGroupSpecificSegmentationData} + * recentlyAddedOrRemovedSegmentationData - ToolGroupSpecificSegmentationData + */ + _handleActiveSegmentation( + toolGroupUID: string, + recentlyAddedOrRemovedSegmentationData: ToolGroupSpecificSegmentationData + ): void { + const state = this.getSegmentationState( + toolGroupUID + ) as ToolGroupSpecificSegmentationState + + // 1. If there is no segmentationData, return early + if (state.length === 0) { + return + } + + // 2. If there is only one segmentationData, make that one active + if (state.length === 1) { + state[0].active = true + return + } + + // 3. If removed SegmentationData was active, make the first one active + const activeSegmentations = state.filter( + (segmentationData) => segmentationData.active + ) + + if (activeSegmentations.length === 0) { + state[0].active = true + return + } + + // 4. If the added segmentation data is active, make other segmentation data inactive + if (recentlyAddedOrRemovedSegmentationData.active) { + state.forEach((segmentationData) => { + if ( + segmentationData.segmentationDataUID !== + recentlyAddedOrRemovedSegmentationData.segmentationDataUID + ) { + segmentationData.active = false + } + }) + } + + // 5. if added/removed segmentation is is inactive, do nothing + } + + _initDefaultColorLutIfNecessary() { + // if colorLutTable is not specified or the default one is not found + if ( + this.state.colorLutTables.length === 0 || + !this.state.colorLutTables[0] + ) { + addColorLUT(0) + } + } +} + +const defaultSegmentationStateManager = new SegmentationStateManager('DEFAULT') +export { defaultSegmentationStateManager } diff --git a/packages/cornerstone-tools/src/stateManagement/segmentation/addSegmentationsForToolGroup.ts b/packages/cornerstone-tools/src/stateManagement/segmentation/addSegmentationsForToolGroup.ts new file mode 100644 index 000000000..cfce5b607 --- /dev/null +++ b/packages/cornerstone-tools/src/stateManagement/segmentation/addSegmentationsForToolGroup.ts @@ -0,0 +1,88 @@ +import _cloneDeep from 'lodash.clonedeep' +import { + SegmentationDataInput, + SegmentationConfig, +} from '../../types/SegmentationStateTypes' +import { checkSegmentationDataIsValid } from './utils' +import Representations from '../../enums/SegmentationRepresentations' +import { getToolGroupByToolGroupUID } from '../../store/ToolGroupManager' + +import { LabelmapDisplay } from '../../tools/displayTools/Labelmap' +import { uuidv4 } from '../../util' + +/** + * Add a segmentation to the viewports of the toolGroup. It will use the + * provided segmentationDataArray to create and configure the segmentation based + * on the representation type and representation specific configuration. + * @param {string} toolGroupUID - The UID of the toolGroup to add the segmentation to. + * @param segmentationDataArray - minimum of volumeUID should be provided, it will + * throw an error if not. If no representation type is provided, it will use + * the default labelmap representation. + * @param {SegmentationConfig} toolGroupSpecificConfig - The toolGroup specific configuration + * for the segmentation display. + */ +async function addSegmentationsForToolGroup( + toolGroupUID: string, + segmentationDataArray: SegmentationDataInput[], + toolGroupSpecificConfig?: SegmentationConfig +): Promise { + checkSegmentationDataIsValid(segmentationDataArray) + + // Check if there exists a toolGroup with the toolGroupUID + const toolGroup = getToolGroupByToolGroupUID(toolGroupUID) + + if (!toolGroup) { + throw new Error(`No tool group found for toolGroupUID: ${toolGroupUID}`) + } + + const promises = segmentationDataArray.map(async (segData) => { + const segmentationData = _cloneDeep(segData) + + segmentationData.segmentationDataUID = uuidv4() + return _addSegmentation( + toolGroupUID, + segmentationData, + toolGroupSpecificConfig + ) + }) + + await Promise.all(promises) +} + +async function _addSegmentation( + toolGroupUID, + segmentationData, + toolGroupSpecificConfig +) { + const representationType = segmentationData.representation?.type + ? segmentationData.representation.type + : Representations.Labelmap + + // create representation config if not provided by + if (!segmentationData.representation) { + segmentationData.representation = { + type: representationType, + } + } + + // Create empty config if not provided by. + // Note: this is representation-required configuration for the segmentation + // For Labelmap, it is the cfun and ofun. Todo: maybe we change this to props? + if (!segmentationData.representation.config) { + segmentationData.representation.config = {} + } + + if (representationType === Representations.Labelmap) { + await LabelmapDisplay.addSegmentationData( + toolGroupUID, + segmentationData, + toolGroupSpecificConfig + ) + } else { + throw new Error( + `The representation type ${representationType} is not supported yet` + ) + } +} + +export default addSegmentationsForToolGroup diff --git a/packages/cornerstone-tools/src/stateManagement/segmentation/colorLUT.ts b/packages/cornerstone-tools/src/stateManagement/segmentation/colorLUT.ts new file mode 100644 index 000000000..0448f63f6 --- /dev/null +++ b/packages/cornerstone-tools/src/stateManagement/segmentation/colorLUT.ts @@ -0,0 +1,141 @@ +// import state, { Color, ColorLUT, getLabelmapStateForElement } from './state' +import { ColorLUT } from '../../types/SegmentationStateTypes' +import { addColorLut } from './segmentationState' + +const SEGMENTS_PER_SEGMENTATION = 65535 // Todo: max is bigger, but it seems cfun can go upto 255 anyway + +/** + * addColorLUT - Adds a new color LUT to the state at the given colorLUTIndex. + * If no colorLUT is provided, a new color LUT is generated. + * + * @param {number} colorLUTIndex the index of the colorLUT in the state + * @param {number[][]} [colorLUT] An array of The colorLUT to set. + * @returns {null} + */ +export function addColorLUT( + colorLUTIndex: number, + colorLUT: ColorLUT = [] +): void { + if (colorLUT) { + _checkColorLUTLength(colorLUT, SEGMENTS_PER_SEGMENTATION) + + if (colorLUT.length < SEGMENTS_PER_SEGMENTATION) { + colorLUT = [ + ...colorLUT, + ..._generateNewColorLUT(SEGMENTS_PER_SEGMENTATION - colorLUT.length), + ] + } + } else { + // Auto-generates colorLUT. + colorLUT = colorLUT || _generateNewColorLUT(SEGMENTS_PER_SEGMENTATION) + } + + // Append the "zero" (no label) color to the front of the LUT. + colorLUT.unshift([0, 0, 0, 0]) + + addColorLut(colorLUT, colorLUTIndex) +} + +/** + * Checks the length of `colorLUT` compared to `SEGMENTS_PER_SEGMENTATION` and flags up any warnings. + * @param {number[][]} colorLUT + * @param {number} SEGMENTS_PER_SEGMENTATION + * @returns {boolean} Whether the length is valid. + */ +function _checkColorLUTLength( + colorLUT: ColorLUT, + SEGMENTS_PER_SEGMENTATION: number +) { + if (colorLUT.length < SEGMENTS_PER_SEGMENTATION) { + console.warn( + `The provided colorLUT only provides ${colorLUT.length} labels, whereas SEGMENTS_PER_SEGMENTATION is set to ${SEGMENTS_PER_SEGMENTATION}. Autogenerating the rest.` + ) + } else if (colorLUT.length > SEGMENTS_PER_SEGMENTATION) { + console.warn( + `SEGMENTS_PER_SEGMENTATION is set to ${SEGMENTS_PER_SEGMENTATION}, and the provided colorLUT provides ${colorLUT.length}. Using the first ${SEGMENTS_PER_SEGMENTATION} colors from the LUT.` + ) + } +} + +let hueValue = 222.5 +let l = 0.6 +const goldenAngle = 137.5 +const maxL = 0.82 +const minL = 0.3 +const incL = 0.07 + +/** + * Generates a new color LUT (Look Up Table) of length `numberOfColors`, + * which returns an RGBA color for each segment index. + * + * @param {Number} numberOfColors = 255 The number of colors to generate + * @returns {Number[][]} The array of RGB values. + */ +function _generateNewColorLUT(numberOfColors = 255) { + const rgbArr = [] + + // reset every time we generate new colorLUT to be consistent between csTools initializations + hueValue = 222.5 + l = 0.6 + + for (let i = 0; i < numberOfColors; i++) { + rgbArr.push(getRGBAfromHSLA(getNextHue(), getNextL())) + } + + return rgbArr +} + +function getNextHue() { + hueValue += goldenAngle + + if (hueValue >= 360) { + hueValue -= 360 + } + + return hueValue +} + +function getNextL() { + l += incL + + if (l > maxL) { + const diff = l - maxL + + l = minL + diff + } + + return l +} + +/** + * GetRGBAfromHSL - Returns an RGBA color given H, S, L and A. + * + * @param {Number} hue The hue. + * @param {Number} s = 1 The saturation. + * @param {Number} l = 0.6 The lightness. + * @param {Number} alpha = 255 The alpha. + * @returns {Number[]} The RGBA formatted color. + */ +function getRGBAfromHSLA(hue, s = 1, l = 0.6, alpha = 255) { + const c = (1 - Math.abs(2 * l - 1)) * s + const x = c * (1 - Math.abs(((hue / 60) % 2) - 1)) + const m = l - c / 2 + + let r, g, b + + if (hue < 60) { + ;[r, g, b] = [c, x, 0] + } else if (hue < 120) { + ;[r, g, b] = [x, c, 0] + } else if (hue < 180) { + ;[r, g, b] = [0, c, x] + } else if (hue < 240) { + ;[r, g, b] = [0, x, c] + } else if (hue < 300) { + ;[r, g, b] = [x, 0, c] + } else if (hue < 360) { + ;[r, g, b] = [c, 0, x] + } + + return [(r + m) * 255, (g + m) * 255, (b + m) * 255, alpha] +} diff --git a/packages/cornerstone-tools/src/stateManagement/segmentation/index.ts b/packages/cornerstone-tools/src/stateManagement/segmentation/index.ts new file mode 100644 index 000000000..5d585aed4 --- /dev/null +++ b/packages/cornerstone-tools/src/stateManagement/segmentation/index.ts @@ -0,0 +1,26 @@ +import { + getGlobalSegmentationState, + getGlobalSegmentationDataByUID, + getSegmentationState, + getColorLut, + getToolGroupsWithSegmentation, + getSegmentationDataByUID, +} from './segmentationState' + +import * as SegmentationState from './segmentationState' + +import addSegmentationsForToolGroup from './addSegmentationsForToolGroup' +import removeSegmentationsForToolGroup from './removeSegmentationsForToolGroup' + +export { + getGlobalSegmentationState, + getGlobalSegmentationDataByUID, + getSegmentationState, + getColorLut, + getToolGroupsWithSegmentation, + getSegmentationDataByUID, + // + addSegmentationsForToolGroup, + removeSegmentationsForToolGroup, + SegmentationState, +} diff --git a/packages/cornerstone-tools/src/stateManagement/segmentation/removeSegmentationsForToolGroup.ts b/packages/cornerstone-tools/src/stateManagement/segmentation/removeSegmentationsForToolGroup.ts new file mode 100644 index 000000000..dfa2fa250 --- /dev/null +++ b/packages/cornerstone-tools/src/stateManagement/segmentation/removeSegmentationsForToolGroup.ts @@ -0,0 +1,67 @@ +import SegmentationRepresentations from '../../enums/SegmentationRepresentations' +import { LabelmapDisplay } from '../../tools/displayTools/Labelmap' + +import { + getSegmentationState, + getSegmentationDataByUID, +} from './segmentationState' + +/** + * Remove the segmentation data (representation) from the viewports of the toolGroup. + * @param {string} toolGroupUID - The UID of the toolGroup to remove the segmentation from. + * @param {SegmentationDataInput[]} segmentationDataArray - Array of segmentationData + * containing at least volumeUID. If no representation type is provided, it will + * assume the default labelmap representation should be removed from the viewports. + */ +function removeSegmentationsForToolGroup( + toolGroupUID: string, + segmentationDataUIDs?: string[] | undefined +): void { + const toolGroupSegmentations = getSegmentationState(toolGroupUID) + const toolGroupSegmentationDataUIDs = toolGroupSegmentations.map( + (segData) => segData.segmentationDataUID + ) + + let segmentationDataUIDsToRemove = segmentationDataUIDs + if (segmentationDataUIDsToRemove) { + // make sure the segmentationDataUIDs that are going to be removed belong + // to the toolGroup + const invalidSegmentationDataUIDs = segmentationDataUIDs.filter( + (segmentationDataUID) => + !toolGroupSegmentationDataUIDs.includes(segmentationDataUID) + ) + + if (invalidSegmentationDataUIDs.length > 0) { + throw new Error( + `You are trying to remove segmentationDataUIDs that are not in the toolGroup: segmentationDataUID: ${invalidSegmentationDataUIDs}` + ) + } + } else { + // remove all segmentations + segmentationDataUIDsToRemove = toolGroupSegmentationDataUIDs + } + + segmentationDataUIDsToRemove.forEach((segmentationDataUID) => { + _removeSegmentation(toolGroupUID, segmentationDataUID) + }) +} + +function _removeSegmentation( + toolGroupUID: string, + segmentationDataUID: string +): void { + const segmentationData = getSegmentationDataByUID( + toolGroupUID, + segmentationDataUID + ) + + const { representation } = segmentationData + + if (representation.type === SegmentationRepresentations.Labelmap) { + LabelmapDisplay.removeSegmentationData(toolGroupUID, segmentationDataUID) + } else { + throw new Error(`The representation ${representation} is not supported`) + } +} + +export default removeSegmentationsForToolGroup diff --git a/packages/cornerstone-tools/src/stateManagement/segmentation/segmentationState.ts b/packages/cornerstone-tools/src/stateManagement/segmentation/segmentationState.ts new file mode 100644 index 000000000..95b2f768e --- /dev/null +++ b/packages/cornerstone-tools/src/stateManagement/segmentation/segmentationState.ts @@ -0,0 +1,459 @@ +import { defaultSegmentationStateManager } from './SegmentationStateManager' +import { + triggerSegmentationStateModified, + triggerSegmentationGlobalStateModified, +} from '../../store/SegmentationModule' +import { + GlobalSegmentationState, + GlobalSegmentationData, + ColorLUT, + ToolGroupSpecificSegmentationState, + ToolGroupSpecificSegmentationData, + SegmentationConfig, +} from '../../types/SegmentationStateTypes' + +import { + getDefaultRepresentationConfig, + isValidRepresentationConfig, +} from '../../util/segmentation' +import { deepMerge } from '../../util' + +/** + * It returns the defaultSegmentationStateManager. + */ +function getDefaultSegmentationStateManager() { + return defaultSegmentationStateManager +} + +/************************* + * + * GLOBAL STATE + * + **************************/ + +/** + * Get the global segmentation data for a given segmentation UID + * @param {string} segmentationUID - The UID of the segmentation to get the global + * data for. + * @returns {GlobalSegmentationData} A GlobalSegmentationData object + */ +function getGlobalSegmentationDataByUID( + segmentationUID: string +): GlobalSegmentationData { + const segmentationStateManager = getDefaultSegmentationStateManager() + return segmentationStateManager.getGlobalSegmentationData(segmentationUID) +} + +/** + * Add a new global segmentation data to the segmentation state manager, and + * triggers SEGMENTATION_STATE_MODIFIED event if not suppressed. + * + * @event {SEGMENTATION_STATE_MODIFIED} + * @param {GlobalSegmentationData} segmentationData - The data to add to the global + * segmentation state + * @param {boolean} [suppressEvents] - If true, the event will not be triggered. + */ +function addGlobalSegmentationData( + segmentationData: GlobalSegmentationData, + suppressEvents?: boolean +): void { + const segmentationStateManager = getDefaultSegmentationStateManager() + segmentationStateManager.addGlobalSegmentationData(segmentationData) + + if (!suppressEvents) { + triggerSegmentationGlobalStateModified(segmentationData.volumeUID) + } +} + +/** + * Get all global segmentation states, which includes array of all global + * segmenttation data. + * @returns An array of objects, each of which represents a global segmentation + * data. + */ +function getGlobalSegmentationState(): GlobalSegmentationState | [] { + const segmentationStateManager = getDefaultSegmentationStateManager() + return segmentationStateManager.getGlobalSegmentationState() +} + +/*************************** + * + * ToolGroup Specific State + * + ***************************/ + +/** + * Get the segmentation state for a tool group. It will return an array of + * segmentation data objects. + * @param {string} toolGroupUID - The unique identifier of the tool group. + * @returns {ToolGroupSpecificSegmentationState} An array of segmentation data + * objects. + */ +function getSegmentationState( + toolGroupUID: string +): ToolGroupSpecificSegmentationState | [] { + const segmentationStateManager = getDefaultSegmentationStateManager() + return segmentationStateManager.getSegmentationState(toolGroupUID) +} + +/** + * Get the segmentation data object for a given tool group and + * segmentation data UID. It searches all the toolGroup specific segmentation + * data objects and returns the first one that matches the UID. + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * data belongs to. + * @param {string} segmentationDataUID - The UID of the segmentation data to + * retrieve. + * @returns {ToolGroupSpecificSegmentationData} Segmentation Data object. + */ +function getSegmentationDataByUID( + toolGroupUID: string, + segmentationDataUID: string +): ToolGroupSpecificSegmentationData | undefined { + const segmentationStateManager = getDefaultSegmentationStateManager() + return segmentationStateManager.getSegmentationDataByUID( + toolGroupUID, + segmentationDataUID + ) +} + +/** + * Remove a segmentation data from the segmentation state manager for a toolGroup. + * It fires SEGMENTATION_STATE_MODIFIED event. + * + * @event {SEGMENTATION_STATE_MODIFIED} + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * data belongs to. + * @param {string} segmentationDataUID - The UID of the segmentation data to + * remove. + */ +function removeSegmentationData( + toolGroupUID: string, + segmentationDataUID: string +): void { + const segmentationStateManager = getDefaultSegmentationStateManager() + segmentationStateManager.removeSegmentationData( + toolGroupUID, + segmentationDataUID + ) + + triggerSegmentationStateModified(toolGroupUID) +} + +/** + * Add the given segmentation data to the given tool group state. It fires + * SEGMENTATION_STATE_MODIFIED event if not suppressed. + * + * @event {SEGMENTATION_STATE_MODIFIED} + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * data is for. + * @param {ToolGroupSpecificSegmentationData} segmentationData - The data to add to + * the segmentation state. + * @param {boolean} [suppressEvents] - boolean + */ +function addSegmentationData( + toolGroupUID: string, + segmentationData: ToolGroupSpecificSegmentationData, + suppressEvents?: boolean +): void { + const segmentationStateManager = getDefaultSegmentationStateManager() + _initGlobalStateIfNecessary(segmentationStateManager, segmentationData) + + segmentationStateManager.addSegmentationData(toolGroupUID, segmentationData) + + if (!suppressEvents) { + triggerSegmentationStateModified(toolGroupUID) + } +} + +/*************************** + * + * Global Configuration + * + ***************************/ + +/** + * It returns the global segmentation config. Note that the toolGroup-specific + * configuration has higher priority than the global configuration and overwrites + * the global configuration for each representation. + * @returns The global segmentation configuration for all segmentations. + */ +function getGlobalSegmentationConfig(): SegmentationConfig { + const segmentationStateManager = getDefaultSegmentationStateManager() + return segmentationStateManager.getGlobalSegmentationConfig() +} + +/** + * Set the global segmentation configuration. It fires SEGMENTATION_GLOBAL_STATE_MODIFIED + * event if not suppressed. + * + * @event {SEGMENTATION_GLOBAL_STATE_MODIFIED} + * @param {SegmentationConfig} config - The new global segmentation config. + * @param {boolean} [suppressEvents] - If true, the + * `segmentationGlobalStateModified` event will not be triggered. + */ +function setGlobalSegmentationConfig( + config: SegmentationConfig, + suppressEvents?: boolean +): void { + const segmentationStateManager = getDefaultSegmentationStateManager() + segmentationStateManager.setGlobalSegmentationConfig(config) + + if (!suppressEvents) { + triggerSegmentationGlobalStateModified() + } +} + +/*************************** + * + * ToolGroup Specific Configuration + * + ***************************/ + +/** + * Set the segmentation config for the probided toolGroup. ToolGroup specific + * configuration overwrites the global configuration for each representation. + * It fires SEGMENTATION_STATE_MODIFIED event if not suppressed. + * + * @event {SEGMENTATION_STATE_MODIFIED} + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * config is being set for. + * @param {SegmentationConfig} config - The new configuration for the tool group. + * @param {boolean} [suppressEvents] - If true, the event will not be triggered. + */ +function setSegmentationConfig( + toolGroupUID: string, + config: SegmentationConfig, + suppressEvents?: boolean +): void { + const segmentationStateManager = getDefaultSegmentationStateManager() + segmentationStateManager.setSegmentationConfig(toolGroupUID, config) + + if (!suppressEvents) { + triggerSegmentationStateModified(toolGroupUID) + } +} + +/** + * Get the segmentation config for a given tool group which contains each + * segmentation representation configuration. + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * config belongs to. + * @returns A SegmentationConfig object. + */ +function getSegmentationConfig(toolGroupUID: string): SegmentationConfig { + const segmentationStateManager = getDefaultSegmentationStateManager() + return segmentationStateManager.getSegmentationConfig(toolGroupUID) +} + +/*************************** + * + * Utilities + * + ***************************/ + +/** + * Get the tool group UIDs that have a segmentation with the given UID + * @param {string} segmentationUID - The UID of the segmentation to get the tool + * groups for. + * @returns An array of tool group UIDs. + */ +function getToolGroupsWithSegmentation(segmentationUID: string): string[] { + const segmentationStateManager = getDefaultSegmentationStateManager() + return segmentationStateManager.getToolGroupsWithSegmentation(segmentationUID) +} + +/** + * Get the list of all tool groups currently in the segmentation state manager. + * @returns An array of tool group UIDs. + */ +function getToolGroups(): string[] { + const segmentationStateManager = getDefaultSegmentationStateManager() + return segmentationStateManager.getToolGroups() +} + +/** + * Get the color lut for a given index + * @param {number} index - The index of the color lut to retrieve. + * @returns A ColorLUT array. + */ +function getColorLut(index: number): ColorLUT | undefined { + const segmentationStateManager = getDefaultSegmentationStateManager() + return segmentationStateManager.getColorLut(index) +} + +/** + * Add a color LUT to the segmentation state manager + * @param {ColorLUT} colorLut - The color LUT array to add. + * @param {number} index - The index of the color LUT to add. + */ +function addColorLut(colorLut: ColorLUT, index: number): void { + const segmentationStateManager = getDefaultSegmentationStateManager() + segmentationStateManager.addColorLut(colorLut, index) + // Todo: trigger event color LUT added +} + +/** + * Set the active segmentation data for a tool group. It searches the segmentation + * state of the toolGroup and sets the active segmentation data to the one with + * the given UID. It fires SEGMENTATION_STATE_MODIFIED event if not suppressed. + * + * @event {SEGMENTATION_STATE_MODIFIED} + * @param {string} toolGroupUID - The UID of the tool group that owns the + * segmentation data. + * @param {string} segmentationDataUID - The UID of the segmentation data to set as + * active. + * @param {boolean} [suppressEvents] - If true, the segmentation state will be + * updated, but no events will be triggered. + */ +function setActiveSegmentationData( + toolGroupUID: string, + segmentationDataUID: string, + suppressEvents?: boolean +): void { + const segmentationStateManager = getDefaultSegmentationStateManager() + segmentationStateManager.setActiveSegmentationData( + toolGroupUID, + segmentationDataUID + ) + + if (!suppressEvents) { + triggerSegmentationStateModified(toolGroupUID) + } +} + +/** + * Get the active segmentation data for a given tool group by searching the + * segmentation state of the tool group and returning the segmentation data with + * the given UID. + * + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * data belongs to. + * @returns The active segmentation data for the tool group. + */ +function getActiveSegmentationData( + toolGroupUID: string +): ToolGroupSpecificSegmentationData | undefined { + const segmentationStateManager = getDefaultSegmentationStateManager() + + const toolGroupSegmentations = + segmentationStateManager.getSegmentationState(toolGroupUID) + + if (toolGroupSegmentations.length === 0) { + return + } + + const activeSegmentationData = toolGroupSegmentations.find( + (segmentationData: ToolGroupSpecificSegmentationData) => + segmentationData.active + ) + + return activeSegmentationData +} + +/** + * If no global state exists, it create a default one and If the global config + * is not valid, create a default one + * + * @param segmentationStateManager - The state manager for the segmentation. + * @param segmentationData - The segmentation data that we want to add to the + * global state. + */ +function _initGlobalStateIfNecessary( + segmentationStateManager, + segmentationData +) { + const globalSegmentationData = getGlobalSegmentationDataByUID( + segmentationData.volumeUID + ) + // for the representation, if no global config exists, create default one + const { + representation: { type: representationType }, + } = segmentationData + + const globalConfig = getGlobalSegmentationConfig() + const globalRepresentationConfig = + globalConfig.representations[representationType] + const validConfig = isValidRepresentationConfig( + representationType, + globalRepresentationConfig + ) + + // If global segmentationData is not found, or if the global config is not + // valid, we use default values to create both, but we need to only + // fire the event for global state modified once, so we suppress each events. + const suppressEvents = !globalSegmentationData || !validConfig + + // if no global state exists, create a default one + if (!globalSegmentationData) { + const { volumeUID } = segmentationData + + const defaultGlobalData: GlobalSegmentationData = { + volumeUID: volumeUID, + label: volumeUID, + referenceVolumeUID: null, + cachedStats: {}, + referenceImageId: null, + activeSegmentIndex: 1, + segmentsLocked: new Set(), + } + + addGlobalSegmentationData(defaultGlobalData, suppressEvents) + } + + // Todo: we can check the validity of global config for each representation + // when we are setting it up at the setGlobalSegmentationConfig function, not here + if (!validConfig) { + // create default config + const defaultRepresentationConfig = + getDefaultRepresentationConfig(representationType) + + const mergedRepresentationConfig = deepMerge( + defaultRepresentationConfig, + globalRepresentationConfig + ) + + const newGlobalConfig = { + ...globalConfig, + representations: { + ...globalConfig.representations, + [representationType]: mergedRepresentationConfig, + }, + } + + setGlobalSegmentationConfig(newGlobalConfig, suppressEvents) + } + + // If we have suppressed events, means that we have created a new global state + // and/or a new default config for the representation, so we need to trigger + // the event to notify the listeners. + if (suppressEvents) { + triggerSegmentationGlobalStateModified(segmentationData.volumeUID) + } +} + +export { + // config + getGlobalSegmentationConfig, + getSegmentationConfig, + setGlobalSegmentationConfig, + setSegmentationConfig, + // colorLUT + addColorLut, + getColorLut, + // get/set global state + getGlobalSegmentationState, + getGlobalSegmentationDataByUID, + addGlobalSegmentationData, + // toolGroup state + getSegmentationState, + addSegmentationData, + removeSegmentationData, + getSegmentationDataByUID, + setActiveSegmentationData, + getActiveSegmentationData, + getToolGroupsWithSegmentation, + getToolGroups, + // Utility + getDefaultSegmentationStateManager, +} diff --git a/packages/cornerstone-tools/src/stateManagement/segmentation/utils.ts b/packages/cornerstone-tools/src/stateManagement/segmentation/utils.ts new file mode 100644 index 000000000..24ac9f1d3 --- /dev/null +++ b/packages/cornerstone-tools/src/stateManagement/segmentation/utils.ts @@ -0,0 +1,23 @@ +import { SegmentationDataInput } from '../../types/SegmentationStateTypes' + +/** + * Checks if the segmentationDataArray is valid meaning it contains + * volumeUID of the segmentation. + * @param {Partial[]} segmentationDataArray + */ +function checkSegmentationDataIsValid( + segmentationDataArray: SegmentationDataInput[] +): void { + if (!segmentationDataArray || !segmentationDataArray.length) { + throw new Error('The segmentationDataArray undefined or empty array') + } + + // check if volumeUID is present in all the segmentationDataArray + segmentationDataArray.forEach((segmentationData) => { + if (!segmentationData.volumeUID) { + throw new Error('volumeUID is missing in the segmentationData') + } + }) +} + +export { checkSegmentationDataIsValid } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/activeLabelmapController.ts b/packages/cornerstone-tools/src/store/SegmentationModule/activeLabelmapController.ts deleted file mode 100644 index 0cb42ce0e..000000000 --- a/packages/cornerstone-tools/src/store/SegmentationModule/activeLabelmapController.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { - getEnabledElement, - VolumeViewport, -} from '@precisionmetrics/cornerstone-render' - -import state, { getLabelmapsStateForElement } from './state' -import { triggerLabelmapStateUpdated } from './triggerLabelmapStateUpdated' - -/** - * Returns the index of the active `Labelmap3D`. - * - * @param {HTMLElement} HTML Div element - * @returns {number} The index of the active `Labelmap3D`. - */ -function getActiveLabelmapIndex(element: HTMLElement): number { - const viewportLabelmapsState = getLabelmapsStateForElement(element) - - if (!viewportLabelmapsState) { - return - } - - return viewportLabelmapsState.activeLabelmapIndex -} - -/** - * Returns the next labelmap Index that can be set on the element. It checks - * all the available labelmaps for the element, and increases that number by 1 - * or return 0 if no labelmap is provided - * @param element HTMLElement - * @returns next LabelmapIndex - */ -function getNextLabelmapIndex(element: HTMLElement): number { - const viewportLabelmapsState = getLabelmapsStateForElement(element) - - if (!viewportLabelmapsState) { - return 0 - } - - // next labelmap index = current length of labelmaps - return viewportLabelmapsState.labelmaps.length -} - -/** - * Returns the VolumeUID of the active `Labelmap`. - * - * @param {HTMLElement} HTML element - * @returns {number} The index of the active `Labelmap3D`. - */ -function getActiveLabelmapUID(element: HTMLElement): string { - const viewportLabelmapsState = getLabelmapsStateForElement(element) - - if (!viewportLabelmapsState) { - return - } - - const { activeLabelmapIndex } = viewportLabelmapsState - return viewportLabelmapsState.labelmaps[activeLabelmapIndex].volumeUID -} - -/** - * Sets the active `labelmapIndex` for the labelmaps displayed on this - * element. Creates the corresponding `Labelmap3D` if it doesn't exist. - * - * @param {HTMLElement} element HTML Element - * @param {number} labelmapIndex = 0 The index of the labelmap. - * @returns {string} labelmap UID which is the volumeUID of the labelmap which is active now - */ -async function setActiveLabelmapIndex( - element: HTMLElement, - labelmapIndex = 0 -): Promise { - const enabledElement = getEnabledElement(element) - - if (!enabledElement) { - return - } - - const { viewportUID, viewport } = enabledElement - - if (!(viewport instanceof VolumeViewport)) { - throw new Error('Segmentation for StackViewport is not supported yet') - } - - // volumeViewport - const viewportLabelmapsState = state.volumeViewports[viewportUID] - - // check if the labelmapIndex is valid - if (!viewportLabelmapsState?.labelmaps[labelmapIndex]) { - console.warn( - `No labelmap found for index ${labelmapIndex} on element ${element}` - ) - - return - } - - // Update active viewportUID on all viewports with the same volume - state.volumeViewports[viewport.uid].activeLabelmapIndex = labelmapIndex - - const { volumeUID: labelmapUID } = - viewportLabelmapsState.labelmaps[labelmapIndex] - triggerLabelmapStateUpdated(labelmapUID, element) - return viewportLabelmapsState.labelmaps[labelmapIndex].volumeUID -} - -// this method SHOULD not be used to create a new labelmap -function setActiveLabelmapByLabelmapUID( - element: HTMLElement, - labelmapUID: string -): void { - // volumeViewport - const viewportLabelmapsState = getLabelmapsStateForElement(element) - - if ( - !viewportLabelmapsState || - viewportLabelmapsState.labelmaps.length === 0 - ) { - throw new Error(`No labelmap found for ${element}`) - } - - const labelmapIndex = viewportLabelmapsState.labelmaps.findIndex( - ({ volumeUID }) => labelmapUID === volumeUID - ) - - if (labelmapIndex === undefined) { - throw new Error(`No labelmap found with name of ${labelmapUID}`) - } - - setActiveLabelmapIndex(element, labelmapIndex) -} - -// activeLabelmapController -export { - // get - getActiveLabelmapIndex, - getActiveLabelmapUID, - // set - setActiveLabelmapIndex, - setActiveLabelmapByLabelmapUID, - // utils - getNextLabelmapIndex, -} - -export default { - // get - getActiveLabelmapIndex, - getActiveLabelmapUID, - // set - setActiveLabelmapIndex, - setActiveLabelmapByLabelmapUID, - // utils - getNextLabelmapIndex, -} diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/activeSegmentationController.ts b/packages/cornerstone-tools/src/store/SegmentationModule/activeSegmentationController.ts new file mode 100644 index 000000000..01417127a --- /dev/null +++ b/packages/cornerstone-tools/src/store/SegmentationModule/activeSegmentationController.ts @@ -0,0 +1,62 @@ +import { + getActiveSegmentationData, + setActiveSegmentationData, + getGlobalSegmentationDataByUID, +} from '../../stateManagement/segmentation/segmentationState' + +/** + * Get the active segmentation info for the first viewport in the tool group with + * the given toolGroupUID. + * @param {string} toolGroupUID - The UID of the tool group that the user is + * currently interacting with. + */ +function getActiveSegmentationInfo(toolGroupUID: string): { + volumeUID: string + segmentationDataUID: string + activeSegmentIndex: number +} { + const activeSegmentationData = getActiveSegmentationData(toolGroupUID) + + if (!activeSegmentationData) { + return null + } + + const globalState = getGlobalSegmentationDataByUID( + activeSegmentationData.volumeUID + ) + + return { + volumeUID: activeSegmentationData.volumeUID, + segmentationDataUID: activeSegmentationData.segmentationDataUID, + activeSegmentIndex: globalState.activeSegmentIndex, + } +} + +/** + * Set the active segmentation for the given tool group for all its viewports + * + * @param {string} toolGroupUID - The ID of the tool group to set the active + * segmentation for. + * @param {string} segmentationDataUID - The UID of the segmentation data to set as + * active. + */ +function setActiveSegmentation( + toolGroupUID: string, + segmentationDataUID: string +): void { + setActiveSegmentationData(toolGroupUID, segmentationDataUID) +} + +export { + // get + getActiveSegmentationInfo, + // set + setActiveSegmentation, +} + +export default { + // get + getActiveSegmentationInfo, + // set + setActiveSegmentation, +} diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/addSegmentationToElement.ts b/packages/cornerstone-tools/src/store/SegmentationModule/addSegmentationToElement.ts new file mode 100644 index 000000000..9bbe4a023 --- /dev/null +++ b/packages/cornerstone-tools/src/store/SegmentationModule/addSegmentationToElement.ts @@ -0,0 +1,58 @@ +import { + getEnabledElement, + addVolumesOnViewports, +} from '@precisionmetrics/cornerstone-render' + +import SegmentationRepresentations from '../../enums/SegmentationRepresentations' +import { ToolGroupSpecificSegmentationData } from '../../types/SegmentationStateTypes' + +/** + * It adds a segmentation data to the viewport's HTML Element. NOTE: This function + * should not be called directly. You should use addSegmentationToToolGroup instead. + * Remember that segmentations are not added directly to the viewport's HTML Element, + * you should create a toolGroup on the viewports and add the segmentation to the + * toolGroup. + * + * @param {HTMLElement} element - The element that will be rendered. + * @param {ToolGroupSpecificSegmentationData} segmentationData - + * ToolGroupSpecificSegmentationData + */ +async function addSegmentationToElement( + element: HTMLElement, + segmentationData: ToolGroupSpecificSegmentationData +): Promise { + if (!element || !segmentationData) { + throw new Error('You need to provide an element and a segmentation') + } + + const enabledElement = getEnabledElement(element) + const { renderingEngine, viewport } = enabledElement + const { uid: viewportUID } = viewport + + // Default to true since we are setting a new segmentation, however, + // in the event listener, we will make other segmentations visible/invisible + // based on the config + const visibility = true + + const { representation, segmentationDataUID } = segmentationData + + if (representation.type === SegmentationRepresentations.Labelmap) { + const { volumeUID } = segmentationData + // Add labelmap volumes to the viewports to be be rendered, but not force the render + await addVolumesOnViewports( + renderingEngine, + [ + { + volumeUID, + actorUID: segmentationDataUID, + visibility, + }, + ], + [viewportUID] + ) + } else { + throw new Error('Only labelmap representation is supported for now') + } +} + +export default addSegmentationToElement diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/colorLUT.ts b/packages/cornerstone-tools/src/store/SegmentationModule/colorLUT.ts deleted file mode 100644 index 049a4552a..000000000 --- a/packages/cornerstone-tools/src/store/SegmentationModule/colorLUT.ts +++ /dev/null @@ -1,175 +0,0 @@ -import state, { Color, ColorLUT, getLabelmapStateForElement } from './state' -import config from './segmentationConfig' - -/** - * SetColorLUT - Sets the labelmap to a specific LUT, or generates a new LUT. - * - * @param {number} labelmapIndex The labelmap index to apply the color LUT to. - * @param {number[][]} [colorLUT] An array of The colorLUT to set. - * @returns {null} - */ -export function setColorLUT( - colorLUTIndex: number, - colorLUT: ColorLUT = [] -): void { - const { segmentsPerLabelmap } = config - - if (colorLUT) { - _checkColorLUTLength(colorLUT, segmentsPerLabelmap) - - if (colorLUT.length < segmentsPerLabelmap) { - colorLUT = [ - ...colorLUT, - ..._generateNewColorLUT(segmentsPerLabelmap - colorLUT.length), - ] - } - } else { - // Auto-generates colorLUT. - colorLUT = colorLUT || _generateNewColorLUT(segmentsPerLabelmap) - } - - // Append the "zero" (no label) color to the front of the LUT. - colorLUT.unshift([0, 0, 0, 0]) - - state.colorLutTables[colorLUTIndex] = colorLUT -} - -export function setColorLUTIndexForLabelmap(labelmap, colorLUTIndex) { - // Todo - // labelmap3D.colorLUTIndex = colorLUTIndex -} - -export function getColorForSegmentIndex( - element: HTMLElement, - segmentIndex: number, - labelmapIndex?: number -): Color { - const colorLUT = getColorLUT(element, labelmapIndex) - return colorLUT[segmentIndex] -} - -/** - * Sets a single color of a colorLUT. - * - * @param {Object|number} labelmap3DOrColorLUTIndex Either a `Labelmap3D` object (who's referenced colorLUT will be changed), or a colorLUTIndex. - * @param {number} segmentIndex The segmentIndex color to change. - * @param {number[]} colorArray The color values in RGBA array format (required length 4). - */ -export function setColorForSegmentIndexOfColorLUT( - labelmapOrColorLUTIndex, - segmentIndex, - colorArray -) { - // Todo - // const colorLUT = getColorLUT(labelmapOrColorLUTIndex) - // colorLUT[segmentIndex] = colorArray -} - -export function getColorLUT( - element: HTMLElement, - labelmapIndex?: number -): ColorLUT { - const viewportLabelmapState = getLabelmapStateForElement( - element, - labelmapIndex - ) - return state.colorLutTables[viewportLabelmapState.colorLUTIndex] -} - -/** - * Checks the length of `colorLUT` compared to `segmentsPerLabelmap` and flags up any warnings. - * @param {number[][]} colorLUT - * @param {number} segmentsPerLabelmap - * @returns {boolean} Whether the length is valid. - */ -function _checkColorLUTLength(colorLUT: ColorLUT, segmentsPerLabelmap: number) { - if (colorLUT.length < segmentsPerLabelmap) { - console.warn( - `The provided colorLUT only provides ${colorLUT.length} labels, whereas segmentsPerLabelmap is set to ${segmentsPerLabelmap}. Autogenerating the rest.` - ) - } else if (colorLUT.length > segmentsPerLabelmap) { - console.warn( - `segmentsPerLabelmap is set to ${segmentsPerLabelmap}, and the provided colorLUT provides ${colorLUT.length}. Using the first ${segmentsPerLabelmap} colors from the LUT.` - ) - } -} - -/** - * Generates a new color LUT (Look Up Table) of length `numberOfColors`, - * which returns an RGBA color for each segment index. - * - * @param {Number} numberOfColors = 255 The number of colors to generate - * @returns {Number[][]} The array of RGB values. - */ -function _generateNewColorLUT(numberOfColors = 255) { - const rgbArr = [] - - for (let i = 0; i < numberOfColors; i++) { - rgbArr.push(getRGBAfromHSLA(getNextHue(), getNextL())) - } - - return rgbArr -} - -const goldenAngle = 137.5 -let hueValue = 222.5 - -function getNextHue() { - hueValue += goldenAngle - - if (hueValue >= 360) { - hueValue -= 360 - } - - return hueValue -} - -let l = 0.6 -const maxL = 0.82 -const minL = 0.3 -const incL = 0.07 - -function getNextL() { - l += incL - - if (l > maxL) { - const diff = l - maxL - - l = minL + diff - } - - return l -} - -/** - * GetRGBAfromHSL - Returns an RGBA color given H, S, L and A. - * - * @param {Number} hue The hue. - * @param {Number} s = 1 The saturation. - * @param {Number} l = 0.6 The lightness. - * @param {Number} alpha = 255 The alpha. - * @returns {Number[]} The RGBA formatted color. - */ -function getRGBAfromHSLA(hue, s = 1, l = 0.6, alpha = 255) { - const c = (1 - Math.abs(2 * l - 1)) * s - const x = c * (1 - Math.abs(((hue / 60) % 2) - 1)) - const m = l - c / 2 - - let r, g, b - - if (hue < 60) { - ;[r, g, b] = [c, x, 0] - } else if (hue < 120) { - ;[r, g, b] = [x, c, 0] - } else if (hue < 180) { - ;[r, g, b] = [0, c, x] - } else if (hue < 240) { - ;[r, g, b] = [0, x, c] - } else if (hue < 300) { - ;[r, g, b] = [x, 0, c] - } else if (hue < 360) { - ;[r, g, b] = [c, 0, x] - } - - return [(r + m) * 255, (g + m) * 255, (b + m) * 255, alpha] -} diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/addEmptySegmentationVolumeForViewport.ts b/packages/cornerstone-tools/src/store/SegmentationModule/createNewSegmentationForViewport.ts similarity index 58% rename from packages/cornerstone-tools/src/store/SegmentationModule/addEmptySegmentationVolumeForViewport.ts rename to packages/cornerstone-tools/src/store/SegmentationModule/createNewSegmentationForViewport.ts index f801a9839..0960c0bbc 100644 --- a/packages/cornerstone-tools/src/store/SegmentationModule/addEmptySegmentationVolumeForViewport.ts +++ b/packages/cornerstone-tools/src/store/SegmentationModule/createNewSegmentationForViewport.ts @@ -7,8 +7,7 @@ import { } from '@precisionmetrics/cornerstone-render' import { Point3 } from '../../types' -import setLabelmapForElement from './setLabelmapForElement' -import { getNextLabelmapIndex } from './activeLabelmapController' +import { uuidv4 } from '../../util' type LabelmapOptions = { volumeUID?: string @@ -22,11 +21,17 @@ type LabelmapOptions = { origin?: Point3 direction?: Float32Array } + /** - * It renders a labelmap 3D volume into the viewport that the element belongs to - * @param {element, labelmap, callback, labelmapIndex, immediateRender} + * Create a new 3D segmentation volume from the default imageData presented in the + * viewport. It looks at the metadata of the imageData to determine the volume + * dimensions and spacing if particular options are not provided. + * + * @param {VolumeViewport} viewport - VolumeViewport + * @param {LabelmapOptions} [options] - LabelmapOptions + * @returns A promise that resolves to the UID of the new labelmap. */ -async function addEmptySegmentationVolumeForViewport( +async function createNewSegmentationForViewport( viewport: VolumeViewport, options?: LabelmapOptions ): Promise { @@ -41,32 +46,26 @@ async function addEmptySegmentationVolumeForViewport( throw new Error('Segmentation not ready for stackViewport') } - // Create a new labelmap at the labelmapIndex, If there is no labelmap at that index const { uid } = viewport.getDefaultActor() - const segmentationIndex = getNextLabelmapIndex(element) - const segmentationUID = `${viewport.uid}:-${uid}-segmentation-${segmentationIndex}` + // Name the segmentation volume with the viewport UID + const segmentationUID = `${uid}-based-segmentation-${ + options?.volumeUID ?? uuidv4().slice(0, 8) + }` - let segmentation if (options) { // create a new labelmap with its own properties // This allows creation of a higher resolution labelmap vs reference volume const properties = _cloneDeep(options) - segmentation = await createLocalVolume(properties, segmentationUID) + await createLocalVolume(properties, segmentationUID) } else { // create a labelmap from a reference volume const { uid: volumeUID } = viewport.getDefaultActor() - segmentation = await createAndCacheDerivedVolume(volumeUID, { + await createAndCacheDerivedVolume(volumeUID, { uid: segmentationUID, }) } - await setLabelmapForElement({ - element: viewport.element, - labelmap: segmentation, - labelmapIndex: segmentationIndex, - }) - return segmentationUID } -export default addEmptySegmentationVolumeForViewport +export default createNewSegmentationForViewport diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/hideSegmentController.ts b/packages/cornerstone-tools/src/store/SegmentationModule/hideSegmentController.ts deleted file mode 100644 index 64da6f762..000000000 --- a/packages/cornerstone-tools/src/store/SegmentationModule/hideSegmentController.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getLabelmapsStateForElement } from './state' - -import { getEnabledElement } from '@precisionmetrics/cornerstone-render' - -/** - * Toggles the visibility of a segmentation - * @param element HTML element - * @param labelmapUID UID of the labelmap to toggle - * @returns - */ -function toggleSegmentationVisibility( - element: HTMLElement, - labelmapUID?: string -): void { - const { labelmaps } = getLabelmapsStateForElement(element) - const { viewport } = getEnabledElement(element) - - let labelmapsToUpdate = labelmaps - - if (labelmapUID) { - labelmapsToUpdate = labelmaps.filter((l) => l.volumeUID === labelmapUID) - } - - labelmapsToUpdate.forEach((labelmap) => { - const { visibility, volumeUID } = labelmap - const { volumeActor } = viewport.getActor(volumeUID) - - if (!volumeActor) { - throw new Error(`Volume actor for labelmapUID ${labelmapUID} not found`) - } - - // toggle visibility for the labelmap - const visibilityToSet = !visibility - volumeActor.setVisibility(visibilityToSet) - labelmap.visibility = visibilityToSet - viewport.render() - }) -} - -export { toggleSegmentationVisibility } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/index.ts b/packages/cornerstone-tools/src/store/SegmentationModule/index.ts index 1daf1bb4d..9f610030a 100644 --- a/packages/cornerstone-tools/src/store/SegmentationModule/index.ts +++ b/packages/cornerstone-tools/src/store/SegmentationModule/index.ts @@ -1,72 +1,47 @@ -import setLabelmapForElement from './setLabelmapForElement' +import lockedSegmentController from './lockedSegmentController' +import segmentIndexController from './segmentIndexController' +import activeSegmentationController from './activeSegmentationController' +import segmentationVisibilityController from './segmentationVisibilityController' +import segmentationColorController from './segmentationColorController' +import segmentationConfigController from './segmentationConfigController' +import createNewSegmentationForViewport from './createNewSegmentationForViewport' import { - removeLabelmapForElement, - removeLabelmapForAllElements, -} from './removeLabelmapForElement' - -import { getGlobalStateForLabelmapUID, setLabelmapGlobalState } from './state' - -import { getLabelmapUIDsForElement, getLabelmapUIDForElement } from './utils' -import * as lockedSegmentController from './lockedSegmentController' -import * as segmentIndexController from './segmentIndexController' -import * as activeLabelmapController from './activeLabelmapController' -import * as hideSegmentController from './hideSegmentController' -import config, { setGlobalConfig } from './segmentationConfig' -import addEmptySegmentationVolumeForViewport from './addEmptySegmentationVolumeForViewport' -import { setColorLUT, getColorForSegmentIndex } from './colorLUT' + triggerSegmentationStateModified, + triggerSegmentationGlobalStateModified, + triggerSegmentationDataModified, +} from './triggerSegmentationEvents' export { - setLabelmapForElement, - removeLabelmapForElement, - removeLabelmapForAllElements, - addEmptySegmentationVolumeForViewport, - getLabelmapUIDForElement, - setColorLUT, - getColorForSegmentIndex, - config, - setGlobalConfig, + createNewSegmentationForViewport, lockedSegmentController, segmentIndexController, - activeLabelmapController, - hideSegmentController, - getGlobalStateForLabelmapUID, - setLabelmapGlobalState, + activeSegmentationController, + segmentationVisibilityController, + segmentationColorController, + segmentationConfigController, + triggerSegmentationStateModified, + triggerSegmentationGlobalStateModified, + triggerSegmentationDataModified, } export default { - // Set labelmap for element - setLabelmapForElement, - removeLabelmapForElement, - removeLabelmapForAllElements, - addEmptySegmentationVolumeForViewport, - - // Set/Get Labelmap - getLabelmapUIDsForElement, - - // active labelmap utils - activeLabelmapController, - + createNewSegmentationForViewport, + activeSegmentationController, // - hideSegmentController, + segmentationVisibilityController, + segmentationColorController, // Segment index utils segmentIndexController, - // Config - config, - setGlobalConfig, - - // ColorLUT - setColorLUT, - getColorForSegmentIndex, - - // Utils - getLabelmapUIDForElement, - // Locked segment index lockedSegmentController, - // state - getGlobalStateForLabelmapUID, - setLabelmapGlobalState, + // Configuration controller + segmentationConfigController, + + // triggers + triggerSegmentationStateModified, + triggerSegmentationGlobalStateModified, + triggerSegmentationDataModified, } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/lockedSegmentController.ts b/packages/cornerstone-tools/src/store/SegmentationModule/lockedSegmentController.ts index dcac553c5..c9bc8a898 100644 --- a/packages/cornerstone-tools/src/store/SegmentationModule/lockedSegmentController.ts +++ b/packages/cornerstone-tools/src/store/SegmentationModule/lockedSegmentController.ts @@ -1,130 +1,159 @@ -import { getGlobalStateForLabelmapUID } from './state' +import { getActiveSegmentationInfo } from './activeSegmentationController' -import { getLabelmapUIDForElement } from './utils' +import { getGlobalSegmentationDataByUID } from '../../stateManagement/segmentation/segmentationState' +import { triggerSegmentationGlobalStateModified } from './triggerSegmentationEvents' /** - * Returns the lock status of the segment index for the element's labelmapIndex-th labelmap. - * If no labelmapIndex is provided it uses the active labelmap - * @param element HTML element - * @param segmentIndex segment Index - * @param labelmapIndex? labelmap Index - * @returns + * Get the locked status of a segment index in a segmentation + * + * @param {string} toolGroupUID - The UID of the tool group that contains the + * segmentation. + * @param {number} segmentIndex - The index of the segment + * @returns A boolean value indicating whether the segment is locked or not for modification */ -function getSegmentIndexLockedStatusForElement( - element: HTMLElement, - segmentIndex: number, - labelmapIndex?: number +// Todo: should this be based on a segmentationUID instead of a toolGroupUID? +function getSegmentIndexLockedStatus( + toolGroupUID: string, + segmentIndex: number ): boolean { - const labelmapUID = getLabelmapUIDForElement(element, labelmapIndex) - const labelmapGlobalState = getGlobalStateForLabelmapUID(labelmapUID) + const activeSegmentationInfo = getActiveSegmentationInfo(toolGroupUID) - if (!labelmapGlobalState) { - return false + if (!activeSegmentationInfo) { + throw new Error('element does not contain an active segmentation') } - return labelmapGlobalState.segmentsLocked.has(segmentIndex) -} + const { volumeUID: segmentationUID } = activeSegmentationInfo + const segmentationGlobalState = + getGlobalSegmentationDataByUID(segmentationUID) -/** - * Returns the locked segments for the element's labelmapIndex-th labelmap - * If no labelmapIndex is provided it uses the active labelmap - * - * @param element HTML element - * @param labelmapIndex labelmap Index - * @returns - */ -function getLockedSegmentsForElement( - element: HTMLElement, - labelmapIndex?: number -): number[] { - const labelmapUID = getLabelmapUIDForElement(element, labelmapIndex) - const labelmapGlobalState = getGlobalStateForLabelmapUID(labelmapUID) - return Array.from(labelmapGlobalState.segmentsLocked) + const lockedSegments = segmentationGlobalState.segmentsLocked + + return lockedSegments.has(segmentIndex) } /** - * Toggles the locked status of segments for the element's labelmapIndex-th labelmap - * If no labelmapIndex is provided it uses the active labelmap - * @param element HTML element - * @param segmentIndex segment index - * @param labelmapIndex labelmap index - * @returns + * Set the locked status of a segment in a segmentation globally. It fires + * a global state modified event. + * + * @event {SegmentationGlobalStateModifiedEvent} + * @param {string} toolGroupUID - the UID of the tool group that contains the + * segmentation + * @param {number} segmentIndex - the index of the segment to lock/unlock + * @param {boolean} locked - boolean */ -function toggleSegmentIndexLockedForElement( - element: HTMLElement, +// Todo: shouldn't this be a based on a segmentationUID instead of a toolGroupUID? +function setSegmentIndexLockedStatus( + toolGroupUID: string, segmentIndex: number, - labelmapIndex?: number + locked: boolean ): void { - const labelmapUID = getLabelmapUIDForElement(element, labelmapIndex) - const lockedStatus = getSegmentIndexLockedStatusForElement( - element, - segmentIndex, - labelmapIndex - ) + const activeSegmentationInfo = getActiveSegmentationInfo(toolGroupUID) - const labelmapGlobalState = getGlobalStateForLabelmapUID(labelmapUID) + if (!activeSegmentationInfo) { + throw new Error('element does not contain an active segmentation') + } + + const { volumeUID: segmentationUID } = activeSegmentationInfo - const toggledStatus = !lockedStatus + const segmentationGlobalState = + getGlobalSegmentationDataByUID(segmentationUID) - if (toggledStatus === true) { - labelmapGlobalState.segmentsLocked.add(segmentIndex) - return + const { segmentsLocked } = segmentationGlobalState + + if (locked) { + segmentsLocked.add(segmentIndex) + } else { + segmentsLocked.delete(segmentIndex) } - labelmapGlobalState.segmentsLocked.delete(segmentIndex) + triggerSegmentationGlobalStateModified(segmentationUID) } /** - * Toggles the locked status of segments for labelmapUID - * @param labelmapUID labelmap volumeUID - * @param segmentIndex segment index - * @returns + * Get the locked status for a segment index in a segmentation + * @param {string} segmentationUID - The UID of the segmentation that the segment + * belongs to. + * @param {number} segmentIndex - The index of the segment + * @returns A boolean value indicating whether the segment is locked or not. */ -function toggleSegmentIndexLockedForLabelmapUID( - labelmapUID: string, +function getSegmentIndexLockedStatusForSegmentation( + segmentationUID: string, segmentIndex: number +): boolean { + const globalState = getGlobalSegmentationDataByUID(segmentationUID) + + if (!globalState) { + throw new Error(`No segmentation state found for ${segmentationUID}`) + } + + const { segmentsLocked } = globalState + return segmentsLocked.has(segmentIndex) +} + +/** + * Set the locked status of a segment index in a segmentation + * @param {string} segmentationUID - The UID of the segmentation whose segment + * index is being modified. + * @param {number} segmentIndex - The index of the segment to lock/unlock. + */ +function setSegmentIndexLockedStatusForSegmentation( + segmentationUID: string, + segmentIndex: number, + locked: boolean ): void { - if (!labelmapUID) { - throw new Error('LabelmapUID should be provided') + const segmentationGlobalState = + getGlobalSegmentationDataByUID(segmentationUID) + + if (!segmentationGlobalState) { + throw new Error(`No segmentation state found for ${segmentationUID}`) } - const { segmentsLocked } = getGlobalStateForLabelmapUID(labelmapUID) - if (segmentsLocked.has(segmentIndex)) { - segmentsLocked.delete(segmentIndex) - } else { + const { segmentsLocked } = segmentationGlobalState + + if (locked) { segmentsLocked.add(segmentIndex) + } else { + segmentsLocked.delete(segmentIndex) } + + triggerSegmentationGlobalStateModified(segmentationUID) } /** - * Returns an array of locked segment indices for the provided labelmapUID - * @param labelmapUID Labelmap volumeUID - * @returns + * Get the locked segments for a segmentation + * @param {string} segmentationUID - The UID of the segmentation to get locked + * segments for. + * @returns An array of locked segment indices. */ -function getLockedSegmentsForLabelmapUID(labelmapUID: string): number[] { - const { segmentsLocked } = getGlobalStateForLabelmapUID(labelmapUID) +function getLockedSegmentsForSegmentation( + segmentationUID: string +): number[] | [] { + const globalState = getGlobalSegmentationDataByUID(segmentationUID) + + if (!globalState) { + throw new Error(`No segmentation state found for ${segmentationUID}`) + } + + const { segmentsLocked } = globalState return Array.from(segmentsLocked) } -// lockedSegmentController export { - // get - getLockedSegmentsForLabelmapUID, - getLockedSegmentsForElement, - // toggling lock - toggleSegmentIndexLockedForLabelmapUID, - toggleSegmentIndexLockedForElement, - // - getSegmentIndexLockedStatusForElement, + // toolGroup active segmentation + getSegmentIndexLockedStatus, + setSegmentIndexLockedStatus, + // set + getSegmentIndexLockedStatusForSegmentation, + setSegmentIndexLockedStatusForSegmentation, + getLockedSegmentsForSegmentation, } export default { - // get - getLockedSegmentsForLabelmapUID, - getLockedSegmentsForElement, - // toggling lock - toggleSegmentIndexLockedForLabelmapUID, - toggleSegmentIndexLockedForElement, - // - getSegmentIndexLockedStatusForElement, + // toolGroup active segmentation + getSegmentIndexLockedStatus, + setSegmentIndexLockedStatus, + // set + getSegmentIndexLockedStatusForSegmentation, + setSegmentIndexLockedStatusForSegmentation, + getLockedSegmentsForSegmentation, } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/removeLabelmapForElement.ts b/packages/cornerstone-tools/src/store/SegmentationModule/removeLabelmapForElement.ts deleted file mode 100644 index ada555856..000000000 --- a/packages/cornerstone-tools/src/store/SegmentationModule/removeLabelmapForElement.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - getEnabledElement, - triggerEvent, - StackViewport, - cache, - getRenderingEngines, - getVolumeViewportsContatiningSameVolumes, -} from '@precisionmetrics/cornerstone-render' - -import { CornerstoneTools3DEvents as EVENTS } from '../../enums' - -import state, { removeLabelmapFromGlobalState } from './state' - -function removeLabelmapForAllElements( - labelmapUID: string, - removeFromCache = false -): void { - const viewportUIDs = Object.keys(state.volumeViewports) - - // Todo: better way to get elements from viewportUIDs - const renderingEngine = getRenderingEngines()[0] - - viewportUIDs.forEach((viewportUID) => { - const { element } = renderingEngine.getViewport(viewportUID) - removeLabelmapForElement(element, labelmapUID, removeFromCache) - }) -} - -function removeLabelmapForElement( - element: HTMLElement, - labelmapUID: string, - removeFromCache = false -): void { - const enabledElement = getEnabledElement(element) - const { viewport } = enabledElement - - // StackViewport Implementation - if (viewport instanceof StackViewport) { - throw new Error('Segmentation for StackViewport is not supported yet') - } - - const allViewportsWithLabelmap = [viewport] - - // Updating viewport-specific labelmap states - allViewportsWithLabelmap.forEach((viewport) => { - viewport.removeVolumes([labelmapUID]) - const viewportLabelmapsState = state.volumeViewports[viewport.uid] - - if (!viewportLabelmapsState) { - return - } - - // check which labelmap index is associated with the labelmap UID - const labelmapIndex = viewportLabelmapsState.labelmaps.findIndex( - (labelmap) => labelmap.volumeUID === labelmapUID - ) - - if (labelmapIndex === -1) { - return - } - - // if viewport's current activeLabelmapIndex is the same as the labelmapIndex - // then we need to update the activeLabelmapIndex to index 0, after - // we remove the labelmap from viewport's state. - // Todo: this should move somewehere else - const removingActiveLabelmap = - viewportLabelmapsState.activeLabelmapIndex === labelmapIndex - - // remove the labelmap from the viewport's state - viewportLabelmapsState.labelmaps.splice(labelmapIndex, 1) - - // if there are labelmaps left in the viewport's state - if (viewportLabelmapsState.labelmaps.length > 0 && removingActiveLabelmap) { - viewportLabelmapsState.activeLabelmapIndex = 0 - } else { - delete state.volumeViewports[viewport.uid] - } - }) - - const eventData = { - element, - labelmapUID, - } - - triggerEvent(element, EVENTS.LABELMAP_REMOVED, eventData) - - // remove the labelmap from the cache - if (removeFromCache) { - if (cache.getVolumeLoadObject(labelmapUID)) { - cache.removeVolumeLoadObject(labelmapUID) - } - removeLabelmapFromGlobalState(labelmapUID) - } -} - -export { removeLabelmapForElement, removeLabelmapForAllElements } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/removeSegmentationFromElement.ts b/packages/cornerstone-tools/src/store/SegmentationModule/removeSegmentationFromElement.ts new file mode 100644 index 000000000..b26242481 --- /dev/null +++ b/packages/cornerstone-tools/src/store/SegmentationModule/removeSegmentationFromElement.ts @@ -0,0 +1,37 @@ +import { + getEnabledElement, + VolumeViewport, +} from '@precisionmetrics/cornerstone-render' +import { ToolGroupSpecificSegmentationData } from '../../types/SegmentationStateTypes' +import SegmentationRepresentations from '../../enums/SegmentationRepresentations' + +/** + * Remove the segmentation from the viewport's HTML Element. + * NOTE: This function should not be called directly. You should use removeSegmentationFromToolGroup instead. + * Remember that segmentations are not removed directly to the viewport's HTML Element, + * you should use the toolGroups to do that + * + * @param {HTMLElement} element - The element that the segmentation is being added + * to. + * @param {ToolGroupSpecificSegmentationData} segmentationData - + * ToolGroupSpecificSegmentationData + * @param [removeFromCache=false] - boolean + */ +function removeSegmentationFromElement( + element: HTMLElement, + segmentationData: ToolGroupSpecificSegmentationData, + removeFromCache = false // Todo +): void { + const enabledElement = getEnabledElement(element) + const { viewport } = enabledElement + + const { representation, segmentationDataUID } = segmentationData + + if (representation.type === SegmentationRepresentations.Labelmap) { + ;(viewport as VolumeViewport).removeVolumeActors([segmentationDataUID]) + } else { + throw new Error('Only labelmap representation is supported for now') + } +} + +export default removeSegmentationFromElement diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/segmentIndexController.ts b/packages/cornerstone-tools/src/store/SegmentationModule/segmentIndexController.ts index 275f681e6..aeb103e37 100644 --- a/packages/cornerstone-tools/src/store/SegmentationModule/segmentIndexController.ts +++ b/packages/cornerstone-tools/src/store/SegmentationModule/segmentIndexController.ts @@ -1,103 +1,114 @@ -import { - getEnabledElement, - VolumeViewport, -} from '@precisionmetrics/cornerstone-render' - -import state, { - getActiveLabelmapState, - getGlobalStateForLabelmapUID, -} from './state' -import { getActiveLabelmapUID } from './activeLabelmapController' +import { getActiveSegmentationInfo } from './activeSegmentationController' +import { getGlobalSegmentationDataByUID } from '../../stateManagement/segmentation/segmentationState' +import { triggerSegmentationGlobalStateModified } from '.' /** - * Returns the index of the active Segment for the current active labelmap + * Returns the active segment index for the active segmentation in the tool group * - * @param {HTMLElement} element HTML element - * @returns {number} The active segment index + * @param {string} toolGroupUID - The UID of the tool group that contains an + * active segmentation. + * @returns The active segment index. */ -function getActiveSegmentIndex(element: HTMLElement): number | undefined { - const viewportLabelmapState = getActiveLabelmapState(element) +function getActiveSegmentIndex(toolGroupUID: string): number | undefined { + const segmentationInfo = getActiveSegmentationInfo(toolGroupUID) - if (!viewportLabelmapState) { - // Todo: check this - return 1 + if (!segmentationInfo) { + throw new Error('toolGroup does not contain an active segmentation') } - const activeLabelmapGlobalState = getGlobalStateForLabelmapUID( - viewportLabelmapState.volumeUID - ) + const { volumeUID } = segmentationInfo + const activeSegmentationGlobalState = + getGlobalSegmentationDataByUID(volumeUID) - if (activeLabelmapGlobalState) { - return activeLabelmapGlobalState.activeSegmentIndex + if (activeSegmentationGlobalState) { + return activeSegmentationGlobalState.activeSegmentIndex } } /** - * Returns the active segment index for the element based on the labelmapUID it renders - * @param element HTML element - * @param labelmapUID volumeUID of the labelmap - * @returns - */ -function getActiveSegmentIndexForLabelmapUID(labelmapUID: string): number { - const activeLabelmapGlobalState = getGlobalStateForLabelmapUID(labelmapUID) - return activeLabelmapGlobalState.activeSegmentIndex -} - -/** - * Sets the active `segmentIndex` for the active labelmap of the HTML element + * Set the active segment index for the active segmentation of the toolGroup. + * It fires a global state modified event. * - * - * @param {HTMLElement} element HTML Element - * @param {number} segmentIndex = 1 SegmentIndex - * @returns {string} + * @event {SEGMENTATION_GLOBAL_STATE_MODIFIED} + * @param {string} toolGroupUID - The UID of the tool group that contains the + * segmentation. + * @param {number} segmentIndex - The index of the segment to be activated. */ function setActiveSegmentIndex( - element: HTMLElement, - segmentIndex = 1 -): Promise { - const enabledElement = getEnabledElement(element) + toolGroupUID: string, + segmentIndex: number +): void { + const segmentationInfo = getActiveSegmentationInfo(toolGroupUID) - if (!enabledElement) { - return + if (!segmentationInfo) { + throw new Error('element does not contain an active segmentation') } - const { viewport, viewportUID } = enabledElement + const { volumeUID: segmentationUID } = segmentationInfo + const activeSegmentationGlobalState = + getGlobalSegmentationDataByUID(segmentationUID) - // stackViewport - if (!(viewport instanceof VolumeViewport)) { - throw new Error('Segmentation for StackViewport is not supported yet') - } + if (activeSegmentationGlobalState?.activeSegmentIndex !== segmentIndex) { + activeSegmentationGlobalState.activeSegmentIndex = segmentIndex - // volumeViewport - // Todo: should this initialize the state when no labelmaps? I don't think so - if (!state.volumeViewports[viewportUID]) { - throw new Error( - 'Canvas does not contain an active labelmap, create one first before setting the segment Index' - ) + triggerSegmentationGlobalStateModified(segmentationUID) } +} + +/** + * Set the active segment index for a segmentation UID. It fires a global state + * modified event. + * + * @event {SEGMENTATION_GLOBAL_STATE_MODIFIED} + * @param {string} segmentationUID - The UID of the segmentation that the segment + * belongs to. + * @param {number} segmentIndex - The index of the segment to be activated. + */ +function setActiveSegmentIndexForSegmentation( + segmentationUID: string, + segmentIndex: number +): void { + const activeSegmentationGlobalState = + getGlobalSegmentationDataByUID(segmentationUID) - const activeLabelmapUID = getActiveLabelmapUID(element) + if (activeSegmentationGlobalState?.activeSegmentIndex !== segmentIndex) { + activeSegmentationGlobalState.activeSegmentIndex = segmentIndex - const labelmapGlobalState = getGlobalStateForLabelmapUID(activeLabelmapUID) + triggerSegmentationGlobalStateModified(segmentationUID) + } +} - if (labelmapGlobalState) { - labelmapGlobalState.activeSegmentIndex = segmentIndex +/** + * Get the active segment index for a segmentation in the global state + * @param {string} segmentationUID - The UID of the segmentation to get the active + * segment index from. + * @returns The active segment index for the given segmentation. + */ +function getActiveSegmentIndexForSegmentation( + segmentationUID: string +): number | undefined { + const activeSegmentationGlobalState = + getGlobalSegmentationDataByUID(segmentationUID) + + if (activeSegmentationGlobalState) { + return activeSegmentationGlobalState.activeSegmentIndex } } -// segmentIndexController -export { - // get +export default { + // toolGroup Active Segmentation getActiveSegmentIndex, - getActiveSegmentIndexForLabelmapUID, - // set setActiveSegmentIndex, + // global segmentation + getActiveSegmentIndexForSegmentation, + setActiveSegmentIndexForSegmentation, } -export default { - // get +export { + // toolGroup Active Segmentation getActiveSegmentIndex, - getActiveSegmentIndexForLabelmapUID, - // set setActiveSegmentIndex, + // global segmentation + getActiveSegmentIndexForSegmentation, + setActiveSegmentIndexForSegmentation, } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/segmentationColorController.ts b/packages/cornerstone-tools/src/store/SegmentationModule/segmentationColorController.ts new file mode 100644 index 000000000..3b0d1eb31 --- /dev/null +++ b/packages/cornerstone-tools/src/store/SegmentationModule/segmentationColorController.ts @@ -0,0 +1,51 @@ +import * as SegmentationState from '../../stateManagement/segmentation/segmentationState' + +import { Color } from '../../types/SegmentationStateTypes' + +/** + * Given a tool group UID, a segmentation data UID, and a segment index, return the + * color for that segment. It can be used for segmentation tools that need to + * display the color of their annotation. + * + * @param {string} toolGroupUID - The UID of the tool group that owns the + * segmentation data. + * @param {string} segmentationDataUID - The UID of the segmentation data + * @param {number} segmentIndex - The index of the segment in the segmentation + * @returns A color. + */ +function getColorForSegmentIndex( + toolGroupUID: string, + segmentationDataUID: string, + segmentIndex: number +): Color { + const segmentationData = SegmentationState.getSegmentationDataByUID( + toolGroupUID, + segmentationDataUID + ) + + if (!segmentationData) { + throw new Error( + `Segmentation data with UID ${segmentationDataUID} does not exist for tool group ${toolGroupUID}` + ) + } + + const { colorLUTIndex } = segmentationData + + // get colorLUT + const colorLut = SegmentationState.getColorLut(colorLUTIndex) + return colorLut[segmentIndex] +} + +/** + * Add a color LUT to the segmentation state to be used by the segmentations + * @param {Color[]} colorLUT - A list of colors to be added to the color lookup + * table. + * @param {number} colorLUTIndex - The index of the color LUT in the state to be + * updated. + */ +function addColorLut(colorLUT: Color[], colorLUTIndex = 0): void { + SegmentationState.addColorLut(colorLUT, colorLUTIndex) +} + +export default { getColorForSegmentIndex, addColorLut } +export { getColorForSegmentIndex } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/segmentationConfig.ts b/packages/cornerstone-tools/src/store/SegmentationModule/segmentationConfig.ts deleted file mode 100644 index f20e580e6..000000000 --- a/packages/cornerstone-tools/src/store/SegmentationModule/segmentationConfig.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { triggerLabelmapsStateUpdated } from './triggerLabelmapStateUpdated' - -export interface ISegmentationConfig { - enabled: boolean - renderOutline: boolean - outlineWidth: number - outlineWidthActive: number - outlineWidthInactive: number - renderFill: boolean - renderInactiveLabelmaps: boolean - segmentsPerLabelmap: number - fillAlpha: number - fillAlphaInactive: number -} - -const defaultSegmentationConfig: ISegmentationConfig = { - // render labelmaps or not - enabled: true, - renderInactiveLabelmaps: true, - // Outline - renderOutline: true, - outlineWidth: 3, - outlineWidthActive: 3, - outlineWidthInactive: 2, - // Todo: not supported yet - // outlineAlpha: 0.7, - // outlineAlphaInactive: 0.35, - // Fill inside the render maps - renderFill: true, - fillAlpha: 0.9, - fillAlphaInactive: 0.85, - // ColorLUT - segmentsPerLabelmap: 65535, // Todo: max is bigger, but it seems cfun can go upto 255 anyway - // Brush - // radius: 10, - // minRadius: 1, - // maxRadius: 50, - // storeHistory: true, -} - -/** - * Sets the global config for all labelmaps - * @param config segmentation config - * @param immediate re-render labelmaps immediately - */ -function setGlobalConfig( - config: Partial, - immediate = true -): void { - Object.assign(defaultSegmentationConfig, config) - if (immediate) { - // re-render all labelmaps so that the changed config gets applied - triggerLabelmapsStateUpdated() - } -} - -// todo: setting configuration per labelmap (not globally) -function setLabelmapConfig( - element: HTMLElement, - labelmapUID: string, - config: Partial, - immediate = true -): void { - // Look into the state for all the viewports that have the same labelmap in their labelmaps and apply the config -} - -// todo: setting configuration per element (element) for the active labelmap (not globally) -function setElementActiveLabelmapConfig( - element: HTMLElement, - config: Partial, - immediate = true -): void {} - -export default defaultSegmentationConfig -export { setGlobalConfig } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/segmentationConfigController.ts b/packages/cornerstone-tools/src/store/SegmentationModule/segmentationConfigController.ts new file mode 100644 index 000000000..9624fb63e --- /dev/null +++ b/packages/cornerstone-tools/src/store/SegmentationModule/segmentationConfigController.ts @@ -0,0 +1,201 @@ +import SegmentationRepresentations from '../../enums/SegmentationRepresentations' +import * as SegmentationState from '../../stateManagement/segmentation/segmentationState' + +import { + RepresentationConfig, + SegmentationConfig, +} from '../../types/SegmentationStateTypes' + +/** + * It returns the global segmentation config. + * @returns The global segmentation config containing the representations + * config for each representation type and renderInactiveSegmentations flag. + */ +function getGlobalSegmentationConfig(): SegmentationConfig { + const globalConfig = SegmentationState.getGlobalSegmentationConfig() + return globalConfig +} + +/** + * Set the global segmentation config + * @param {SegmentationConfig} segmentationConfig - SegmentationConfig + */ +function setGlobalSegmentationConfig( + segmentationConfig: SegmentationConfig +): void { + SegmentationState.setGlobalSegmentationConfig(segmentationConfig) +} + +/** + * Given a representation type, return the corresponding global representation config + * @param {SegmentationRepresentations} representationType - The type of + * representation to query + * @returns A representation configuration object. + */ +function getGlobalRepresentationConfig( + representationType: SegmentationRepresentations +): RepresentationConfig { + const globalConfig = getGlobalSegmentationConfig() + return globalConfig.representations[representationType] +} + +/** + * Set the global configuration for a given representation type. It fires + * a SEGMENTATION_GLOBAL_STATE_MODIFIED event. + * + * @event {SEGMENTATION_GLOBAL_STATE_MODIFIED} + * @param {SegmentationRepresentations} representationType - The type of + * representation to set config for + * @param {RepresentationConfig} config - The configuration for the representation. + */ +function setGlobalRepresentationConfig( + representationType: SegmentationRepresentations, + config: RepresentationConfig +): void { + const globalConfig = getGlobalSegmentationConfig() + + setGlobalSegmentationConfig({ + ...globalConfig, + representations: { + ...globalConfig.representations, + [representationType]: config, + }, + }) +} + +/** + * It takes a representation type and a partial representation config, and updates + * the global representation config with the partial config. It fires a + * SEGMENTATION_GLOBAL_STATE_MODIFIED event. + * + * @event {SEGMENTATION_GLOBAL_STATE_MODIFIED} + * @param {SegmentationRepresentations} representationType - The type of + * representation to update. + * @param config - Partial + */ +function updateGlobalRepresentationConfig( + representationType: SegmentationRepresentations, + config: Partial +): void { + const representationConfig = getGlobalRepresentationConfig(representationType) + + setGlobalRepresentationConfig(representationType, { + ...representationConfig, + ...config, + }) +} + +/** + * It takes a partial config object and updates the global config with it + * @param config - Partial + */ +function updateGlobalSegmentationConfig( + config: Partial +): void { + const globalConfig = getGlobalSegmentationConfig() + + setGlobalSegmentationConfig({ + ...globalConfig, + ...config, + }) +} + +/** + * Get the toolGroup specific segmentation config + * @param {string} toolGroupUID - The UID of the tool group + * @returns A SegmentationConfig object. + */ +function getSegmentationConfig(toolGroupUID: string): SegmentationConfig { + return SegmentationState.getSegmentationConfig(toolGroupUID) +} + +/** + * Set the toolGroup specific segmentation config. + * It fires a SEGMENTATION_STATE_MODIFIED event. + * + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * config is for. + * @param {SegmentationConfig} segmentationConfig - The segmentation config to set. + */ +function setSegmentationConfig( + toolGroupUID: string, + segmentationConfig: SegmentationConfig +): void { + SegmentationState.setSegmentationConfig(toolGroupUID, segmentationConfig) +} + +/** + * Set the representation config for a given tool group for the given representation type. + * It fires a SEGMENTATION_STATE_MODIFIED event. + * + * @param {string} toolGroupUID - The unique identifier of the tool group. + * @param {SegmentationRepresentations} representationType - The type of + * representation to set config for. + * @param {RepresentationConfig} representationConfig - The configuration for the + * representation. + */ +function setRepresentationConfig( + toolGroupUID: string, + representationType: SegmentationRepresentations, + representationConfig: RepresentationConfig +): void { + const segmentationConfig = + SegmentationState.getSegmentationConfig(toolGroupUID) + + if (segmentationConfig) { + const config = { + ...segmentationConfig, + representations: { + ...segmentationConfig.representations, + [representationType]: representationConfig, + }, + } + + setSegmentationConfig(toolGroupUID, config) + } +} + +/** + * Get the representation config for a given tool group and representation type + * @param {string} toolGroupUID - The UID of the tool group that contains the tool + * that you want to get the representation config for. + * @param {SegmentationRepresentations} representationType - The type of + * representation to get. + * @returns A RepresentationConfig object. + */ +function getRepresentationConfig( + toolGroupUID: string, + representationType: SegmentationRepresentations +): RepresentationConfig { + const segmentationConfig = getSegmentationConfig(toolGroupUID) + + if (segmentationConfig) { + return segmentationConfig.representations[representationType] + } +} + +export { + getGlobalSegmentationConfig, + setGlobalSegmentationConfig, + getGlobalRepresentationConfig, + setGlobalRepresentationConfig, + updateGlobalSegmentationConfig, + updateGlobalRepresentationConfig, + getSegmentationConfig, + setSegmentationConfig, + setRepresentationConfig, + getRepresentationConfig, +} + +export default { + getGlobalSegmentationConfig, + setGlobalSegmentationConfig, + getGlobalRepresentationConfig, + setGlobalRepresentationConfig, + updateGlobalSegmentationConfig, + updateGlobalRepresentationConfig, + getSegmentationConfig, + setSegmentationConfig, + setRepresentationConfig, + getRepresentationConfig, +} diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/segmentationVisibilityController.ts b/packages/cornerstone-tools/src/store/SegmentationModule/segmentationVisibilityController.ts new file mode 100644 index 000000000..8b6c89e3f --- /dev/null +++ b/packages/cornerstone-tools/src/store/SegmentationModule/segmentationVisibilityController.ts @@ -0,0 +1,65 @@ +import { triggerSegmentationStateModified } from './triggerSegmentationEvents' +import { getSegmentationState } from '../../stateManagement/segmentation/segmentationState' +import { ToolGroupSpecificSegmentationData } from '../../types/SegmentationStateTypes' + +/** + * Set the visibility of a segmentation data for a given tool group. It fires + * a SEGMENTATION_STATE_MODIFIED event. + * + * @event {SEGMENTATION_STATE_MODIFIED} + * @param {string} toolGroupUID - The UID of the tool group that contains the + * segmentation. + * @param {string} segmentationDataUID - The UID of the segmentation data to + * modify its visibility. + * @param {boolean} visibility - boolean + */ +function setSegmentationVisibility( + toolGroupUID: string, + segmentationDataUID: string, + visibility: boolean +): void { + const toolGroupSegmentations = getSegmentationState(toolGroupUID) + + if (!toolGroupSegmentations) { + return + } + + toolGroupSegmentations.forEach( + (segmentationData: ToolGroupSpecificSegmentationData) => { + if (segmentationData.segmentationDataUID === segmentationDataUID) { + segmentationData.visibility = visibility + triggerSegmentationStateModified(toolGroupUID) + } + } + ) +} + +/** + * Get the visibility of a segmentation data for a given tool group. + * + * @param {string} toolGroupUID - The UID of the tool group that the segmentation + * data belongs to. + * @param {string} segmentationDataUID - The UID of the segmentation data to get + * @returns A boolean value that indicates whether the segmentation data is visible or + * not on the toolGroup + */ +function getSegmentationVisibility( + toolGroupUID: string, + segmentationDataUID: string +): boolean | undefined { + const toolGroupSegmentations = getSegmentationState(toolGroupUID) + + const segmentationData = toolGroupSegmentations.find( + (segmentationData: ToolGroupSpecificSegmentationData) => + segmentationData.segmentationDataUID === segmentationDataUID + ) + + if (!segmentationData) { + return + } + + return segmentationData.visibility +} + +export default { setSegmentationVisibility, getSegmentationVisibility } +export { setSegmentationVisibility, getSegmentationVisibility } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/setLabelmapColorAndOpacity.ts b/packages/cornerstone-tools/src/store/SegmentationModule/setLabelmapColorAndOpacity.ts deleted file mode 100644 index 3f2a41dfa..000000000 --- a/packages/cornerstone-tools/src/store/SegmentationModule/setLabelmapColorAndOpacity.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { vtkVolume } from 'vtk.js/Sources/Rendering/Core/Volume' -import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction' -import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction' - -import defaultConfig, { ISegmentationConfig } from './segmentationConfig' -import state from './state' -import _cloneDeep from 'lodash.clonedeep' - -const MAX_NUMBER_COLORS = 255 - -function setLabelmapColorAndOpacity( - volumeActor: vtkVolume, - cfun: vtkColorTransferFunction, - ofun: vtkPiecewiseFunction, - colorLUTIndex: number, - labelmapConfig: Partial, - isActiveLabelmap: boolean, - visibility = true -): void { - ofun.addPoint(0, 0) - - // Todo: this should be moved a level up - let config = _cloneDeep(defaultConfig) - - // if custom config per labelmap - if (labelmapConfig) { - config = Object.assign(config, labelmapConfig) - } - - const fillAlpha = isActiveLabelmap - ? config.fillAlpha - : config.fillAlphaInactive - const outlineWidth = isActiveLabelmap - ? config.outlineWidthActive - : config.outlineWidthInactive - - // Note: MAX_NUMBER_COLORS = 256 is needed because the current method to generate - // the default color table uses RGB. - const colorLUT = state.colorLutTables[colorLUTIndex] - const numColors = Math.min(256, colorLUT.length) - - for (let i = 0; i < numColors; i++) { - const color = colorLUT[i] - cfun.addRGBPoint( - i, - color[0] / MAX_NUMBER_COLORS, - color[1] / MAX_NUMBER_COLORS, - color[2] / MAX_NUMBER_COLORS - ) - - // Set the opacity per label. - const segmentOpacity = (color[3] / 255) * fillAlpha - ofun.addPoint(i, segmentOpacity) - } - - ofun.setClamping(false) - - volumeActor.getProperty().setRGBTransferFunction(0, cfun) - volumeActor.getProperty().setScalarOpacity(0, ofun) - volumeActor.getProperty().setInterpolationTypeToNearest() - - volumeActor.getProperty().setUseLabelOutline(config.renderOutline) - volumeActor.getProperty().setLabelOutlineThickness(outlineWidth) - - // Set visibility based on whether actor visibility is specifically asked - // to be turned on/off (on by default) AND whether is is in active but - // we are rendering inactive labelmap - const visible = - visibility && (isActiveLabelmap || config.renderInactiveLabelmaps) - volumeActor.setVisibility(visible) -} - -export default setLabelmapColorAndOpacity diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/setLabelmapForElement.ts b/packages/cornerstone-tools/src/store/SegmentationModule/setLabelmapForElement.ts deleted file mode 100644 index 9c233ad55..000000000 --- a/packages/cornerstone-tools/src/store/SegmentationModule/setLabelmapForElement.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - getEnabledElement, - StackViewport, - addVolumesOnViewports, -} from '@precisionmetrics/cornerstone-render' - -import state, { - getGlobalStateForLabelmapUID, - setLabelmapGlobalState, - setLabelmapViewportSpecificState, -} from './state' -import { triggerLabelmapStateUpdated } from './triggerLabelmapStateUpdated' - -/** - * It renders a labelmap 3D volume into the viewport that the element belongs to - * @param {element, labelmap, callback, labelmapIndex, immediateRender} - */ -async function setLabelmapForElement({ - element, - labelmap, - labelmapIndex = 0, - colorLUTIndex = 0, -}) { - const enabledElement = getEnabledElement(element) - const { renderingEngine, viewport } = enabledElement - - // Segmentation VolumeUID - const { uid: labelmapUID } = labelmap - - // StackViewport Implementation - if (viewport instanceof StackViewport) { - throw new Error('Segmentation for StackViewport is not supported yet') - } - - // Updating segmentation state for viewports - // Creating a global state for labelmap if not found - const labelmapGlobalState = getGlobalStateForLabelmapUID(labelmapUID) - if (!labelmapGlobalState) { - setLabelmapGlobalState(labelmapUID) - } - - const { uid: viewportUID } = viewport - - // VolumeViewport Implementation - let viewportLabelmapsState = state.volumeViewports[viewportUID] - - // If first time with this state - if (!viewportLabelmapsState) { - // If no state is assigned for the viewport for segmentation: create an empty - // segState for the viewport and assign the requested labelmapIndex as the active one. - viewportLabelmapsState = { - activeLabelmapIndex: labelmapIndex, - labelmaps: [], - } - state.volumeViewports[viewportUID] = viewportLabelmapsState - } - - // Updating the active labelmapIndex - state.volumeViewports[viewportUID].activeLabelmapIndex = labelmapIndex - - setLabelmapViewportSpecificState(viewportUID, labelmapUID, labelmapIndex) - - // Default to true since we are setting a new labelmap, however, - // in the event listener, we will make other segmentations visible/invisible - // based on the config - const visibility = true - - // Add labelmap volumes to the viewports to be be rendered, but not force the render - await addVolumesOnViewports( - renderingEngine, - [ - { - volumeUID: labelmapUID, - visibility, - }, - ], - [viewportUID] - ) - - // Trigger the labelmap state updated event which - // will trigger the event for all the viewports that have the labelmap - triggerLabelmapStateUpdated(labelmapUID, element) -} - -export default setLabelmapForElement - -export { setLabelmapForElement } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/state.ts b/packages/cornerstone-tools/src/store/SegmentationModule/state.ts deleted file mode 100644 index e54c1ebce..000000000 --- a/packages/cornerstone-tools/src/store/SegmentationModule/state.ts +++ /dev/null @@ -1,388 +0,0 @@ -import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction' -import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction' -import { ISegmentationConfig } from './segmentationConfig' -import { - getEnabledElement, - VolumeViewport, -} from '@precisionmetrics/cornerstone-render' -import { getActiveLabelmapIndex } from './activeLabelmapController' -import { setColorLUT } from './colorLUT' -import { triggerLabelmapStateUpdated } from './triggerLabelmapStateUpdated' - -type LabelmapGlobalState = { - volumeUID: string - label: string - referenceVolumeUID?: string - cachedStats: { [key: string]: number } - referenceImageId?: string - activeSegmentIndex: number - segmentsLocked: Set -} - -export type ViewportLabelmapsState = { - activeLabelmapIndex: number - labelmaps: ViewportLabelmapState[] -} - -export type ViewportLabelmapState = { - volumeUID: string - segmentsHidden: Set - visibility: boolean - colorLUTIndex: number - cfun: vtkColorTransferFunction - ofun: vtkPiecewiseFunction - labelmapConfig: Partial -} - -// RGBA as 0-255 -export type Color = [number, number, number, number] - -// [[0,0,0,0], [200,200,200,200], ....] -export type ColorLUT = Array - -export interface SegmentationState { - labelmaps: Array - volumeViewports: { [key: string]: ViewportLabelmapsState } - colorLutTables: Array - stackViewports: any -} - -const state: SegmentationState = { - colorLutTables: [ - //[ - // ColorLUTTable-0 - // [0, 0, 0, 0], - // [255, 0, 0, 255], - // [0, 255, 0, 255], - // [0, 0, 255, 255], - // ...... , - //], - ], - labelmaps: [ - // { - // volumeUID: "labelmapUID1", - // label: "label1", - // referenceVolumeUID: "referenceVolumeName", // volume viewport - // referenceImageId: "referenceImageId", // stack viewport - // activeSegmentIndex: 1, - // segmentsLocked: Set(), - // cacheStats: {} // storing labelmap specific statistics - // } - // { - // volumeUID: "labelmapUID2", - // label: "label1", - // referenceVolumeUID: "referenceVolumeName", // volume viewport - // referenceImageId: "referenceImageId", // stack viewport - // activeSegmentIndex: 1, - // segmentsLocked: Set(), - // cacheStats: {} // storing labelmap specific statistics - // } - ], - volumeViewports: { - // viewportUID: { - // activeLabelmapUID: 1, - // labelmaps: [ - // { - // volumeUID: 'labelmapUID1', - // colorLUTIndex: 0, - // visibility: true, - // cfun: cfun, - // ofun: ofun, - // segmentsHidden: Set(), - // labelmapConfig: { - // renderOutline: true, - // outlineWidth: 3, - // outlineWidthActive: 3, - // outlineWidthInactive: 2, - // renderFill: true, - // fillAlpha: 0.9, - // fillAlphaInactive: 0.85, - // }, - // }, - // { - // volumeUID: 'labelmapUID2', - // colorLUTIndex: 0, - // visibility: true, - // cfun: cfun, - // ofun: ofun, - // segmentsHidden: Set(), - // labelmapConfig: { - // renderOutline: true, - // outlineWidth: 3, - // outlineWidthActive: 3, - // outlineWidthInactive: 2, - // renderFill: true, - // fillAlpha: 0.9, - // fillAlphaInactive: 0.85, - // }, - // }, - // ], - // }, - }, - stackViewports: { - // Not implemented yet - }, -} - -/** - * Returns the viewport specific labelmapsState for HTML element - * @param element HTML element - * @returns ViewportLabelmapsState - */ -function getGlobalStateForLabelmapUID( - labelmapUID: string -): LabelmapGlobalState { - return state.labelmaps.find( - (labelmapState) => labelmapState.volumeUID === labelmapUID - ) -} - -function _initDefaultColorLUT() { - if (state.colorLutTables.length === 0) { - setColorLUT(0) - } -} - -/** - * Sets the labelmap globalState, including {volumeUID, referenceVolumeUID, - * referenceImageId, activeSegmentIndex, segmentsLocked}, if no state is given - * it will create an empty default global state - * @param labelmapUID labelmapUID - * @param newState Global state of the labelmap - * @param overwrite force overwriting already existing labelmapState - */ -function setLabelmapGlobalState( - labelmapUID: string, - newState: LabelmapGlobalState = { - volumeUID: labelmapUID, - label: labelmapUID, - referenceVolumeUID: null, - cachedStats: {}, - referenceImageId: null, - activeSegmentIndex: 1, - segmentsLocked: new Set(), - } -): void { - // Creating the default color LUT if not created yet - _initDefaultColorLUT() - - // Don't allow overwriting existing labelmapState with the same labelmapUID - const existingState = state.labelmaps.find( - (labelmapState) => labelmapState.volumeUID === labelmapUID - ) - - if (existingState) { - if (newState.volumeUID && newState.volumeUID !== labelmapUID) { - throw new Error( - `Labelmap state with volumeUID ${newState.volumeUID} already exists` - ) - } - } - - // merge the new state with the existing state - const updatedState = { - ...existingState, - ...newState, - } - - // Is there any existing state? - if (!existingState) { - state.labelmaps.push({ - volumeUID: labelmapUID, - label: updatedState.label, - referenceVolumeUID: updatedState.referenceVolumeUID, - cachedStats: updatedState.cachedStats, - referenceImageId: updatedState.referenceImageId, - activeSegmentIndex: updatedState.activeSegmentIndex, - segmentsLocked: updatedState.segmentsLocked, - }) - } else { - // If there is an existing state, replace it - const index = state.labelmaps.findIndex( - (labelmapState) => labelmapState.volumeUID === labelmapUID - ) - state.labelmaps[index] = updatedState - } - - triggerLabelmapStateUpdated(labelmapUID) -} - -/** - * Sets the labelmap viewport-specific state - * - * @param viewportUID labelmapUID - * @param labelmapUID labelmapUID - * @param viewportLabelmapState viewport-specific state of the labelmap - * @param overwrite force overwriting already existing labelmapState - */ -function setLabelmapViewportSpecificState( - viewportUID: string, - labelmapUID: string, - labelmapIndex = 0, - viewportLabelmapState?: ViewportLabelmapState -): void { - let labelmapState = viewportLabelmapState - if (!labelmapState) { - labelmapState = { - volumeUID: labelmapUID, - segmentsHidden: new Set(), - visibility: true, - colorLUTIndex: 0, - cfun: vtkColorTransferFunction.newInstance(), - ofun: vtkPiecewiseFunction.newInstance(), - labelmapConfig: {}, - } - } - // Todo: check if there is a labelmapGlobalState - const viewportLabelmapsState = _getLabelmapsStateForViewportUID(viewportUID) - - if (!viewportLabelmapsState) { - state.volumeViewports[viewportUID] = { - activeLabelmapIndex: 0, - labelmaps: [], - } - } - - state.volumeViewports[viewportUID].labelmaps[labelmapIndex] = { - volumeUID: labelmapUID, - segmentsHidden: labelmapState.segmentsHidden, - visibility: labelmapState.visibility, - colorLUTIndex: labelmapState.colorLUTIndex, - cfun: labelmapState.cfun, - ofun: labelmapState.ofun, - labelmapConfig: labelmapState.labelmapConfig, - } -} - -/** - * Returns the viewport specific labelmapsState for HTML element - * @param element HTML element - * @returns ViewportLabelmapsState - */ -function getLabelmapsStateForElement( - element: HTMLElement -): ViewportLabelmapsState { - const enabledElement = getEnabledElement(element) - - if (!enabledElement) { - return - } - - const { viewport, viewportUID } = enabledElement - - // Todo: stack Viewport - if (!(viewport instanceof VolumeViewport)) { - throw new Error('Stack Viewport segmentation not supported yet') - } - - return _getLabelmapsStateForViewportUID(viewportUID) -} - -function removeLabelmapFromGlobalState(labelmapUID: string): void { - const labelmapGlobalState = getGlobalStateForLabelmapUID(labelmapUID) - - if (labelmapGlobalState) { - const labelmapGlobalIndex = state.labelmaps.findIndex( - (labelmap) => labelmap.volumeUID === labelmapUID - ) - - state.labelmaps.splice(labelmapGlobalIndex, 1) - } -} - -// function removeLabelmapFromContainingViewports(labelmapUID: string): void { -// // get viewportUIDs in the state -// const viewportUIDs = Object.keys(state.volumeViewports) - -// // remove the labelmap from all viewports -// viewportUIDs.forEach((viewportUID) => { -// const viewportLabelmapsState = state.volumeViewports[viewportUID] - -// if (viewportLabelmapsState) { -// const labelmapIndex = viewportLabelmapsState.labelmaps.findIndex( -// (labelmap) => labelmap.volumeUID === labelmapUID -// ) - -// if (labelmapIndex !== -1) { -// viewportLabelmapsState.labelmaps.splice(labelmapIndex, 1) -// } -// } -// }) -// } - -/** - * Returns the viewport specific labelmapState for a viewportUID and the provided - * labelmapIndex, or if index not provided, for the activeLabelmap - * @param viewportUID viewportUID - * @param [labelmapIndexOrUID] labelmapIndex - * @returns ViewportLabelmapState - */ -function getLabelmapStateForElement( - element: HTMLElement, - labelmapIndex?: number -): ViewportLabelmapState { - const { viewportUID } = getEnabledElement(element) - return _getLabelmapStateForViewportUID(viewportUID, labelmapIndex) -} - -/** - * Returns the viewport specific labelmapS State for HTML element - * @param element HTML element - * @returns ViewportLabelmapsState - */ -function getActiveLabelmapState( - element: HTMLElement -): ViewportLabelmapState | undefined { - const activeLabelmapIndex = getActiveLabelmapIndex(element) - const labelmapsState = getLabelmapsStateForElement(element) - - if (!labelmapsState) { - return - } - - return labelmapsState.labelmaps[activeLabelmapIndex] -} - -/** - * Returns the viewport specific labelmapsState for a viewportUID - * @param viewportUID viewportUID - * @returns ViewportLabelmapsState - */ -function _getLabelmapsStateForViewportUID( - viewportUID: string -): ViewportLabelmapsState { - return state.volumeViewports[viewportUID] -} - -/** - * Returns the viewport specific labelmapsState for a viewportUID - * @param viewportUID viewportUID - * @returns ViewportLabelmapsState - */ -function _getLabelmapStateForViewportUID( - viewportUID: string, - labelmapIndex?: number -): ViewportLabelmapState { - const viewportLabelmapsState = state.volumeViewports[viewportUID] - - const index = - labelmapIndex === undefined - ? viewportLabelmapsState.activeLabelmapIndex - : labelmapIndex - - return viewportLabelmapsState.labelmaps[index] -} - -export default state -export { - // get - getLabelmapsStateForElement, - getLabelmapStateForElement, - getActiveLabelmapState, - getGlobalStateForLabelmapUID, - // set - setLabelmapGlobalState, - setLabelmapViewportSpecificState, - // remove - removeLabelmapFromGlobalState, -} diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/triggerLabelmapStateUpdated.ts b/packages/cornerstone-tools/src/store/SegmentationModule/triggerLabelmapStateUpdated.ts deleted file mode 100644 index 41a8ff328..000000000 --- a/packages/cornerstone-tools/src/store/SegmentationModule/triggerLabelmapStateUpdated.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { - getRenderingEngines, - VolumeViewport, - triggerEvent, -} from '@precisionmetrics/cornerstone-render' - -import state from './state' -import { CornerstoneTools3DEvents as EVENTS } from '../../enums' - -export type LabelmapStateUpdatedEvent = { - element: HTMLElement - labelmapUID: string // volumeUID of the labelmap whose state has been updated - labelmapIndex: number // index of the modified labelmap in viewport's array of labelmaps state - activeLabelmapIndex: number // active labelmapIndex for the viewport - renderingEngineUID: string - viewportUID: string - id: string - cachedStats: any -} - -/** - * Returns the list of viewportUIDs that include labelmapUID in their labelmaps state - * @param labelmapUID volumeUID of the labelmap - * @returns array of viewportUIDs - */ -function _getViewportUIDsForLabelmapUID(labelmapUID: string): string[] { - const viewportUIDs = [] - Object.keys(state.volumeViewports).forEach((viewportUID) => { - const viewportLabelmapsState = state.volumeViewports[viewportUID] - viewportLabelmapsState.labelmaps.forEach((labelmapState) => { - if (labelmapState.volumeUID === labelmapUID) { - viewportUIDs.push(viewportUID) - } - }) - }) - return viewportUIDs -} - -/** - * Finds the viewports containing the labelmap (by UID), and triggers a - * LABELMAP_STATE_UPDATED event on those viewports. If an element is provided, - * it only triggers on the viewport containing the element. - * @param labelmapUID volumeUID of the labelmap - */ -function triggerLabelmapStateUpdated( - labelmapUID: string, - element?: HTMLElement -): void { - const viewportUIDs = _getViewportUIDsForLabelmapUID(labelmapUID) - - const renderingEngine = getRenderingEngines()[0] - const { uid: renderingEngineUID } = renderingEngine - - viewportUIDs.forEach((viewportUID) => { - const viewportLabelmapsState = state.volumeViewports[viewportUID] - const viewport = renderingEngine.getViewport(viewportUID) as VolumeViewport - const { element } = viewport - - // If the viewport displays the labelmap (either active or inactive), but - // its state with regard to labelmap has not changed, bail out - if (element && element !== viewport.element) { - return - } - - const activeLabelmapIndex = viewportLabelmapsState.activeLabelmapIndex - - viewportLabelmapsState.labelmaps.forEach((labelmapState, labelmapIndex) => { - // Only trigger event for the the requested labelmapUID - if (labelmapState.volumeUID !== labelmapUID) { - return - } - - // get the global state of the labelmap - const labelmapGlobalState = state.labelmaps.find( - (labelmap) => labelmap.volumeUID === labelmapUID - ) - - // get cached stats - const { cachedStats } = labelmapGlobalState - - const eventData: LabelmapStateUpdatedEvent = { - id: labelmapUID, - labelmapUID, - element, - labelmapIndex, - activeLabelmapIndex, - renderingEngineUID, - viewportUID, - cachedStats, - } - - triggerEvent(element, EVENTS.LABELMAP_STATE_UPDATED, eventData) - }) - }) -} - -/** - * Finds the viewports containing the labelmapUIDs, and triggers - * LABELMAP_UPDATED event on those viewports for all the provided labelmapUIDs. - * If no labelmapUIDs are provided, it will trigger a LABELMAP_UPDATED on all - * the labelmaps in the state. - * @param labelmapUID volumeUID of the labelmap - */ -function triggerLabelmapsStateUpdated(labelmapUIDs?: string[]): void { - const { volumeViewports } = state - - if (!volumeViewports) { - return - } - - let labelmapUIDsToUse = labelmapUIDs - if (!labelmapUIDs || !labelmapUIDs.length) { - labelmapUIDsToUse = state.labelmaps.map(({ volumeUID }) => volumeUID) - } - - labelmapUIDsToUse.forEach((labelmapUID) => { - triggerLabelmapStateUpdated(labelmapUID) - }) -} - -export { triggerLabelmapsStateUpdated, triggerLabelmapStateUpdated } diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/triggerSegmentationEvents.ts b/packages/cornerstone-tools/src/store/SegmentationModule/triggerSegmentationEvents.ts new file mode 100644 index 000000000..8f63c755c --- /dev/null +++ b/packages/cornerstone-tools/src/store/SegmentationModule/triggerSegmentationEvents.ts @@ -0,0 +1,70 @@ +import { triggerEvent, eventTarget } from '@precisionmetrics/cornerstone-render' + +import { CornerstoneTools3DEvents as EVENTS } from '../../enums' +import { + getToolGroupsWithSegmentation, + getToolGroups, + getGlobalSegmentationState, +} from '../../stateManagement/segmentation/segmentationState' + +/** + * Trigger an event on the viewport elements that the segmentation state has been + * updated + * @param enabledElement - The enabled element that has been updated. + */ +function triggerSegmentationStateModified(toolGroupUID: string): void { + const eventData = { + toolGroupUID, + } + + triggerEvent(eventTarget, EVENTS.SEGMENTATION_STATE_MODIFIED, eventData) +} + +function triggerSegmentationGlobalStateModified( + segmentationUID?: string +): void { + let toolGroupUIDs, segmentationUIDs + + if (segmentationUID) { + toolGroupUIDs = getToolGroupsWithSegmentation(segmentationUID) + segmentationUIDs = [segmentationUID] + } else { + // get all toolGroups + toolGroupUIDs = getToolGroups() + segmentationUIDs = getGlobalSegmentationState().map( + ({ volumeUID }) => volumeUID + ) + } + + // 1. Trigger an event notifying all listeners about the segmentationUID + // that has been updated. + triggerEvent(eventTarget, EVENTS.SEGMENTATION_GLOBAL_STATE_MODIFIED, { + segmentationUIDs, + }) + + // 2. Notify all viewports that render the segmentationUID in order to update the + // rendering based on the new global state. + toolGroupUIDs.forEach((toolGroupUID) => { + triggerSegmentationStateModified(toolGroupUID) + }) +} + +function triggerSegmentationDataModified( + toolGroupUID: string, + segmentationDataUID: string +): void { + const eventDetail = { + toolGroupUID, + segmentationDataUID, + } + + triggerEvent(eventTarget, EVENTS.SEGMENTATION_DATA_MODIFIED, eventDetail) +} + +export { + // ToolGroup Specific + triggerSegmentationStateModified, + // Global + triggerSegmentationDataModified, + triggerSegmentationGlobalStateModified, +} diff --git a/packages/cornerstone-tools/src/store/SegmentationModule/utils.ts b/packages/cornerstone-tools/src/store/SegmentationModule/utils.ts deleted file mode 100644 index 0767ef64a..000000000 --- a/packages/cornerstone-tools/src/store/SegmentationModule/utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { getLabelmapsStateForElement } from './state' -import { getActiveLabelmapIndex } from './activeLabelmapController' - -/** - * Returns all the labelmapUIDs of the HTML element (active and inactive) - * @param element HTML element - * @returns - */ -function getLabelmapUIDsForElement(element: HTMLElement): string[] { - const viewportLabelmapsState = getLabelmapsStateForElement(element) - - if (!viewportLabelmapsState) { - return [] - } - - return viewportLabelmapsState.labelmaps.map(({ volumeUID }) => volumeUID) -} - -/** - * Returns the labelmapUID that the element is rendering, if no labelmapIndex is - * provided it uses the active labelmapIndex - * @param element HTML Element - * @param labelmapIndex labelmap index in the viewportLabelmapsState - * @returns labelmapUID - */ -function getLabelmapUIDForElement( - element: HTMLElement, - labelmapIndex?: number -): string | undefined { - const viewportLabelmapsState = getLabelmapsStateForElement(element) - - if (!viewportLabelmapsState) { - return - } - - const index = - labelmapIndex === undefined - ? getActiveLabelmapIndex(element) - : labelmapIndex - - return viewportLabelmapsState.labelmaps[index].volumeUID -} - -export { getLabelmapUIDsForElement, getLabelmapUIDForElement } diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/createToolGroup.ts b/packages/cornerstone-tools/src/store/ToolGroupManager/createToolGroup.ts index 335dd66c6..e8230c758 100644 --- a/packages/cornerstone-tools/src/store/ToolGroupManager/createToolGroup.ts +++ b/packages/cornerstone-tools/src/store/ToolGroupManager/createToolGroup.ts @@ -1,8 +1,7 @@ import { ToolBindings, ToolModes } from '../../enums' import { getRenderingEngine } from '@precisionmetrics/cornerstone-render' import { state } from '../index' -import IToolGroup from './IToolGroup' -import ISetToolModeOptions from '../../types/ISetToolModeOptions' +import { ISetToolModeOptions, IToolGroup } from '../../types' import deepmerge from '../../util/deepMerge' import { MouseCursor, SVGMouseCursor } from '../../cursors' @@ -10,26 +9,26 @@ import { initElementCursor } from '../../cursors/elementCursor' const { Active, Passive, Enabled, Disabled } = ToolModes -function createToolGroup(toolGroupId: string): IToolGroup | undefined { +function createToolGroup(toolGroupUID: string): IToolGroup | undefined { // Exit early if ID conflict const toolGroupWithIdExists = state.toolGroups.some( - (tg) => tg.id === toolGroupId + (tg) => tg.uid === toolGroupUID ) if (toolGroupWithIdExists) { - console.warn(`'${toolGroupId}' already exists.`) + console.warn(`'${toolGroupUID}' already exists.`) return } // Create const toolGroup: IToolGroup = { - _toolInstances: {}, // tool instances - id: toolGroupId, - viewports: [], + uid: toolGroupUID, + viewportsInfo: [], toolOptions: {}, // tools modes etc. + _toolInstances: {}, // tool instances // getViewportUIDs: function () { - return this.viewports.map(({ viewportUID }) => viewportUID) + return this.viewportsInfo.map(({ viewportUID }) => viewportUID) }, getToolInstance: function (toolName) { const toolInstance = this._toolInstances[toolName] @@ -59,7 +58,7 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { if (localToolInstance) { console.warn( - `'${toolName}' is already registered for ToolGroup ${this.id}.` + `'${toolName}' is already registered for ToolGroup ${this.uid}.` ) return } @@ -74,7 +73,10 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { toolConfiguration ) - const instantiatedTool = new ToolClass(mergedToolConfiguration) + const instantiatedTool = new ToolClass({ + ...mergedToolConfiguration, + toolGroupUID: this.uid, + }) // API instead of directly exposing schema? // Maybe not here, but feels like a "must" for any method outside of the ToolGroup itself @@ -84,7 +86,7 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { renderingEngineUID: string, viewportUID?: string ): void { - this.viewports.push({ renderingEngineUID, viewportUID }) + this.viewportsInfo.push({ renderingEngineUID, viewportUID }) }, /** * Removes viewport from the toolGroup. If only renderingEngineUID is defined @@ -99,7 +101,7 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { ): void { const indices = [] - this.viewports.forEach((vp, index) => { + this.viewportsInfo.forEach((vp, index) => { let match = false if (vp.renderingEngineUID === renderingEngineUID) { match = true @@ -116,7 +118,7 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { if (indices.length) { // going in reverse to not mess up the indexes to be removed for (let i = indices.length - 1; i >= 0; i--) { - this.viewports.splice(indices[i], 1) + this.viewportsInfo.splice(indices[i], 1) } } }, @@ -154,14 +156,11 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { } if (typeof this._toolInstances[toolName].init === 'function') { - this._toolInstances[toolName].init(this.viewports) + this._toolInstances[toolName].init(this.viewportsInfo) } this.refreshViewports() }, - setToolPassive: function ( - toolName: string, - toolModeOptions: ISetToolModeOptions - ): void { + setToolPassive: function (toolName: string): void { if (this._toolInstances[toolName] === undefined) { console.warn( `Tool ${toolName} not added to toolgroup, can't set tool mode.` @@ -172,11 +171,10 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { // Wwe should only remove the primary button bindings and keep // the other ones (Zoom on right click) + const toolModeOptions = this.getToolModeOptions(toolName) const toolOptions = Object.assign( { - bindings: this.toolOptions[toolName] - ? this.toolOptions[toolName].bindings - : [], + bindings: toolModeOptions ? toolModeOptions.bindings : [], }, toolModeOptions, { @@ -200,10 +198,7 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { this._toolInstances[toolName].mode = mode this.refreshViewports() }, - setToolEnabled: function ( - toolName: string, - toolModeOptions: ISetToolModeOptions - ): void { + setToolEnabled: function (toolName: string): void { if (this._toolInstances[toolName] === undefined) { console.warn( `Tool ${toolName} not added to toolgroup, can't set tool mode.` @@ -212,26 +207,21 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { return } - // Would only need this for sanity check if not instantiating/hydrating - // const tool = this.toolOptions[toolName]; - const toolModeOptionsWithMode = Object.assign( - { - bindings: [], - }, - toolModeOptions, - { - mode: Enabled, - } - ) + const toolModeOptionsWithMode = { + bindings: [], + mode: Enabled, + } this.toolOptions[toolName] = toolModeOptionsWithMode this._toolInstances[toolName].mode = Enabled + + if (this._toolInstances[toolName].enableCallback) { + this._toolInstances[toolName].enableCallback(this.uid) + } + this.refreshViewports() }, - setToolDisabled: function ( - toolName: string, - toolModeOptions: ISetToolModeOptions - ): void { + setToolDisabled: function (toolName: string): void { if (this._toolInstances[toolName] === undefined) { console.warn( `Tool ${toolName} not added to toolgroup, can't set tool mode.` @@ -241,19 +231,24 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { // Would only need this for sanity check if not instantiating/hydrating // const tool = this.toolOptions[toolName]; - const toolModeOptionsWithMode = Object.assign( - { - bindings: [], - }, - toolModeOptions, - { - mode: Disabled, - } - ) + const toolModeOptionsWithMode = { + bindings: [], + mode: Disabled, + } + this.toolOptions[toolName] = toolModeOptionsWithMode this._toolInstances[toolName].mode = Disabled + + if (this._toolInstances[toolName].disableCallback) { + this._toolInstances[toolName].disableCallback(this.uid) + } this.refreshViewports() }, + // Todo: + // setToolConfiguration(){}, + getToolModeOptions(toolName: string) { + return this.toolOptions[toolName] + }, getActivePrimaryButtonTools() { return Object.keys(this.toolOptions).find((toolName) => { const toolModeOptions = this.toolOptions[toolName] @@ -271,7 +266,7 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { ) }, refreshViewports(): void { - this.viewports.forEach(({ renderingEngineUID, viewportUID }) => { + this.viewportsInfo.forEach(({ renderingEngineUID, viewportUID }) => { getRenderingEngine(renderingEngineUID).renderViewport(viewportUID) }) }, @@ -284,7 +279,7 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { if (!cursor) { cursor = MouseCursor.getDefinedCursor('default') } - this.viewports.forEach(({ renderingEngineUID, viewportUID }) => { + this.viewportsInfo.forEach(({ renderingEngineUID, viewportUID }) => { const viewport = getRenderingEngine(renderingEngineUID).getViewport(viewportUID) if (viewport && viewport.element) { diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/destroy.ts b/packages/cornerstone-tools/src/store/ToolGroupManager/destroy.ts index 5ef84be4d..61afa2b08 100644 --- a/packages/cornerstone-tools/src/store/ToolGroupManager/destroy.ts +++ b/packages/cornerstone-tools/src/store/ToolGroupManager/destroy.ts @@ -1,10 +1,20 @@ // `BaseManager` or IManager interface for duplicate API between ToolGroup/Synchronizer? -import { state } from '../index' +import { state as csToolsState } from '../index' +import destroyToolGroupByToolGroupUID from './destroyToolGroupByToolGroupUID' // ToolGroups function entirely by their "state" being queried and leveraged -// removing a ToolGroup from state is equivelant to killing it +// removing a ToolGroup from state is equivalant to killing it. Calling +// destroyToolGroupByToolGroupUID() to make sure the SegmentationDisplayTools +// have been removed from the toolGroup Viewports. //Todo: this makes more sense +// to be based on events, but we don't have any toolGroup created/removed events function destroy(): void { - state.toolGroups = [] + const toolGroups = [...csToolsState.toolGroups] + + for (const toolGroup of toolGroups) { + destroyToolGroupByToolGroupUID(toolGroup.uid) + } + + csToolsState.toolGroups = [] } export default destroy diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/destroyToolGroupById.ts b/packages/cornerstone-tools/src/store/ToolGroupManager/destroyToolGroupByToolGroupUID.ts similarity index 52% rename from packages/cornerstone-tools/src/store/ToolGroupManager/destroyToolGroupById.ts rename to packages/cornerstone-tools/src/store/ToolGroupManager/destroyToolGroupByToolGroupUID.ts index 608b41f1d..2d4d8b261 100644 --- a/packages/cornerstone-tools/src/store/ToolGroupManager/destroyToolGroupById.ts +++ b/packages/cornerstone-tools/src/store/ToolGroupManager/destroyToolGroupByToolGroupUID.ts @@ -1,15 +1,17 @@ import { state } from '../index' +import { removeSegmentationsForToolGroup } from '../../stateManagement/segmentation' // ToolGroups function entirely by their "state" being queried and leveraged // removing a ToolGroup from state is equivelant to killing it -function destroyToolGroupById(toolGroupId: string): void { +function destroyToolGroupByToolGroupUID(toolGroupUID: string): void { const toolGroupIndex = state.toolGroups.findIndex( - (tg) => tg.id === toolGroupId + (tg) => tg.uid === toolGroupUID ) if (toolGroupIndex > -1) { + removeSegmentationsForToolGroup(toolGroupUID) state.toolGroups.splice(toolGroupIndex, 1) } } -export default destroyToolGroupById +export default destroyToolGroupByToolGroupUID diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/getAllToolGroups.ts b/packages/cornerstone-tools/src/store/ToolGroupManager/getAllToolGroups.ts index 47de33439..b273593e0 100644 --- a/packages/cornerstone-tools/src/store/ToolGroupManager/getAllToolGroups.ts +++ b/packages/cornerstone-tools/src/store/ToolGroupManager/getAllToolGroups.ts @@ -1,5 +1,5 @@ import { state } from '../index' -import IToolGroup from './IToolGroup' +import { IToolGroup } from '../../types' function getAllToolGroups(): Array { return state.toolGroups diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroup.ts b/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroup.ts new file mode 100644 index 000000000..7e375745c --- /dev/null +++ b/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroup.ts @@ -0,0 +1,30 @@ +import { state } from '../index' +import { IToolGroup } from '../../types' + +function getToolGroup( + renderingEngineUID: string, + viewportUID: string +): IToolGroup | undefined { + const toolGroupFilteredByUIDs = state.toolGroups.filter((tg) => + tg.viewportsInfo.some( + (vp) => + vp.renderingEngineUID === renderingEngineUID && + (!vp.viewportUID || vp.viewportUID === viewportUID) + ) + ) + + if (!toolGroupFilteredByUIDs.length) { + return + } + + if (toolGroupFilteredByUIDs.length > 1) { + throw new Error( + `Multiple tool groups found for renderingEngineUID: ${renderingEngineUID} and viewportUID: ${viewportUID}. You should only + have one tool group per viewport in a renderingEngine.` + ) + } + + return toolGroupFilteredByUIDs[0] +} + +export default getToolGroup diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroupById.ts b/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroupById.ts deleted file mode 100644 index 4e149743d..000000000 --- a/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroupById.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { state } from '../index' -import IToolGroup from './IToolGroup' - -function getToolGroupById(toolGroupId: string): IToolGroup | void { - return state.toolGroups.find((s) => s.id === toolGroupId) -} - -export default getToolGroupById diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroupByToolGroupUID.ts b/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroupByToolGroupUID.ts new file mode 100644 index 000000000..185d7c0d4 --- /dev/null +++ b/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroupByToolGroupUID.ts @@ -0,0 +1,10 @@ +import { state } from '../index' +import { IToolGroup } from '../../types' + +function getToolGroupByToolGroupUID( + toolGroupUID: string +): IToolGroup | undefined { + return state.toolGroups.find((s) => s.uid === toolGroupUID) +} + +export default getToolGroupByToolGroupUID diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroups.ts b/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroups.ts deleted file mode 100644 index aa4752e21..000000000 --- a/packages/cornerstone-tools/src/store/ToolGroupManager/getToolGroups.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { state } from '../index' -import IToolGroup from './IToolGroup' - -function getToolGroups( - renderingEngineUID: string, - viewportUID: string -): Array { - const toolGroupsFilteredByUIDs = state.toolGroups.filter((tg) => - tg.viewports.some( - (vp) => - vp.renderingEngineUID === renderingEngineUID && - (!vp.viewportUID || vp.viewportUID === viewportUID) - ) - ) - - return toolGroupsFilteredByUIDs -} - -export default getToolGroups diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/index.js b/packages/cornerstone-tools/src/store/ToolGroupManager/index.js deleted file mode 100644 index 147c1c238..000000000 --- a/packages/cornerstone-tools/src/store/ToolGroupManager/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import createToolGroup from './createToolGroup' -import destroyToolGroupById from './destroyToolGroupById' -import destroy from './destroy' -import getToolGroupById from './getToolGroupById' -import getToolGroups from './getToolGroups' -import getAllToolGroups from './getAllToolGroups' - -export default { - createToolGroup, - destroy, - destroyToolGroupById, - getToolGroupById, - getToolGroups, - getAllToolGroups, -} diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/index.ts b/packages/cornerstone-tools/src/store/ToolGroupManager/index.ts new file mode 100644 index 000000000..53f97c23e --- /dev/null +++ b/packages/cornerstone-tools/src/store/ToolGroupManager/index.ts @@ -0,0 +1,24 @@ +import createToolGroup from './createToolGroup' +import destroyToolGroupByToolGroupUID from './destroyToolGroupByToolGroupUID' +import destroy from './destroy' +import getToolGroupByToolGroupUID from './getToolGroupByToolGroupUID' +import getToolGroup from './getToolGroup' +import getAllToolGroups from './getAllToolGroups' + +export default { + createToolGroup, + destroy, + destroyToolGroupByToolGroupUID, + getToolGroupByToolGroupUID, + getToolGroup, + getAllToolGroups, +} + +export { + createToolGroup, + destroy, + destroyToolGroupByToolGroupUID, + getToolGroupByToolGroupUID, + getToolGroup, + getAllToolGroups, +} diff --git a/packages/cornerstone-tools/src/store/addEnabledElement.ts b/packages/cornerstone-tools/src/store/addEnabledElement.ts index 9497d12db..69f6a8338 100644 --- a/packages/cornerstone-tools/src/store/addEnabledElement.ts +++ b/packages/cornerstone-tools/src/store/addEnabledElement.ts @@ -2,7 +2,6 @@ import { mouseEventListeners, wheelEventListener, keyEventListener, - labelmapStateEventListener, } from '../eventListeners' import { imageRenderedEventDispatcher, @@ -36,7 +35,6 @@ export default function addEnabledElement(evt: CustomEvent): void { mouseEventListeners.enable(element) wheelEventListener.enable(element) keyEventListener.enable(element) - labelmapStateEventListener.enable(element) // Dispatchers: renderer imageRenderedEventDispatcher.enable(element) diff --git a/packages/cornerstone-tools/src/store/addTool.ts b/packages/cornerstone-tools/src/store/addTool.ts index 36170947d..c2d72cd38 100644 --- a/packages/cornerstone-tools/src/store/addTool.ts +++ b/packages/cornerstone-tools/src/store/addTool.ts @@ -14,7 +14,9 @@ export function addTool(ToolClass, toolOptions) { const toolAlreadyAdded = state.tools[tool.name] !== undefined if (!hasToolName) { - throw new Error(`Tool with configuration did not produce a toolName: ${toolOptions}`) + throw new Error( + `Tool with configuration did not produce a toolName: ${toolOptions}` + ) } if (toolAlreadyAdded) { @@ -32,13 +34,17 @@ export function removeTool(ToolClass, toolOptions = {}) { const hasToolName = typeof tool.name !== 'undefined' && tool.name !== '' if (!hasToolName) { - throw new Error(`Tool with configuration did not produce a toolName: ${toolOptions}`) + throw new Error( + `Tool with configuration did not produce a toolName: ${toolOptions}` + ) } if (!state.tools[tool.name] !== undefined) { delete state.tools[tool.name] } else { - throw new Error(`${tool.name} cannot be removed because it has not been added`) + throw new Error( + `${tool.name} cannot be removed because it has not been added` + ) } } diff --git a/packages/cornerstone-tools/src/store/getToolDataNearPoint.ts b/packages/cornerstone-tools/src/store/getToolDataNearPoint.ts index e920451ee..5c46a0b81 100644 --- a/packages/cornerstone-tools/src/store/getToolDataNearPoint.ts +++ b/packages/cornerstone-tools/src/store/getToolDataNearPoint.ts @@ -1,7 +1,7 @@ import { getEnabledElement, Types } from '@precisionmetrics/cornerstone-render' import { BaseAnnotationTool } from '../tools' import { Point2, ToolSpecificToolData } from '../types' -import { getToolState } from '../stateManagement/toolState' +import { getToolState } from '../stateManagement/annotation/toolState' import ToolGroupManager from './ToolGroupManager' function getToolDataNearPoint( @@ -26,23 +26,26 @@ function getToolDataNearPointOnEnabledElement( proximity: number ): ToolSpecificToolData | null { const { renderingEngineUID, viewportUID } = enabledElement - const toolGroups = ToolGroupManager.getToolGroups( + const toolGroup = ToolGroupManager.getToolGroup( renderingEngineUID, viewportUID ) - for (let i = 0; i < toolGroups.length; ++i) { - const { _toolInstances: tools } = toolGroups[i] - for (const name in tools) { - if (Object.prototype.hasOwnProperty.call(tools, name)) { - const found = findToolDataNearPointByTool( - tools[name], - enabledElement, - point, - proximity - ) - if (found) { - return found - } + + if (!toolGroup) { + return null + } + + const { _toolInstances: tools } = toolGroup + for (const name in tools) { + if (Object.prototype.hasOwnProperty.call(tools, name)) { + const found = findToolDataNearPointByTool( + tools[name], + enabledElement, + point, + proximity + ) + if (found) { + return found } } } diff --git a/packages/cornerstone-tools/src/store/getToolsWithDataForElement.ts b/packages/cornerstone-tools/src/store/getToolsWithDataForElement.ts index fc0c325f8..a79ec29a5 100644 --- a/packages/cornerstone-tools/src/store/getToolsWithDataForElement.ts +++ b/packages/cornerstone-tools/src/store/getToolsWithDataForElement.ts @@ -1,4 +1,4 @@ -import { getToolState } from '../stateManagement/toolState' +import { getToolState } from '../stateManagement/annotation/toolState' import { ToolAndToolStateArray } from '../types/toolStateTypes' import { getEnabledElement } from '@precisionmetrics/cornerstone-render' diff --git a/packages/cornerstone-tools/src/store/index.ts b/packages/cornerstone-tools/src/store/index.ts index 61e6a9999..839cfcf25 100644 --- a/packages/cornerstone-tools/src/store/index.ts +++ b/packages/cornerstone-tools/src/store/index.ts @@ -16,9 +16,6 @@ import { getToolDataNearPointOnEnabledElement, } from './getToolDataNearPoint' -// TODO: -// - getToolGroupsForViewport? - export { // Store state, diff --git a/packages/cornerstone-tools/src/store/removeEnabledElement.ts b/packages/cornerstone-tools/src/store/removeEnabledElement.ts index 51e50e2ae..27fb19bb9 100644 --- a/packages/cornerstone-tools/src/store/removeEnabledElement.ts +++ b/packages/cornerstone-tools/src/store/removeEnabledElement.ts @@ -3,7 +3,6 @@ import { mouseEventListeners, wheelEventListener, keyEventListener, - labelmapStateEventListener, } from '../eventListeners' import { imageRenderedEventDispatcher, @@ -21,9 +20,8 @@ import getToolsWithModesForElement from '../util/getToolsWithModesForElement' import { ToolModes } from '../enums' import { removeToolState } from '../stateManagement' import getSynchronizers from './SynchronizerManager/getSynchronizers' -import getToolGroups from './ToolGroupManager/getToolGroups' +import getToolGroup from './ToolGroupManager/getToolGroup' import { annotationRenderingEngine } from '../util/triggerAnnotationRender' -import { IEnabledElement } from '@precisionmetrics/cornerstone-render/src/types' const VIEWPORT_ELEMENT = 'viewport-element' @@ -50,7 +48,6 @@ function removeEnabledElement(elementDisabledEvt: CustomEvent): void { wheelEventListener.disable(element) keyEventListener.disable(element) // labelmap - labelmapStateEventListener.disable(element) // Dispatchers: renderer imageRenderedEventDispatcher.disable(element) @@ -64,7 +61,7 @@ function removeEnabledElement(elementDisabledEvt: CustomEvent): void { // State // @TODO: We used to "disable" the tool before removal. Should we preserve the hook that would call on tools? _removeViewportFromSynchronizers(element) - _removeViewportFromToolGroups(element) + _removeViewportFromToolGroup(element) // _removeAllToolsForElement(canvas) _removeEnabledElement(element) @@ -79,13 +76,14 @@ const _removeViewportFromSynchronizers = (element: HTMLElement) => { }) } -const _removeViewportFromToolGroups = (element: HTMLElement) => { +const _removeViewportFromToolGroup = (element: HTMLElement) => { const { renderingEngineUID, viewportUID } = getEnabledElement(element) - const toolGroups = getToolGroups(renderingEngineUID, viewportUID) - toolGroups.forEach((toolGroup) => { + const toolGroup = getToolGroup(renderingEngineUID, viewportUID) + + if (toolGroup) { toolGroup.removeViewports(renderingEngineUID, viewportUID) - }) + } } const _removeAllToolsForElement = function (element) { diff --git a/packages/cornerstone-tools/src/store/state.ts b/packages/cornerstone-tools/src/store/state.ts index 6a298f50a..59f325708 100644 --- a/packages/cornerstone-tools/src/store/state.ts +++ b/packages/cornerstone-tools/src/store/state.ts @@ -1,6 +1,6 @@ import _cloneDeep from 'lodash.clonedeep' -import IToolGroup from './ToolGroupManager/IToolGroup' +import { IToolGroup } from '../types' import Synchronizer from './SynchronizerManager/Synchronizer' import svgNodeCache, { resetSvgNodeCache } from './svgNodeCache' import { BaseTool } from '../tools' diff --git a/packages/cornerstone-tools/src/tools/CrosshairsTool.ts b/packages/cornerstone-tools/src/tools/CrosshairsTool.ts index 6ec852e2b..48ec8d8ef 100644 --- a/packages/cornerstone-tools/src/tools/CrosshairsTool.ts +++ b/packages/cornerstone-tools/src/tools/CrosshairsTool.ts @@ -1,6 +1,7 @@ import { BaseAnnotationTool } from './base' // ~~ VTK Viewport import { + getEnabledElementByUIDs, getEnabledElement, RenderingEngine, getRenderingEngine, @@ -12,7 +13,7 @@ import { addToolState, getToolState, removeToolStateByToolDataUID, -} from '../stateManagement/toolState' +} from '../stateManagement/annotation/toolState' import { drawCircle as drawCircleSvg, drawHandles as drawHandlesSvg, @@ -33,7 +34,7 @@ import { Point2, Point3, } from '../types' -import { isToolDataLocked } from '../stateManagement/toolDataLocking' +import { isToolDataLocked } from '../stateManagement/annotation/toolDataLocking' import triggerAnnotationRenderForViewportUIDs from '../util/triggerAnnotationRenderForViewportUIDs' const { liangBarksyClip } = math.vec2 @@ -170,11 +171,12 @@ export default class CrosshairsTool extends BaseAnnotationTool { normal: Point3 point: Point3 } => { - const renderingEngine = getRenderingEngine(renderingEngineUID) - const viewport = renderingEngine.getViewport(viewportUID) + const enabledElement = getEnabledElementByUIDs( + renderingEngineUID, + viewportUID + ) + const { FrameOfReferenceUID, viewport } = enabledElement const { element } = viewport - const enabledElement = getEnabledElement(element) - const { FrameOfReferenceUID } = enabledElement const { position, focalPoint, viewPlaneNormal } = viewport.getCamera() // Check if there is already toolData for this viewport diff --git a/packages/cornerstone-tools/src/tools/annotation/BidirectionalTool.ts b/packages/cornerstone-tools/src/tools/annotation/BidirectionalTool.ts index 3748f5cfa..9faeeb038 100644 --- a/packages/cornerstone-tools/src/tools/annotation/BidirectionalTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/BidirectionalTool.ts @@ -17,8 +17,8 @@ import { addToolState, getToolState, removeToolState, -} from '../../stateManagement/toolState' -import { isToolDataLocked } from '../../stateManagement/toolDataLocking' +} from '../../stateManagement/annotation/toolState' +import { isToolDataLocked } from '../../stateManagement/annotation/toolDataLocking' import { drawLine as drawLineSvg, drawHandles as drawHandlesSvg, diff --git a/packages/cornerstone-tools/src/tools/annotation/EllipticalRoiTool.ts b/packages/cornerstone-tools/src/tools/annotation/EllipticalRoiTool.ts index b20922e4c..208178902 100644 --- a/packages/cornerstone-tools/src/tools/annotation/EllipticalRoiTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/EllipticalRoiTool.ts @@ -15,8 +15,8 @@ import { addToolState, getToolState, removeToolState, -} from '../../stateManagement/toolState' -import { isToolDataLocked } from '../../stateManagement/toolDataLocking' +} from '../../stateManagement/annotation/toolState' +import { isToolDataLocked } from '../../stateManagement/annotation/toolDataLocking' import { drawEllipse as drawEllipseSvg, drawHandles as drawHandlesSvg, diff --git a/packages/cornerstone-tools/src/tools/annotation/LengthTool.ts b/packages/cornerstone-tools/src/tools/annotation/LengthTool.ts index 769e20eeb..2a0079d13 100644 --- a/packages/cornerstone-tools/src/tools/annotation/LengthTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/LengthTool.ts @@ -18,8 +18,8 @@ import { addToolState, getToolState, removeToolState, -} from '../../stateManagement/toolState' -import { isToolDataLocked } from '../../stateManagement/toolDataLocking' +} from '../../stateManagement/annotation/toolState' +import { isToolDataLocked } from '../../stateManagement/annotation/toolDataLocking' import { lineSegment } from '../../util/math' import { diff --git a/packages/cornerstone-tools/src/tools/annotation/PET/SUVPeakTool.ts b/packages/cornerstone-tools/src/tools/annotation/PET/SUVPeakTool.ts index bc0863800..c8f3df132 100644 --- a/packages/cornerstone-tools/src/tools/annotation/PET/SUVPeakTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/PET/SUVPeakTool.ts @@ -18,7 +18,7 @@ import { } from '../../../cursors/elementCursor' import { state } from '../../../store' -import { isToolDataLocked } from '../../../stateManagement/toolDataLocking' +import { isToolDataLocked } from '../../../stateManagement/annotation/toolDataLocking' import { getCanvasEllipseCorners } from '../../../util/math/ellipse' import { getViewportUIDsWithToolToRender } from '../../../util/viewportFilters' @@ -30,7 +30,10 @@ import { import triggerAnnotationRenderForViewportUIDs from '../../../util/triggerAnnotationRenderForViewportUIDs' import { getToolStateForDisplay, getImageIdForTool } from '../../../util/planar' -import { addToolState, getToolState } from '../../../stateManagement/toolState' +import { + addToolState, + getToolState, +} from '../../../stateManagement/annotation/toolState' import suvPeakStrategy from './suvPeakStrategy' import EllipticalRoiTool, { EllipticalRoiSpecificToolData, diff --git a/packages/cornerstone-tools/src/tools/annotation/ProbeTool.ts b/packages/cornerstone-tools/src/tools/annotation/ProbeTool.ts index fca392407..535236505 100644 --- a/packages/cornerstone-tools/src/tools/annotation/ProbeTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/ProbeTool.ts @@ -16,7 +16,7 @@ import { addToolState, getToolState, removeToolState, -} from '../../stateManagement/toolState' +} from '../../stateManagement/annotation/toolState' import { drawHandles as drawHandlesSvg, drawTextBox as drawTextBoxSvg, diff --git a/packages/cornerstone-tools/src/tools/annotation/RectangleRoiTool.ts b/packages/cornerstone-tools/src/tools/annotation/RectangleRoiTool.ts index 0761d6e89..a6a6a615d 100644 --- a/packages/cornerstone-tools/src/tools/annotation/RectangleRoiTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/RectangleRoiTool.ts @@ -18,7 +18,7 @@ import { getToolState, removeToolState, } from '../../stateManagement' -import { isToolDataLocked } from '../../stateManagement/toolDataLocking' +import { isToolDataLocked } from '../../stateManagement/annotation/toolDataLocking' import { drawHandles as drawHandlesSvg, diff --git a/packages/cornerstone-tools/src/tools/base/BaseAnnotationTool.ts b/packages/cornerstone-tools/src/tools/base/BaseAnnotationTool.ts index 78b5ef53d..fefaa3a8f 100644 --- a/packages/cornerstone-tools/src/tools/base/BaseAnnotationTool.ts +++ b/packages/cornerstone-tools/src/tools/base/BaseAnnotationTool.ts @@ -6,9 +6,9 @@ import { import { vec4 } from 'gl-matrix' import BaseTool from './BaseTool' -import { isToolDataLocked } from '../../stateManagement/toolDataLocking' -import { getStyleProperty } from '../../stateManagement/toolStyle' -import { getViewportSpecificStateManager } from '../../stateManagement/toolState' +import { isToolDataLocked } from '../../stateManagement/annotation/toolDataLocking' +import { getStyleProperty } from '../../stateManagement/annotation/toolStyle' +import { getViewportSpecificStateManager } from '../../stateManagement/annotation/toolState' import { ToolSpecificToolData, ToolSpecificToolState, diff --git a/packages/cornerstone-tools/src/tools/base/BaseTool.ts b/packages/cornerstone-tools/src/tools/base/BaseTool.ts index 240b6b10d..742398f21 100644 --- a/packages/cornerstone-tools/src/tools/base/BaseTool.ts +++ b/packages/cornerstone-tools/src/tools/base/BaseTool.ts @@ -12,6 +12,7 @@ abstract class BaseTool { public supportedInteractionTypes: Array public configuration: Record public mode: ToolModes + public toolGroupUID: string // the toolGroup this instance belongs to constructor( toolConfiguration: Record, @@ -26,6 +27,7 @@ abstract class BaseTool { name, configuration = {}, supportedInteractionTypes, + toolGroupUID, } = this.initialConfiguration // If strategies are not initialized in the tool config @@ -36,6 +38,7 @@ abstract class BaseTool { configuration.strategyOptions = {} } + this.toolGroupUID = toolGroupUID this.name = name this.supportedInteractionTypes = supportedInteractionTypes || [] this.configuration = Object.assign({}, configuration) diff --git a/packages/cornerstone-tools/src/tools/displayTools/Labelmap/LabelmapConfig.ts b/packages/cornerstone-tools/src/tools/displayTools/Labelmap/LabelmapConfig.ts new file mode 100644 index 000000000..20bcb0843 --- /dev/null +++ b/packages/cornerstone-tools/src/tools/displayTools/Labelmap/LabelmapConfig.ts @@ -0,0 +1,45 @@ +export type LabelmapConfig = { + renderOutline?: boolean + outlineWidth?: number + outlineWidthActive?: number + outlineWidthInactive?: number + renderFill?: boolean + fillAlpha?: number + fillAlphaInactive?: number +} + +const defaultLabelmapConfig: LabelmapConfig = { + renderOutline: true, + outlineWidth: 3, + outlineWidthActive: 3, + outlineWidthInactive: 2, + renderFill: true, + fillAlpha: 0.9, + fillAlphaInactive: 0.85, + // Todo: not supported yet + // outlineAlpha: 0.7, + // outlineAlphaInactive: 0.35, + // Fill inside the render maps +} + +function getDefaultLabelmapConfig(): LabelmapConfig { + return defaultLabelmapConfig +} + +// Checks if the labelmap config is valid, which means +// if all the required fields are present and have the correct type +function isValidLabelmapConfig(config): boolean { + return ( + config && + typeof config.renderOutline === 'boolean' && + typeof config.outlineWidth === 'number' && + typeof config.outlineWidthActive === 'number' && + typeof config.outlineWidthInactive === 'number' && + typeof config.renderFill === 'boolean' && + typeof config.fillAlpha === 'number' && + typeof config.fillAlphaInactive === 'number' + ) +} + +export default getDefaultLabelmapConfig +export { isValidLabelmapConfig } diff --git a/packages/cornerstone-tools/src/tools/displayTools/Labelmap/LabelmapDisplay.ts b/packages/cornerstone-tools/src/tools/displayTools/Labelmap/LabelmapDisplay.ts new file mode 100644 index 000000000..0e122cb5b --- /dev/null +++ b/packages/cornerstone-tools/src/tools/displayTools/Labelmap/LabelmapDisplay.ts @@ -0,0 +1,305 @@ +import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction' +import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction' + +import { + cache, + getEnabledElementByUIDs, +} from '@precisionmetrics/cornerstone-render' + +import type { + VolumeViewport, + StackViewport, +} from '@precisionmetrics/cornerstone-render' +import { vtkVolume } from 'vtk.js/Sources/Rendering/Core/Volume' + +import * as SegmentationState from '../../../stateManagement/segmentation/segmentationState' +import { LabelmapRepresentation } from '../../../types/SegmentationRepresentationTypes' +import Representations from '../../../enums/SegmentationRepresentations' +import { getToolGroupByToolGroupUID } from '../../../store/ToolGroupManager' +import type { LabelmapConfig } from './LabelmapConfig' +import { + SegmentationConfig, + ToolGroupSpecificSegmentationData, +} from '../../../types/SegmentationStateTypes' + +import addSegmentationToElement from '../../../store/SegmentationModule/addSegmentationToElement' +import removeSegmentationFromElement from '../../../store/SegmentationModule/removeSegmentationFromElement' + +import { deepMerge } from '../../../util' +import { IToolGroup } from '../../../types' + +const MAX_NUMBER_COLORS = 255 + +/** + * For each viewport, and for each segmentation, set the segmentation for the viewport's enabled element + * Initializes the global and viewport specific state for the segmentation in the + * SegmentationStateManager. + * @param {IToolGroup} toolGroup - the tool group that contains the viewports + * @param {Partial[]} segmentationDataArray - the array of segmentation data + */ +async function addSegmentationData( + toolGroupUID: string, + segmentationData: Partial, + toolGroupSpecificConfig?: SegmentationConfig +): Promise { + const { volumeUID, segmentationDataUID, representation } = segmentationData + + await _addLabelmapToToolGroupViewports(toolGroupUID, segmentationData) + + // Viewport Specific Rendering State for the segmentation + // Merging the default configuration with the configuration passed in the arguments + const segmentsHidden = + segmentationData.segmentsHidden !== undefined + ? segmentationData.segmentsHidden + : (new Set() as Set) + + const visibility = + segmentationData.visibility !== undefined + ? segmentationData.visibility + : true + + const colorLUTIndex = + segmentationData.colorLUTIndex !== undefined + ? segmentationData.colorLUTIndex + : 0 + + const active = + segmentationData.active !== undefined ? segmentationData.active : true + + const cfun = + representation.config.cfun || vtkColorTransferFunction.newInstance() + const ofun = representation.config.ofun || vtkPiecewiseFunction.newInstance() + + const mergedSegmentationData = { + volumeUID, + segmentationDataUID, + segmentsHidden, + visibility, + colorLUTIndex, + active, + representation: { + type: Representations.Labelmap, + config: { + cfun, + ofun, + }, + }, + } as ToolGroupSpecificSegmentationData + + // Update the toolGroup specific configuration + if (toolGroupSpecificConfig) { + // Since setting configuration on toolGroup will trigger a segmentationState + // updated event, we don't want to trigger the event twice, so we suppress + // the first one + const suppressEvents = true + const currentToolGroupConfig = + SegmentationState.getSegmentationConfig(toolGroupUID) + + const mergedConfig = deepMerge( + currentToolGroupConfig, + toolGroupSpecificConfig + ) + + SegmentationState.setSegmentationConfig( + toolGroupUID, + { + renderInactiveSegmentations: + mergedConfig.renderInactiveSegmentations || true, + representations: { + ...mergedConfig.representations, + }, + }, + suppressEvents + ) + } + + // Add data first + SegmentationState.addSegmentationData(toolGroupUID, mergedSegmentationData) +} + +/** + * For each viewport, and for each segmentation, set the segmentation for the viewport's enabled element + * Initializes the global and viewport specific state for the segmentation in the + * SegmentationStateManager. + * @param {IToolGroup} toolGroup - the tool group that contains the viewports + * @param {Partial[]} segmentationDataArray - the array of segmentation data + */ +function removeSegmentationData( + toolGroupUID: string, + segmentationDataUID: string +): void { + _removeLabelmapFromToolGroupViewports(toolGroupUID, segmentationDataUID) + SegmentationState.removeSegmentationData(toolGroupUID, segmentationDataUID) +} + +/** + * It takes the enabled element, the segmentation UID, and the configuration, and + * it sets the segmentation for the enabled element as a labelmap + * @param enabledElement - The cornerstone enabled element + * @param {string} segmentationUID - The UID of the segmentation to be rendered. + * @param {unknown} configuration - The configuration object for the labelmap. + */ +function render( + viewport: VolumeViewport | StackViewport, + segmentationData: ToolGroupSpecificSegmentationData, + config: SegmentationConfig +): void { + const { + volumeUID: labelmapUID, + colorLUTIndex, + active, + representation, + segmentationDataUID, + visibility, + } = segmentationData + + const labelmapRepresentation = representation as LabelmapRepresentation + + const labelmap = cache.getVolume(labelmapUID) + + if (!labelmap) { + throw new Error(`No Labelmap found for UID: ${labelmapUID}`) + } + + const actor = viewport.getActor(segmentationDataUID) + + if (!actor) { + console.warn('No actor found for actorUID: ', segmentationDataUID) + } + + const { volumeActor } = actor + const { cfun, ofun } = labelmapRepresentation.config + + const labelmapConfig = config.representations[Representations.Labelmap] + const renderInactiveSegmentations = config.renderInactiveSegmentations + + _setLabelmapColorAndOpacity( + volumeActor, + cfun, + ofun, + colorLUTIndex, + labelmapConfig, + active, + renderInactiveSegmentations, + visibility + ) +} + +function _setLabelmapColorAndOpacity( + volumeActor: vtkVolume, + cfun: vtkColorTransferFunction, + ofun: vtkPiecewiseFunction, + colorLUTIndex: number, + labelmapConfig: LabelmapConfig, + isActiveLabelmap: boolean, + renderInactiveSegmentations: boolean, + visibility = true +): void { + ofun.addPoint(0, 0) + + const fillAlpha = isActiveLabelmap + ? labelmapConfig.fillAlpha + : labelmapConfig.fillAlphaInactive + const outlineWidth = isActiveLabelmap + ? labelmapConfig.outlineWidthActive + : labelmapConfig.outlineWidthInactive + + // Note: MAX_NUMBER_COLORS = 256 is needed because the current method to generate + // the default color table uses RGB. + + const colorLUT = SegmentationState.getColorLut(colorLUTIndex) + const numColors = Math.min(256, colorLUT.length) + + for (let i = 0; i < numColors; i++) { + const color = colorLUT[i] + cfun.addRGBPoint( + i, + color[0] / MAX_NUMBER_COLORS, + color[1] / MAX_NUMBER_COLORS, + color[2] / MAX_NUMBER_COLORS + ) + + // Set the opacity per label. + const segmentOpacity = (color[3] / 255) * fillAlpha + ofun.addPoint(i, segmentOpacity) + } + + ofun.setClamping(false) + + volumeActor.getProperty().setRGBTransferFunction(0, cfun) + volumeActor.getProperty().setScalarOpacity(0, ofun) + volumeActor.getProperty().setInterpolationTypeToNearest() + + volumeActor.getProperty().setUseLabelOutline(labelmapConfig.renderOutline) + volumeActor.getProperty().setLabelOutlineThickness(outlineWidth) + + // Set visibility based on whether actor visibility is specifically asked + // to be turned on/off (on by default) AND whether is is in active but + // we are rendering inactive labelmap + const visible = + visibility && (isActiveLabelmap || renderInactiveSegmentations) + volumeActor.setVisibility(visible) +} + +function _removeLabelmapFromToolGroupViewports( + toolGroupUID: string, + segmentationDataUID: string +): void { + const toolGroup = getToolGroupByToolGroupUID(toolGroupUID) + + if (toolGroup === undefined) { + throw new Error( + `ToolGroup with ToolGroupUID ${toolGroupUID} does not exist` + ) + } + + const { viewportsInfo } = toolGroup + + const segmentationData = SegmentationState.getSegmentationDataByUID( + toolGroupUID, + segmentationDataUID + ) + + for (const viewportInfo of viewportsInfo) { + const { viewportUID, renderingEngineUID } = viewportInfo + const enabledElement = getEnabledElementByUIDs( + renderingEngineUID, + viewportUID + ) + removeSegmentationFromElement( + enabledElement.viewport.element, + segmentationData + ) + } +} + +async function _addLabelmapToToolGroupViewports( + toolGroupUID, + segmentationData +): Promise { + const toolGroup = getToolGroupByToolGroupUID(toolGroupUID) as IToolGroup + const { viewportsInfo } = toolGroup + + for (const viewportInfo of viewportsInfo) { + const { viewportUID, renderingEngineUID } = viewportInfo + const enabledElement = getEnabledElementByUIDs( + renderingEngineUID, + viewportUID + ) + + if (!enabledElement) { + throw new Error( + `No enabled element found for rendering engine: ${renderingEngineUID} and viewport: ${viewportUID}` + ) + } + + const { viewport } = enabledElement + addSegmentationToElement(viewport.element, segmentationData) + } +} + +export default { + render, + addSegmentationData, + removeSegmentationData, +} diff --git a/packages/cornerstone-tools/src/tools/displayTools/Labelmap/index.ts b/packages/cornerstone-tools/src/tools/displayTools/Labelmap/index.ts new file mode 100644 index 000000000..1263cc584 --- /dev/null +++ b/packages/cornerstone-tools/src/tools/displayTools/Labelmap/index.ts @@ -0,0 +1,4 @@ +import LabelmapDisplay from './LabelmapDisplay' +import LabelmapConfig from './LabelmapConfig' + +export { LabelmapDisplay, LabelmapConfig } diff --git a/packages/cornerstone-tools/src/tools/displayTools/SegmentationDisplayTool.ts b/packages/cornerstone-tools/src/tools/displayTools/SegmentationDisplayTool.ts new file mode 100644 index 000000000..dfa9b68f6 --- /dev/null +++ b/packages/cornerstone-tools/src/tools/displayTools/SegmentationDisplayTool.ts @@ -0,0 +1,122 @@ +import { BaseTool } from '../base' +import { getEnabledElementByUIDs } from '@precisionmetrics/cornerstone-render' +import Representations from '../../enums/SegmentationRepresentations' +import { getSegmentationState } from '../../stateManagement/segmentation/segmentationState' +import { LabelmapDisplay } from './Labelmap' +import { + triggerSegmentationStateModified, + segmentationConfigController, +} from '../../store/SegmentationModule' +import { getToolGroupByToolGroupUID } from '../../store/ToolGroupManager' +import { + ToolGroupSpecificSegmentationData, + SegmentationConfig, +} from '../../types/SegmentationStateTypes' + +import { deepMerge } from '../../util' + +export default class SegmentationDisplayTool extends BaseTool { + constructor(toolConfiguration = {}) { + super(toolConfiguration, { + name: 'SegmentationDisplay', + configuration: {}, + }) + } + + // Todo: this is too weird that we are passing toolGroupUID to the enableCallback + enableCallback(): void { + const toolGroupUID = this.toolGroupUID + const toolGroupSegmentationState = getSegmentationState(toolGroupUID) + + if (toolGroupSegmentationState.length === 0) { + return + } + + // for each segmentationData, make the visibility false + for (const segmentationData of toolGroupSegmentationState) { + segmentationData.visibility = true + } + + // trigger the update + triggerSegmentationStateModified(toolGroupUID) + } + + disableCallback(): void { + const toolGroupUID = this.toolGroupUID + const toolGroupSegmentationState = getSegmentationState(toolGroupUID) + + if (toolGroupSegmentationState.length === 0) { + return + } + + // for each segmentationData, make the visibility false + for (const segmentationData of toolGroupSegmentationState) { + segmentationData.visibility = false + } + + // trigger the update + triggerSegmentationStateModified(toolGroupUID) + } + + renderToolData(evt): void { + const { toolGroupUID } = evt + + const toolGroup = getToolGroupByToolGroupUID(toolGroupUID) + + if (!toolGroup) { + return + } + + const toolGroupSegmentationState = getSegmentationState(toolGroupUID) + + // toolGroup Viewports + const toolGroupViewports = toolGroup.viewportsInfo.map( + ({ renderingEngineUID, viewportUID }) => { + const enabledElement = getEnabledElementByUIDs( + renderingEngineUID, + viewportUID + ) + + if (enabledElement) { + return enabledElement.viewport + } + } + ) + + // Render each segmentationData, in each viewport in the toolGroup + toolGroupSegmentationState.forEach( + (segmentationData: ToolGroupSpecificSegmentationData) => { + const config = this._getSegmentationConfig(toolGroupUID) + const { representation } = segmentationData + + toolGroupViewports.forEach((viewport) => { + if (representation.type == Representations.Labelmap) { + LabelmapDisplay.render(viewport, segmentationData, config) + } else { + throw new Error( + `Render for ${representation.type} is not supported yet` + ) + } + }) + } + ) + + // for all viewports in the toolGroup trigger a re-render + toolGroupViewports.forEach((viewport) => { + viewport.render() + }) + } + + _getSegmentationConfig(toolGroupUID: string): SegmentationConfig { + const toolGroupConfig = + segmentationConfigController.getSegmentationConfig(toolGroupUID) + + const globalConfig = + segmentationConfigController.getGlobalSegmentationConfig() + + // merge two configurations and override the global config + const mergedConfig = deepMerge(globalConfig, toolGroupConfig) + + return mergedConfig + } +} diff --git a/packages/cornerstone-tools/src/tools/index.ts b/packages/cornerstone-tools/src/tools/index.ts index 1e788eda3..bab92ecce 100644 --- a/packages/cornerstone-tools/src/tools/index.ts +++ b/packages/cornerstone-tools/src/tools/index.ts @@ -13,6 +13,11 @@ import LengthTool from './annotation/LengthTool' import ProbeTool from './annotation/ProbeTool' import RectangleRoiTool from './annotation/RectangleRoiTool' import EllipticalRoiTool from './annotation/EllipticalRoiTool' + +// Segmentation DisplayTool +import SegmentationDisplayTool from './displayTools/SegmentationDisplayTool' + +// Segmentation Tools import RectangleScissorsTool from './segmentation/RectangleScissorsTool' import CircleScissorsTool from './segmentation/CircleScissorsTool' import SphereScissorsTool from './segmentation/SphereScissorsTool' @@ -41,7 +46,9 @@ export { ProbeTool, RectangleRoiTool, EllipticalRoiTool, - // Segmentations + // Segmentations Display + SegmentationDisplayTool, + // Segmentations Tools RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, diff --git a/packages/cornerstone-tools/src/tools/segmentation/CircleScissorsTool.ts b/packages/cornerstone-tools/src/tools/segmentation/CircleScissorsTool.ts index 2af4f0b92..84540fdcb 100644 --- a/packages/cornerstone-tools/src/tools/segmentation/CircleScissorsTool.ts +++ b/packages/cornerstone-tools/src/tools/segmentation/CircleScissorsTool.ts @@ -2,10 +2,9 @@ import { cache, getEnabledElement, StackViewport, - VolumeViewport, } from '@precisionmetrics/cornerstone-render' import { BaseTool } from '../base' -import { Point3, Point2 } from '../../types' +import { Point3 } from '../../types' import { fillInsideCircle } from './strategies/fillCircle' import { CornerstoneTools3DEvents as EVENTS } from '../../enums' @@ -17,10 +16,10 @@ import { import triggerAnnotationRenderForViewportUIDs from '../../util/triggerAnnotationRenderForViewportUIDs' import { - getColorForSegmentIndex, + segmentationColorController, lockedSegmentController, segmentIndexController, - activeLabelmapController, + activeSegmentationController, } from '../../store/SegmentationModule' // Todo @@ -30,14 +29,15 @@ import { * @public * @class CircleScissorsTool * @memberof Tools - * @classdesc Tool for manipulating labelmap data by drawing a rectangle. + * @classdesc Tool for manipulating segmentation data by drawing a rectangle. * @extends Tools.Base.BaseTool */ export default class CircleScissorsTool extends BaseTool { editData: { toolData: any - labelmap: any + segmentation: any segmentIndex: number + segmentationDataUID: string segmentsLocked: number[] segmentColor: [number, number, number, number] viewportUIDsToRender: string[] @@ -53,7 +53,7 @@ export default class CircleScissorsTool extends BaseTool { constructor(toolConfiguration = {}) { super(toolConfiguration, { - name: 'CircleScissors', + name: 'CircleScissor', supportedInteractionTypes: ['Mouse', 'Touch'], configuration: { strategies: { @@ -79,29 +79,28 @@ export default class CircleScissorsTool extends BaseTool { const camera = viewport.getCamera() const { viewPlaneNormal, viewUp } = camera + const toolGroupUID = this.toolGroupUID - const labelmapIndex = - activeLabelmapController.getActiveLabelmapIndex(element) - if (labelmapIndex === undefined) { + const activeSegmentationInfo = + activeSegmentationController.getActiveSegmentationInfo(toolGroupUID) + if (!activeSegmentationInfo) { throw new Error( - 'No active labelmap detected, create one before using scissors tool' + 'No active segmentation detected, create one before using scissors tool' ) } - const labelmapUID = await activeLabelmapController.getActiveLabelmapUID( - element - ) - - const segmentIndex = segmentIndexController.getActiveSegmentIndex(element) - const segmentColor = getColorForSegmentIndex( - element, - segmentIndex, - labelmapIndex - ) + const { volumeUID, segmentationDataUID } = activeSegmentationInfo + const segmentIndex = + segmentIndexController.getActiveSegmentIndex(toolGroupUID) const segmentsLocked = - lockedSegmentController.getLockedSegmentsForElement(element) + lockedSegmentController.getLockedSegmentsForSegmentation(volumeUID) + const segmentColor = segmentationColorController.getColorForSegmentIndex( + toolGroupUID, + activeSegmentationInfo.segmentationDataUID, + segmentIndex + ) - const labelmap = cache.getVolume(labelmapUID) + const segmentation = cache.getVolume(volumeUID) // Todo: Used for drawing the svg only, we might not need it at all const toolData = { @@ -129,9 +128,10 @@ export default class CircleScissorsTool extends BaseTool { this.editData = { toolData, - labelmap, + segmentation, centerCanvas: canvasPos, segmentIndex, + segmentationDataUID, segmentsLocked, segmentColor, viewportUIDsToRender, @@ -203,9 +203,10 @@ export default class CircleScissorsTool extends BaseTool { toolData, newAnnotation, hasMoved, - labelmap, + segmentation, segmentIndex, segmentsLocked, + segmentationDataUID, } = this.editData const { data } = toolData const { viewPlaneNormal, viewUp } = toolData.metadata @@ -222,7 +223,7 @@ export default class CircleScissorsTool extends BaseTool { resetElementCursor(element) const enabledElement = getEnabledElement(element) - const { viewport, renderingEngine } = enabledElement + const { viewport } = enabledElement this.editData = null this.isDrawing = false @@ -233,20 +234,16 @@ export default class CircleScissorsTool extends BaseTool { const operationData = { points: data.handles.points, - volume: labelmap, + volume: segmentation, segmentIndex, segmentsLocked, viewPlaneNormal, + toolGroupUID: this.toolGroupUID, + segmentationDataUID, viewUp, } - const eventDetail = { - element, - enabledElement, - renderingEngine, - } - - this.applyActiveStrategy(eventDetail, operationData) + this.applyActiveStrategy(enabledElement, operationData) } /** diff --git a/packages/cornerstone-tools/src/tools/segmentation/RectangleRoiStartEndThreshold.ts b/packages/cornerstone-tools/src/tools/segmentation/RectangleRoiStartEndThreshold.ts index ab9e5439e..fc829300b 100644 --- a/packages/cornerstone-tools/src/tools/segmentation/RectangleRoiStartEndThreshold.ts +++ b/packages/cornerstone-tools/src/tools/segmentation/RectangleRoiStartEndThreshold.ts @@ -15,7 +15,7 @@ import { getSpacingInNormalDirection, } from '../../util/planar' import { addToolState, getToolState } from '../../stateManagement' -import { isToolDataLocked } from '../../stateManagement/toolDataLocking' +import { isToolDataLocked } from '../../stateManagement/annotation/toolDataLocking' import { drawHandles as drawHandlesSvg, drawRect as drawRectSvg, diff --git a/packages/cornerstone-tools/src/tools/segmentation/RectangleRoiThreshold.ts b/packages/cornerstone-tools/src/tools/segmentation/RectangleRoiThreshold.ts index 7780b039f..2faf7bc76 100644 --- a/packages/cornerstone-tools/src/tools/segmentation/RectangleRoiThreshold.ts +++ b/packages/cornerstone-tools/src/tools/segmentation/RectangleRoiThreshold.ts @@ -3,13 +3,12 @@ import { getVolume, Settings, StackViewport, - VolumeViewport, triggerEvent, eventTarget, } from '@precisionmetrics/cornerstone-render' import { getImageIdForTool } from '../../util/planar' import { addToolState, getToolState } from '../../stateManagement' -import { isToolDataLocked } from '../../stateManagement/toolDataLocking' +import { isToolDataLocked } from '../../stateManagement/annotation/toolDataLocking' import { CornerstoneTools3DEvents as EVENTS } from '../../enums' import { @@ -17,10 +16,7 @@ import { drawRect as drawRectSvg, } from '../../drawingSvg' import { getViewportUIDsWithToolToRender } from '../../util/viewportFilters' -import { - resetElementCursor, - hideElementCursor, -} from '../../cursors/elementCursor' +import { hideElementCursor } from '../../cursors/elementCursor' import triggerAnnotationRenderForViewportUIDs from '../../util/triggerAnnotationRenderForViewportUIDs' import { ToolSpecificToolData, Point3 } from '../../types' @@ -45,7 +41,7 @@ export interface RectangleRoiThresholdToolData extends ToolSpecificToolData { points: Point3[] activeHandleIndex: number | null } - // labelmapUID: string + // segmentationUID: string active: boolean } } @@ -108,7 +104,7 @@ export default class RectangleRoiThresholdTool extends RectangleRoiTool { } // Todo: how not to store enabledElement on the toolData, segmentationModule needs the element to - // decide on the active segmentIndex, active labelmapIndex etc. + // decide on the active segmentIndex, active segmentationIndex etc. const toolData = { metadata: { viewPlaneNormal: [...viewPlaneNormal], @@ -136,7 +132,7 @@ export default class RectangleRoiThresholdTool extends RectangleRoiTool { ], activeHandleIndex: null, }, - labelmapUID: null, + segmentationUID: null, active: true, }, } diff --git a/packages/cornerstone-tools/src/tools/segmentation/RectangleScissorsTool.ts b/packages/cornerstone-tools/src/tools/segmentation/RectangleScissorsTool.ts index 4ebfaa313..3f51fc451 100644 --- a/packages/cornerstone-tools/src/tools/segmentation/RectangleScissorsTool.ts +++ b/packages/cornerstone-tools/src/tools/segmentation/RectangleScissorsTool.ts @@ -3,13 +3,12 @@ import { getEnabledElement, Settings, StackViewport, - VolumeViewport, } from '@precisionmetrics/cornerstone-render' import { BaseTool } from '../base' import { Point3, Point2 } from '../../types' import { fillInsideRectangle } from './strategies/fillRectangle' import { eraseInsideRectangle } from './strategies/eraseRectangle' -import { getViewportUIDsWithLabelmapToRender } from '../../util/viewportFilters' +import { getViewportUIDsWithToolToRender } from '../../util/viewportFilters' import { CornerstoneTools3DEvents as EVENTS } from '../../enums' import RectangleRoiTool from '../annotation/RectangleRoiTool' @@ -21,24 +20,25 @@ import { import triggerAnnotationRenderForViewportUIDs from '../../util/triggerAnnotationRenderForViewportUIDs' import { - getColorForSegmentIndex, + segmentationColorController, lockedSegmentController, segmentIndexController, - activeLabelmapController, + activeSegmentationController, } from '../../store/SegmentationModule' /** * @public * @class RectangleScissorsTool * @memberof Tools - * @classdesc Tool for manipulating labelmap data by drawing a rectangle. + * @classdesc Tool for manipulating segmentation data by drawing a rectangle. * @extends Tools.Base.BaseTool */ export default class RectangleScissorsTool extends BaseTool { _throttledCalculateCachedStats: any editData: { toolData: any - labelmap: any + segmentationDataUID: string + segmentation: any segmentIndex: number segmentsLocked: number[] segmentColor: [number, number, number, number] @@ -54,7 +54,7 @@ export default class RectangleScissorsTool extends BaseTool { constructor(toolConfiguration = {}) { super(toolConfiguration, { - name: 'RectangleScissors', + name: 'RectangleScissor', supportedInteractionTypes: ['Mouse', 'Touch'], configuration: { strategies: { @@ -79,27 +79,30 @@ export default class RectangleScissorsTool extends BaseTool { const camera = viewport.getCamera() const { viewPlaneNormal, viewUp } = camera + const toolGroupUID = this.toolGroupUID - const labelmapIndex = - activeLabelmapController.getActiveLabelmapIndex(element) - if (labelmapIndex === undefined) { + const activeSegmentationInfo = + activeSegmentationController.getActiveSegmentationInfo(toolGroupUID) + if (!activeSegmentationInfo) { throw new Error( - 'No active labelmap detected, create one before using scissors tool' + 'No active segmentation detected, create one before using scissors tool' ) } - const labelmapUID = activeLabelmapController.getActiveLabelmapUID(element) - - const segmentIndex = segmentIndexController.getActiveSegmentIndex(element) + // Todo: we should have representation type check if we are going to use this + // tool in other representations other than labelmap + const { segmentationDataUID, volumeUID } = activeSegmentationInfo + const segmentIndex = + segmentIndexController.getActiveSegmentIndex(toolGroupUID) const segmentsLocked = - lockedSegmentController.getLockedSegmentsForElement(element) - const segmentColor = getColorForSegmentIndex( - element, - segmentIndex, - labelmapIndex + lockedSegmentController.getLockedSegmentsForSegmentation(volumeUID) + const segmentColor = segmentationColorController.getColorForSegmentIndex( + toolGroupUID, + activeSegmentationInfo.segmentationDataUID, + segmentIndex ) - const labelmap = cache.getVolume(labelmapUID) + const segmentation = cache.getVolume(volumeUID) // Todo: Used for drawing the svg only, we might not need it at all const toolData = { @@ -129,17 +132,18 @@ export default class RectangleScissorsTool extends BaseTool { // Ensure settings are initialized after tool data instantiation Settings.getObjectSettings(toolData, RectangleRoiTool) - const viewportUIDsToRender = getViewportUIDsWithLabelmapToRender( + const viewportUIDsToRender = getViewportUIDsWithToolToRender( element, this.name ) this.editData = { toolData, - labelmap, + segmentation, segmentIndex, segmentsLocked, segmentColor, + segmentationDataUID, viewportUIDsToRender, handleIndex: 3, movingTextBox: false, @@ -244,7 +248,8 @@ export default class RectangleScissorsTool extends BaseTool { toolData, newAnnotation, hasMoved, - labelmap, + segmentation, + segmentationDataUID, segmentIndex, segmentsLocked, } = this.editData @@ -262,7 +267,7 @@ export default class RectangleScissorsTool extends BaseTool { resetElementCursor(element) const enabledElement = getEnabledElement(element) - const { viewport, renderingEngine } = enabledElement + const { viewport } = enabledElement this.editData = null this.isDrawing = false @@ -273,18 +278,14 @@ export default class RectangleScissorsTool extends BaseTool { const operationData = { points: data.handles.points, - volume: labelmap, + volume: segmentation, + segmentationDataUID, segmentIndex, segmentsLocked, + toolGroupUID: this.toolGroupUID, } - const eventDetail = { - canvas: element, - enabledElement, - renderingEngine, - } - - this.applyActiveStrategy(eventDetail, operationData) + this.applyActiveStrategy(enabledElement, operationData) } /** @@ -350,19 +351,4 @@ export default class RectangleScissorsTool extends BaseTool { } ) } - - _getTargetVolumeUID = (viewport) => { - if (this.configuration.volumeUID) { - return this.configuration.volumeUID - } - - const actors = viewport.getActors() - - if (!actors && !actors.length) { - // No stack to scroll through - return - } - - return actors[0].uid - } } diff --git a/packages/cornerstone-tools/src/tools/segmentation/SphereScissorsTool.ts b/packages/cornerstone-tools/src/tools/segmentation/SphereScissorsTool.ts index 1fcdc72b7..f5aabbc1f 100644 --- a/packages/cornerstone-tools/src/tools/segmentation/SphereScissorsTool.ts +++ b/packages/cornerstone-tools/src/tools/segmentation/SphereScissorsTool.ts @@ -2,10 +2,9 @@ import { cache, getEnabledElement, StackViewport, - VolumeViewport, } from '@precisionmetrics/cornerstone-render' import { BaseTool } from '../base' -import { Point3, Point2 } from '../../types' +import { Point3 } from '../../types' import { fillInsideSphere } from './strategies/fillSphere' import { CornerstoneTools3DEvents as EVENTS } from '../../enums' @@ -17,10 +16,10 @@ import { import triggerAnnotationRenderForViewportUIDs from '../../util/triggerAnnotationRenderForViewportUIDs' import { - getColorForSegmentIndex, + segmentationColorController, lockedSegmentController, segmentIndexController, - activeLabelmapController, + activeSegmentationController, } from '../../store/SegmentationModule' // Todo @@ -30,15 +29,17 @@ import { * @public * @class SphereScissorsTool * @memberof Tools - * @classdesc Tool for manipulating labelmap data by drawing a rectangle. + * @classdesc Tool for manipulating segmentation data by drawing a rectangle. * @extends Tools.Base.BaseTool */ export default class SphereScissorsTool extends BaseTool { editData: { toolData: any - labelmap: any + segmentation: any segmentIndex: number segmentsLocked: number[] + segmentationDataUID: string + toolGroupUID: string segmentColor: [number, number, number, number] viewportUIDsToRender: string[] handleIndex?: number @@ -53,7 +54,7 @@ export default class SphereScissorsTool extends BaseTool { constructor(toolConfiguration = {}) { super(toolConfiguration, { - name: 'SphereScissors', + name: 'SphereScissor', supportedInteractionTypes: ['Mouse', 'Touch'], configuration: { strategies: { @@ -79,29 +80,28 @@ export default class SphereScissorsTool extends BaseTool { const camera = viewport.getCamera() const { viewPlaneNormal, viewUp } = camera + const toolGroupUID = this.toolGroupUID - const labelmapIndex = - activeLabelmapController.getActiveLabelmapIndex(element) - if (labelmapIndex === undefined) { + const activeSegmentationInfo = + activeSegmentationController.getActiveSegmentationInfo(toolGroupUID) + if (!activeSegmentationInfo) { throw new Error( - 'No active labelmap detected, create one before using scissors tool' + 'No active segmentation detected, create one before using scissors tool' ) } - const labelmapUID = await activeLabelmapController.getActiveLabelmapUID( - element - ) - - const segmentIndex = segmentIndexController.getActiveSegmentIndex(element) - const segmentColor = getColorForSegmentIndex( - element, - segmentIndex, - labelmapIndex - ) + const { volumeUID, segmentationDataUID } = activeSegmentationInfo + const segmentIndex = + segmentIndexController.getActiveSegmentIndex(toolGroupUID) const segmentsLocked = - lockedSegmentController.getLockedSegmentsForElement(element) + lockedSegmentController.getLockedSegmentsForSegmentation(volumeUID) + const segmentColor = segmentationColorController.getColorForSegmentIndex( + toolGroupUID, + activeSegmentationInfo.segmentationDataUID, + segmentIndex + ) - const labelmap = cache.getVolume(labelmapUID) + const segmentation = cache.getVolume(volumeUID) // Used for drawing the svg only, we might not need it at all const toolData = { @@ -129,11 +129,13 @@ export default class SphereScissorsTool extends BaseTool { this.editData = { toolData, - labelmap, + segmentation, centerCanvas: canvasPos, segmentIndex, segmentsLocked, segmentColor, + segmentationDataUID, + toolGroupUID, viewportUIDsToRender, handleIndex: 3, movingTextBox: false, @@ -201,9 +203,10 @@ export default class SphereScissorsTool extends BaseTool { toolData, newAnnotation, hasMoved, - labelmap, + segmentation, segmentIndex, segmentsLocked, + segmentationDataUID, } = this.editData const { data } = toolData const { viewPlaneNormal, viewUp } = toolData.metadata @@ -220,7 +223,7 @@ export default class SphereScissorsTool extends BaseTool { resetElementCursor(element) const enabledElement = getEnabledElement(element) - const { viewport, renderingEngine } = enabledElement + const { viewport } = enabledElement this.editData = null this.isDrawing = false @@ -231,20 +234,16 @@ export default class SphereScissorsTool extends BaseTool { const operationData = { points: data.handles.points, - volume: labelmap, + volume: segmentation, segmentIndex, segmentsLocked, + segmentationDataUID, + toolGroupUID: this.toolGroupUID, viewPlaneNormal, viewUp, } - const eventDetail = { - canvas: element, - enabledElement, - renderingEngine, - } - - this.applyActiveStrategy(eventDetail, operationData) + this.applyActiveStrategy(enabledElement, operationData) } /** diff --git a/packages/cornerstone-tools/src/tools/segmentation/strategies/eraseRectangle.ts b/packages/cornerstone-tools/src/tools/segmentation/strategies/eraseRectangle.ts index 219f0437f..c08ead5e1 100644 --- a/packages/cornerstone-tools/src/tools/segmentation/strategies/eraseRectangle.ts +++ b/packages/cornerstone-tools/src/tools/segmentation/strategies/eraseRectangle.ts @@ -1,38 +1,31 @@ import { getBoundingBoxAroundShape } from '../../../util/segmentation' import { Point3 } from '../../../types' -import { ImageVolume } from '@precisionmetrics/cornerstone-render' -import { IEnabledElement } from '@precisionmetrics/cornerstone-render/src/types' -import triggerLabelmapRender from '../../../util/segmentation/triggerLabelmapRender' +import { ImageVolume, Types } from '@precisionmetrics/cornerstone-render' +import { triggerSegmentationDataModified } from '../../../store/SegmentationModule/triggerSegmentationEvents' import pointInShapeCallback from '../../../util/planar/pointInShapeCallback' type EraseOperationData = { + toolGroupUID: string + segmentationDataUID: string points: [Point3, Point3, Point3, Point3] volume: ImageVolume constraintFn: (x: [number, number, number]) => boolean segmentsLocked: number[] } -type FillRectangleEvent = { - enabledElement: IEnabledElement -} - -/** - * eraseRectangle - Erases all pixels inside/outside the region defined - * by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} - */ function eraseRectangle( - evt: FillRectangleEvent, + enabledElement: Types.IEnabledElement, operationData: EraseOperationData, inside = true ): void { - const { enabledElement } = evt - const { renderingEngine, viewport } = enabledElement - const { volume: labelmapVolume, points, segmentsLocked } = operationData - const { imageData, dimensions, scalarData } = labelmapVolume + const { + volume: segmentation, + points, + segmentsLocked, + segmentationDataUID, + toolGroupUID, + } = operationData + const { imageData, dimensions, scalarData } = segmentation const rectangleCornersIJK = points.map((world) => { return imageData.worldToIndex(world) @@ -63,33 +56,31 @@ function eraseRectangle( callback ) - triggerLabelmapRender(renderingEngine, labelmapVolume, imageData) + triggerSegmentationDataModified(toolGroupUID, segmentationDataUID) } /** - * Erases all pixels inside the region defined by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} + * Erase the rectangle region segment inside the segmentation defined by the operationData. + * It erases the segmentation pixels inside the defined rectangle. + * @param enabledElement - The element for which the segment is being erased. + * @param {EraseOperationData} operationData - EraseOperationData */ export function eraseInsideRectangle( - evt: FillRectangleEvent, + enabledElement: Types.IEnabledElement, operationData: EraseOperationData ): void { - eraseRectangle(evt, operationData, true) + eraseRectangle(enabledElement, operationData, true) } /** - * Erases all pixels outside the region defined by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} + * Erase the rectangle region segment inside the segmentation defined by the operationData. + * It erases the segmentation pixels outside the defined rectangle. + * @param enabledElement - The element for which the segment is being erased. + * @param {EraseOperationData} operationData - EraseOperationData */ export function eraseOutsideRectangle( - evt: FillRectangleEvent, + enabledElement: Types.IEnabledElement, operationData: EraseOperationData ): void { - eraseRectangle(evt, operationData, false) + eraseRectangle(enabledElement, operationData, false) } diff --git a/packages/cornerstone-tools/src/tools/segmentation/strategies/fillCircle.ts b/packages/cornerstone-tools/src/tools/segmentation/strategies/fillCircle.ts index c64989ce9..f63a8c896 100644 --- a/packages/cornerstone-tools/src/tools/segmentation/strategies/fillCircle.ts +++ b/packages/cornerstone-tools/src/tools/segmentation/strategies/fillCircle.ts @@ -9,13 +9,13 @@ import { getCanvasEllipseCorners, pointInEllipse, } from '../../../util/math/ellipse' -import { - getBoundingBoxAroundShape, - triggerLabelmapRender, -} from '../../../util/segmentation' +import { getBoundingBoxAroundShape } from '../../../util/segmentation' +import { triggerSegmentationDataModified } from '../../../store/SegmentationModule/triggerSegmentationEvents' import { pointInShapeCallback } from '../../../util/planar' type OperationData = { + toolGroupUID: string + segmentationDataUID: string points: any // Todo:fix volume: IImageVolume segmentIndex: number @@ -25,38 +25,28 @@ type OperationData = { constraintFn: () => boolean } -type fillCircleEvent = { - enabledElement: IEnabledElement -} - +// Todo: i don't think we need this we can use indexToWorldVec3 function worldToIndex(imageData, ain) { const vout = vec3.fromValues(0, 0, 0) imageData.worldToIndex(ain, vout) return vout } -/** - * fillInsideCircle - Fill all pixels inside/outside the region defined - * by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} - */ function fillCircle( - evt: fillCircleEvent, + enabledElement: IEnabledElement, operationData: OperationData, inside = true ): void { - const { enabledElement } = evt const { - volume: labelmapVolume, + volume: segmentationVolume, points, segmentsLocked, segmentIndex, + toolGroupUID, + segmentationDataUID, } = operationData - const { imageData, dimensions, scalarData } = labelmapVolume - const { viewport, renderingEngine } = enabledElement + const { imageData, dimensions, scalarData } = segmentationVolume + const { viewport } = enabledElement // Average the points to get the center of the ellipse const center = vec3.fromValues(0, 0, 0) @@ -111,34 +101,31 @@ function fillCircle( callback ) - // Todo: optimize modified slices for all orthogonal views - triggerLabelmapRender(renderingEngine, labelmapVolume, imageData) + triggerSegmentationDataModified(toolGroupUID, segmentationDataUID) } /** - * Fill all pixels inside/outside the region defined by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} + * Fill inside the circular region segment inside the segmentation defined by the operationData. + * It fills the segmentation pixels inside the defined circle. + * @param enabledElement - The element for which the segment is being erased. + * @param {EraseOperationData} operationData - EraseOperationData */ export function fillInsideCircle( - evt: fillCircleEvent, + enabledElement: IEnabledElement, operationData: OperationData ): void { - fillCircle(evt, operationData, true) + fillCircle(enabledElement, operationData, true) } /** - * Fill all pixels outside the region defined by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} + * Fill outside the circular region segment inside the segmentation defined by the operationData. + * It fills the segmentation pixels outside the defined circle. + * @param enabledElement - The element for which the segment is being erased. + * @param {EraseOperationData} operationData - EraseOperationData */ export function fillOutsideCircle( - evt: fillCircleEvent, + enabledElement: IEnabledElement, operationData: OperationData ): void { - fillCircle(evt, operationData, false) + fillCircle(enabledElement, operationData, false) } diff --git a/packages/cornerstone-tools/src/tools/segmentation/strategies/fillRectangle.ts b/packages/cornerstone-tools/src/tools/segmentation/strategies/fillRectangle.ts index 6f3b5e048..8de53852b 100644 --- a/packages/cornerstone-tools/src/tools/segmentation/strategies/fillRectangle.ts +++ b/packages/cornerstone-tools/src/tools/segmentation/strategies/fillRectangle.ts @@ -1,11 +1,12 @@ -import { IEnabledElement } from '@precisionmetrics/cornerstone-render/src/types' import { getBoundingBoxAroundShape } from '../../../util/segmentation' import { Point3 } from '../../../types' -import { ImageVolume } from '@precisionmetrics/cornerstone-render' +import { ImageVolume, Types } from '@precisionmetrics/cornerstone-render' import pointInShapeCallback from '../../../util/planar/pointInShapeCallback' -import triggerLabelmapRender from '../../../util/segmentation/triggerLabelmapRender' +import { triggerSegmentationDataModified } from '../../../store/SegmentationModule/triggerSegmentationEvents' type OperationData = { + toolGroupUID: string + segmentationDataUID: string points: [Point3, Point3, Point3, Point3] volume: ImageVolume constraintFn: (x: [number, number, number]) => boolean @@ -13,33 +14,30 @@ type OperationData = { segmentsLocked: number[] } -type FillRectangleEvent = { - enabledElement: IEnabledElement -} - /** - * FillInsideRectangle - Fill all pixels inside/outside the region defined - * by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} + * For each point in the bounding box around the rectangle, if the point is inside + * the rectangle, set the scalar value to the segmentIndex + * @param {string} toolGroupUID - string + * @param {OperationData} operationData - OperationData + * @param {any} [constraintFn] + * @param [inside=true] - boolean */ +// Todo: why we have another constraintFn? in addition to the one in the operationData? function fillRectangle( - evt: FillRectangleEvent, + enabledElement: Types.IEnabledElement, operationData: OperationData, constraintFn?: any, inside = true ): void { - const { enabledElement } = evt - const { renderingEngine, viewport } = enabledElement const { - volume: labelmapVolume, + volume: segmentation, points, segmentsLocked, segmentIndex, + segmentationDataUID, + toolGroupUID, } = operationData - const { imageData, dimensions, scalarData } = labelmapVolume + const { imageData, dimensions, scalarData } = segmentation const rectangleCornersIJK = points.map((world) => { return imageData.worldToIndex(world) @@ -78,35 +76,35 @@ function fillRectangle( callback ) - triggerLabelmapRender(renderingEngine, labelmapVolume, imageData) + triggerSegmentationDataModified(toolGroupUID, segmentationDataUID) } /** - * Fill all pixels inside the region defined by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} + * Fill the inside of a rectangle + * @param {string} toolGroupUID - The unique identifier of the tool group. + * @param {OperationData} operationData - The data that will be used to create the + * new rectangle. + * @param {any} [constraintFn] */ export function fillInsideRectangle( - evt: FillRectangleEvent, + enabledElement: Types.IEnabledElement, operationData: OperationData, constraintFn?: any ): void { - fillRectangle(evt, operationData, constraintFn, true) + fillRectangle(enabledElement, operationData, constraintFn, true) } /** - * Fill all pixels outside the region defined by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} + * Fill the area outside of a rectangle for the toolGroupUID and segmentationDataUID. + * @param {string} toolGroupUID - The unique identifier of the tool group. + * @param {OperationData} operationData - The data that will be used to create the + * new rectangle. + * @param {any} [constraintFn] */ export function fillOutsideRectangle( - evt: FillRectangleEvent, + enabledElement: Types.IEnabledElement, operationData: OperationData, constraintFn?: any ): void { - fillRectangle(evt, operationData, constraintFn, false) + fillRectangle(enabledElement, operationData, constraintFn, false) } diff --git a/packages/cornerstone-tools/src/tools/segmentation/strategies/fillSphere.ts b/packages/cornerstone-tools/src/tools/segmentation/strategies/fillSphere.ts index 894de5e3c..33fffedac 100644 --- a/packages/cornerstone-tools/src/tools/segmentation/strategies/fillSphere.ts +++ b/packages/cornerstone-tools/src/tools/segmentation/strategies/fillSphere.ts @@ -1,42 +1,40 @@ import { Point3, - Point2, IImageVolume, IEnabledElement, } from '@precisionmetrics/cornerstone-render/src/types' -import triggerLabelmapRender from '../../../util/segmentation/triggerLabelmapRender' +import { triggerSegmentationDataModified } from '../../../store/SegmentationModule/triggerSegmentationEvents' import pointInSurroundingSphereCallback from '../../../util/planar/pointInSurroundingSphereCallback' type OperationData = { points: [Point3, Point3, Point3, Point3] volume: IImageVolume + toolGroupUID: string segmentIndex: number + segmentationDataUID: string segmentsLocked: number[] viewPlaneNormal: Point3 viewUp: Point3 constraintFn: () => boolean } -type fillSphereEvent = { - enabledElement: IEnabledElement -} - function fillSphere( - evt: fillSphereEvent, + enabledElement: IEnabledElement, operationData: OperationData, _inside = true ): void { - const { enabledElement } = evt - const { renderingEngine, viewport } = enabledElement + const { viewport } = enabledElement const { - volume: labelmapVolume, + volume: segmentation, segmentsLocked, segmentIndex, + toolGroupUID, + segmentationDataUID, points, } = operationData - const { scalarData, imageData } = labelmapVolume + const { scalarData } = segmentation const callback = ({ index, value }) => { if (segmentsLocked.includes(value)) { @@ -47,38 +45,38 @@ function fillSphere( pointInSurroundingSphereCallback( viewport, - labelmapVolume, + segmentation, [points[0], points[1]], callback ) - triggerLabelmapRender(renderingEngine, labelmapVolume, imageData) + triggerSegmentationDataModified(toolGroupUID, segmentationDataUID) } /** - * Fill all pixels inside/outside the region defined by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} + * Fill inside a sphere with the given segment index in the given operation data. The + * operation data contains the sphere required points. + * @param {IEnabledElement} enabledElement - The element that is enabled and + * selected. + * @param {OperationData} operationData - OperationData */ export function fillInsideSphere( - evt: fillSphereEvent, + enabledElement: IEnabledElement, operationData: OperationData ): void { - fillSphere(evt, operationData, true) + fillSphere(enabledElement, operationData, true) } /** - * Fill all pixels outside the region defined by the rectangle. - * @param {} evt The Cornerstone event. - * @param {} operationData An object containing the `pixelData` to - * modify, the `segmentIndex` and the `points` array. - * @returns {null} + * Fill outside a sphere with the given segment index in the given operation data. The + * operation data contains the sphere required points. + * @param {IEnabledElement} enabledElement - The element that is enabled and + * selected. + * @param {OperationData} operationData - OperationData */ export function fillOutsideSphere( - evt: fillSphereEvent, + enabledElement: IEnabledElement, operationData: OperationData ): void { - fillSphere(evt, operationData, false) + fillSphere(enabledElement, operationData, false) } diff --git a/packages/cornerstone-tools/src/types/ISetToolModeOptions.ts b/packages/cornerstone-tools/src/types/ISetToolModeOptions.ts index 14981757c..f62ec5fc2 100644 --- a/packages/cornerstone-tools/src/types/ISetToolModeOptions.ts +++ b/packages/cornerstone-tools/src/types/ISetToolModeOptions.ts @@ -10,8 +10,9 @@ type IToolBinding = { modifierKey?: ToolBindingKeyboardType } -export default interface ISetToolModeOptions { +interface ISetToolModeOptions { bindings: IToolBinding[] } export { IToolBinding } +export default ISetToolModeOptions diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/IToolGroup.ts b/packages/cornerstone-tools/src/types/IToolGroup.ts similarity index 86% rename from packages/cornerstone-tools/src/store/ToolGroupManager/IToolGroup.ts rename to packages/cornerstone-tools/src/types/IToolGroup.ts index b119b17ff..e7b47768d 100644 --- a/packages/cornerstone-tools/src/store/ToolGroupManager/IToolGroup.ts +++ b/packages/cornerstone-tools/src/types/IToolGroup.ts @@ -1,12 +1,11 @@ import { Types } from '@precisionmetrics/cornerstone-render' - -import ISetToolModeOptions from '../../types/ISetToolModeOptions' +import ISetToolModeOptions from './ISetToolModeOptions' export default interface IToolGroup { // Unserializable instantiated tool classes, keyed by name _toolInstances: Record - id: string - viewports: Array + uid: string + viewportsInfo: Array toolOptions: Record // getViewportUIDs: () => Array @@ -32,6 +31,9 @@ export default interface IToolGroup { setToolDisabled: { (toolName: string, toolModeOptions: ISetToolModeOptions): void } + getToolModeOptions: { + (toolName: string): ISetToolModeOptions + } isPrimaryButtonBinding: { (toolModeOptions: ISetToolModeOptions): boolean } diff --git a/packages/cornerstone-tools/src/types/SegmentationEventTypes.ts b/packages/cornerstone-tools/src/types/SegmentationEventTypes.ts new file mode 100644 index 000000000..730a8a4f4 --- /dev/null +++ b/packages/cornerstone-tools/src/types/SegmentationEventTypes.ts @@ -0,0 +1,10 @@ +import { Types } from '@precisionmetrics/cornerstone-render' + +export type SegmentationDataModifiedEvent = Types.CustomEventType<{ + toolGroupUID: string + segmentationDataUID: string +}> + +export type SegmentationStateModifiedEvent = Types.CustomEventType<{ + toolGroupUID: string +}> diff --git a/packages/cornerstone-tools/src/types/SegmentationRepresentationTypes.ts b/packages/cornerstone-tools/src/types/SegmentationRepresentationTypes.ts new file mode 100644 index 000000000..996bac2e7 --- /dev/null +++ b/packages/cornerstone-tools/src/types/SegmentationRepresentationTypes.ts @@ -0,0 +1,17 @@ +import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction' +import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction' +import Representations from '../enums/SegmentationRepresentations' + +export type LabelmapRepresentation = { + type: typeof Representations.Labelmap + config: { + cfun?: vtkColorTransferFunction + ofun?: vtkPiecewiseFunction + } +} + +/** + * Todo: Other representations + */ + +export type SegmentationRepresentation = LabelmapRepresentation diff --git a/packages/cornerstone-tools/src/types/SegmentationStateTypes.ts b/packages/cornerstone-tools/src/types/SegmentationStateTypes.ts new file mode 100644 index 000000000..b728b3db0 --- /dev/null +++ b/packages/cornerstone-tools/src/types/SegmentationStateTypes.ts @@ -0,0 +1,175 @@ +import { SegmentationRepresentation } from './SegmentationRepresentationTypes' + +import type { LabelmapConfig } from '../tools/displayTools/Labelmap/LabelmapConfig' +/** + * Color LUT State + */ + +// RGBA as 0-255 +export type Color = [number, number, number, number] +// [[0,0,0,0], [200,200,200,200], ....] +export type ColorLUT = Array + +export type RepresentationConfig = LabelmapConfig +/** + * Configurations + */ +export type SegmentationConfig = { + renderInactiveSegmentations: boolean + representations: { + LABELMAP?: LabelmapConfig + // Todo: Mesh: MeshConfig + } +} + +/** + * Global State + */ +export type GlobalSegmentationData = { + volumeUID: string + label: string + referenceVolumeUID?: string + referenceImageId?: string + activeSegmentIndex: number + segmentsLocked: Set + cachedStats: { [key: string]: number } +} + +export type GlobalSegmentationState = GlobalSegmentationData[] + +export type GlobalSegmentationStateWithConfig = { + segmentations: GlobalSegmentationState + config: SegmentationConfig +} + +/** + * ToolGroup Specific State + */ + +export type ToolGroupSpecificSegmentationData = { + volumeUID: string + // unique id for this segmentationData in this viewport which will be `{volumeUID}-{representationType}` + // Todo: Do we need to have Global segmentationData UID? I don't think so. We only need per-viewport + segmentationDataUID: string + active: boolean + segmentsHidden: Set + visibility: boolean + colorLUTIndex: number + representation: SegmentationRepresentation +} + +export type ToolGroupSpecificSegmentationState = + ToolGroupSpecificSegmentationData[] + +export type ToolGroupSpecificSegmentationStateWithConfig = { + segmentations: ToolGroupSpecificSegmentationState + config: SegmentationConfig +} + +/** + * Segmentation State + * + * @example + * ``` + * { + * colorLUT: [], + * global: { + * segmentations: [ + * { + * volumeUID: 'labelmapUID2', + * label: 'label1', + * referenceVolumeUID: 'referenceVolumeName', + * referenceImageId: 'referenceImageId', + * activeSegmentIndex: 1, + * segmentsLocked: Set(), + * cacheStats: {}, + * }, + * { + * volumeUID: 'labelmapUID2', + * label: 'label1', + * referenceVolumeUID: 'referenceVolumeName', + * referenceImageId: 'referenceImageId', + * activeSegmentIndex: 1, + * segmentsLocked: Set(), + * cacheStats: {}, + * }, + * ], + * config: { + * renderInactiveSegmentations: true, + * representations:{ + * LABELMAP: { + * renderOutline: true, + * outlineWidth: 3, + * outlineWidthActive: 3, + * outlineWidthInactive: 2, + * renderFill: true, + * fillAlpha: 0.9, + * fillAlphaInactive: 0.85, + * } + * } + * } + * } + * }, + * toolGroups: { + * toolGroupUID1: { + * segmentations: [ + * { + * volumeUID: 'labelmapUID1', + * segmentationDataUID: "123123" + * active: true, + * colorLUTIndex: 0, + * visibility: true, + * segmentsHidden: Set(), + * representation: { + * type: "labelmap" + * config: { + * cfun: cfun, + * ofun: ofun, + * }, + * } + * }, + * { + * volumeUID: 'labelmapUID1', + * segmentationDataUID: "5987123" + * colorLUTIndex: 1, + * visibility: true, + * segmentsHidden: Set(), + * representation: { + * type: "labelmap" + * config: { + * cfun: cfun, + * ofun: ofun, + * }, + * } + * }, + * ], + * ], + * config: { + * renderInactiveSegmentations: true, + * representations:{ + * LABELMAP: { + * renderOutline: false, + * } + * } + * } + * }, + * toolGroup2: { + * // + * } + * }, + * }, + * } + */ +export interface SegmentationState { + colorLutTables: ColorLUT[] + global: GlobalSegmentationStateWithConfig + toolGroups: { + [key: string]: ToolGroupSpecificSegmentationStateWithConfig + } +} + +// It is partial of ToolGroupSpecificSegmentationData BUT REQUIRES volumeUID +export type SegmentationDataInput = + Partial & { + toolGroupUID: string + } diff --git a/packages/cornerstone-tools/src/types/index.ts b/packages/cornerstone-tools/src/types/index.ts index e13a9d727..9632a2755 100644 --- a/packages/cornerstone-tools/src/types/index.ts +++ b/packages/cornerstone-tools/src/types/index.ts @@ -15,7 +15,28 @@ import type PlanarBoundingBox from './PlanarBoundingBox' import type Point2 from './Point2' import type Point3 from './Point3' import type { IToolBinding } from './ISetToolModeOptions' -import type IToolGroup from '../store/ToolGroupManager/IToolGroup' +import type ISetToolModeOptions from './ISetToolModeOptions' +import type IToolGroup from './IToolGroup' +import type { + SegmentationRepresentation, + LabelmapRepresentation, +} from './SegmentationRepresentationTypes' +import type { + Color, + ColorLUT, + RepresentationConfig, + SegmentationConfig, + GlobalSegmentationData, + GlobalSegmentationState, + GlobalSegmentationStateWithConfig, + ToolGroupSpecificSegmentationData, + ToolGroupSpecificSegmentationStateWithConfig, + ToolGroupSpecificSegmentationState, +} from './SegmentationStateTypes' +import type { + SegmentationDataModifiedEvent, + SegmentationStateModifiedEvent, +} from './SegmentationEventTypes' export type { // ToolState @@ -35,6 +56,22 @@ export type { IPoints, // ToolBindings IToolBinding, + ISetToolModeOptions, // IToolGroup, + // Segmentation + SegmentationRepresentation, + LabelmapRepresentation, + Color, + ColorLUT, + RepresentationConfig, + SegmentationConfig, + GlobalSegmentationData, + GlobalSegmentationState, + GlobalSegmentationStateWithConfig, + ToolGroupSpecificSegmentationData, + ToolGroupSpecificSegmentationStateWithConfig, + ToolGroupSpecificSegmentationState, + SegmentationDataModifiedEvent, + SegmentationStateModifiedEvent, } diff --git a/packages/cornerstone-tools/src/util/getToolDataStyle.ts b/packages/cornerstone-tools/src/util/getToolDataStyle.ts index 29638f619..0a8dca384 100644 --- a/packages/cornerstone-tools/src/util/getToolDataStyle.ts +++ b/packages/cornerstone-tools/src/util/getToolDataStyle.ts @@ -1,7 +1,7 @@ import { ToolDataStates } from '../enums' import { ToolSpecificToolData } from '../types' -import { isToolDataLocked } from '../stateManagement/toolDataLocking' -import { isToolDataSelected } from '../stateManagement/toolDataSelection' +import { isToolDataLocked } from '../stateManagement/annotation/toolDataLocking' +import { isToolDataSelected } from '../stateManagement/annotation/toolDataSelection' type ToolData = { active?: boolean @@ -16,5 +16,6 @@ export default function getToolDataStyle( if (isToolDataSelected(toolData)) return ToolDataStates.Selected if (isToolDataLocked(toolData)) return ToolDataStates.Locked } + return ToolDataStates.Default } diff --git a/packages/cornerstone-tools/src/util/getToolsWithModesForElement.ts b/packages/cornerstone-tools/src/util/getToolsWithModesForElement.ts index a4d4d075b..c61788a11 100644 --- a/packages/cornerstone-tools/src/util/getToolsWithModesForElement.ts +++ b/packages/cornerstone-tools/src/util/getToolsWithModesForElement.ts @@ -20,25 +20,31 @@ export default function getToolsWithModesForElement( const enabledElement = getEnabledElement(element) const { renderingEngineUID, viewportUID } = enabledElement - const toolGroups = ToolGroupManager.getToolGroups( + const toolGroup = ToolGroupManager.getToolGroup( renderingEngineUID, viewportUID ) + if (!toolGroup) { + return [] + } + const enabledTools = [] - for (let i = 0; i < toolGroups.length; i++) { - const toolGroup = toolGroups[i] - const toolGroupToolNames = Object.keys(toolGroup.toolOptions) + const toolGroupToolNames = Object.keys(toolGroup.toolOptions) - for (let j = 0; j < toolGroupToolNames.length; j++) { - const toolName = toolGroupToolNames[j] - const tool = toolGroup.toolOptions[toolName] + for (let j = 0; j < toolGroupToolNames.length; j++) { + const toolName = toolGroupToolNames[j] + const toolOptions = toolGroup.toolOptions[toolName] + + /* filter out tools that don't have options */ + if (!toolOptions) { + continue + } - if (modesFilter.includes(tool.mode)) { - const toolInstance = toolGroup._toolInstances[toolName] - enabledTools.push(toolInstance) - } + if (modesFilter.includes(toolOptions.mode)) { + const toolInstance = toolGroup.getToolInstance(toolName) + enabledTools.push(toolInstance) } } diff --git a/packages/cornerstone-tools/src/util/segmentation/getDefaultRepresentationConfig.ts b/packages/cornerstone-tools/src/util/segmentation/getDefaultRepresentationConfig.ts new file mode 100644 index 000000000..16351e6e2 --- /dev/null +++ b/packages/cornerstone-tools/src/util/segmentation/getDefaultRepresentationConfig.ts @@ -0,0 +1,13 @@ +import getDefaultLabelmapConfig from '../../tools/displayTools/Labelmap/LabelmapConfig' +import SegmentationRepresentation from '../../enums/SegmentationRepresentations' + +export default function getDefaultRepresentationConfig( + representationType: string +) { + switch (representationType) { + case SegmentationRepresentation.Labelmap: + return getDefaultLabelmapConfig() + default: + throw new Error(`Unknown representation type: ${representationType}`) + } +} diff --git a/packages/cornerstone-tools/src/util/segmentation/index.ts b/packages/cornerstone-tools/src/util/segmentation/index.ts index b141b1dfc..d4c06a7f6 100644 --- a/packages/cornerstone-tools/src/util/segmentation/index.ts +++ b/packages/cornerstone-tools/src/util/segmentation/index.ts @@ -5,10 +5,11 @@ import { } from './getBoundingBoxUtils' import thresholdVolumeByRange from './thresholdVolumeByRange' import thresholdVolumeByRoiStats from './thresholdVolumeByRoiStats' -import triggerLabelmapRender from './triggerLabelmapRender' import calculateSuvPeak from './calculateSuvPeak' import calculateTMTV from './calculateTMTV' import createMergedLabelmap from './createMergedLabelmap' +import isValidRepresentationConfig from './isValidRepresentationConfig' +import getDefaultRepresentationConfig from './getDefaultRepresentationConfig' export { getBoundingBoxAroundShape, @@ -16,10 +17,11 @@ export { // fillOutsideBoundingBox, thresholdVolumeByRange, thresholdVolumeByRoiStats, - triggerLabelmapRender, calculateSuvPeak, calculateTMTV, createMergedLabelmap, + isValidRepresentationConfig, + getDefaultRepresentationConfig, } export default { @@ -28,8 +30,9 @@ export default { // fillOutsideBoundingBox, thresholdVolumeByRange, thresholdVolumeByRoiStats, - triggerLabelmapRender, calculateSuvPeak, calculateTMTV, createMergedLabelmap, + isValidRepresentationConfig, + getDefaultRepresentationConfig, } diff --git a/packages/cornerstone-tools/src/util/segmentation/isValidRepresentationConfig.ts b/packages/cornerstone-tools/src/util/segmentation/isValidRepresentationConfig.ts new file mode 100644 index 000000000..cf5a16ccb --- /dev/null +++ b/packages/cornerstone-tools/src/util/segmentation/isValidRepresentationConfig.ts @@ -0,0 +1,15 @@ +import { isValidLabelmapConfig } from '../../tools/displayTools/Labelmap/LabelmapConfig' +import SegmentationRepresentation from '../../enums/SegmentationRepresentations' +import { RepresentationConfig } from '../../types/SegmentationStateTypes' + +export default function isValidRepresentationConfig( + representationType: string, + config: RepresentationConfig +): boolean { + switch (representationType) { + case SegmentationRepresentation.Labelmap: + return isValidLabelmapConfig(config) + default: + throw new Error(`Unknown representation type: ${representationType}`) + } +} diff --git a/packages/cornerstone-tools/src/util/segmentation/thresholdVolumeByRange.ts b/packages/cornerstone-tools/src/util/segmentation/thresholdVolumeByRange.ts index c76efc9e3..97d7dc314 100644 --- a/packages/cornerstone-tools/src/util/segmentation/thresholdVolumeByRange.ts +++ b/packages/cornerstone-tools/src/util/segmentation/thresholdVolumeByRange.ts @@ -6,12 +6,16 @@ import { Point2, } from '@precisionmetrics/cornerstone-render/src/types' +import { cache } from '@precisionmetrics/cornerstone-render' + import { getBoundingBoxAroundShape, extend2DBoundingBoxInViewAxis, } from '../segmentation' import pointInShapeCallback from '../../util/planar/pointInShapeCallback' -import triggerLabelmapRender from './triggerLabelmapRender' +import { triggerSegmentationDataModified } from '../../store/SegmentationModule/triggerSegmentationEvents' +import { ToolGroupSpecificSegmentationData } from '../../types/SegmentationStateTypes' +import * as SegmentationState from '../../stateManagement/segmentation/segmentationState' export type ThresholdRangeOptions = { higherThreshold: number @@ -35,49 +39,55 @@ export type ToolDataForThresholding = { } /** - * Given an array of rectangle toolData, and a labelmap and referenceVolumes: - * It fills the labelmap at SegmentIndex=1 based on a range of thresholds of the referenceVolumes + * Given an array of rectangle toolData, and a segmentation and referenceVolumes: + * It fills the segmentation at SegmentIndex=1 based on a range of thresholds of the referenceVolumes * inside the drawn annotations. - * @param {RectangleRoiThresholdToolData[]} toolDataList Array of rectangle annotaiton toolData - * @param {IImageVolume[]} referenceVolumes array of volumes on whom thresholding is applied - * @param {IImageVolume} labelmap segmentation volume + * @param {string} toolGroupUID - The toolGroupUID of the tool that is performing the operation + * @param {RectangleRoiThresholdToolData[]} toolDataList Array of rectangle annotation toolData + * @param {ToolGroupSpecificSegmentationData} segmentationData - The segmentation data to be modified + * @param {IImageVolume} segmentation segmentation volume * @param {ThresholdRangeOptions} options Options for thresholding */ function thresholdVolumeByRange( + toolGroupUID: string, toolDataList: ToolDataForThresholding[], referenceVolumes: IImageVolume[], - labelmap: IImageVolume, + segmentationData: ToolGroupSpecificSegmentationData, options: ThresholdRangeOptions ): IImageVolume { if (referenceVolumes.length > 1) { throw new Error('thresholding more than one volumes is not supported yet') } - if (!labelmap) { - throw new Error('labelmap is required') + const globalState = SegmentationState.getGlobalSegmentationDataByUID( + segmentationData.volumeUID + ) + + if (!globalState) { + throw new Error('No Segmentation Found') } - const { scalarData, imageData: labelmapImageData } = labelmap + const { volumeUID } = globalState + const segmentation = cache.getVolume(volumeUID) + + const { segmentationDataUID } = segmentationData + + const { scalarData, imageData: segmentationImageData } = segmentation const { lowerThreshold, higherThreshold, numSlicesToProject, overwrite } = options - // set the labelmap to all zeros + // set the segmentation to all zeros if (overwrite) { for (let i = 0; i < scalarData.length; i++) { scalarData[i] = 0 } } - let renderingEngine - toolDataList.forEach((toolData) => { // Threshold Options - const { enabledElement } = toolData.metadata const { data } = toolData const { points } = data.handles - ;({ renderingEngine } = enabledElement) - const referenceVolume = referenceVolumes[0] const { imageData, dimensions } = referenceVolume @@ -96,7 +106,7 @@ function thresholdVolumeByRange( ) let boundsIJK = getBoundingBoxAroundShape(rectangleCornersIJK, dimensions) - // If the tool is 2D but it is configed to project to X amount of slices + // If the tool is 2D but it is configured to project to X amount of slices // Don't project the slices if projectionPoints have been used to define the extents if (numSlicesToProject && !data.cachedStats?.projectionPoints) { boundsIJK = extendBoundingBoxInSliceAxisIfNecessary( @@ -118,15 +128,16 @@ function thresholdVolumeByRange( pointInShapeCallback( boundsIJK, scalarData, - labelmapImageData, + segmentationImageData, dimensions, () => true, callback ) }) - triggerLabelmapRender(renderingEngine, labelmap, labelmapImageData) - return labelmap + triggerSegmentationDataModified(toolGroupUID, segmentationDataUID) + + return segmentation } export function extendBoundingBoxInSliceAxisIfNecessary( diff --git a/packages/cornerstone-tools/src/util/segmentation/thresholdVolumeByRoiStats.ts b/packages/cornerstone-tools/src/util/segmentation/thresholdVolumeByRoiStats.ts index fa2e542b0..5a51f7283 100644 --- a/packages/cornerstone-tools/src/util/segmentation/thresholdVolumeByRoiStats.ts +++ b/packages/cornerstone-tools/src/util/segmentation/thresholdVolumeByRoiStats.ts @@ -2,11 +2,13 @@ import { vec3 } from 'gl-matrix' import { IImageVolume } from '@precisionmetrics/cornerstone-render/src/types' import { getBoundingBoxAroundShape } from '../segmentation' -import { Point3 } from '../../types' +import { Point3, ToolGroupSpecificSegmentationData } from '../../types' import thresholdVolumeByRange, { ToolDataForThresholding, extendBoundingBoxInSliceAxisIfNecessary, } from './thresholdVolumeByRange' +import * as SegmentationState from '../../stateManagement/segmentation/segmentationState' +import { cache } from '@precisionmetrics/cornerstone-render' export type ThresholdRoiStatsOptions = { statistic: 'max' | 'min' @@ -22,28 +24,37 @@ export type ThresholdRoiStatsOptions = { * it thresholds the referenceVolumes based on a weighted value of the statistic. * For instance in radiation oncology, usually 41% of the maximum of the ROI is used * in radiation planning. - * @param {RectangleRoiThresholdToolData[]} toolDataList Array of rectangle annotaiton toolData - * @param {IImageVolume[]} referenceVolumes array of volumes on whom thresholding is applied + * @param {string} toolGroupUID - The toolGroupUID of the tool that is performing the operation + * @param {RectangleRoiThresholdToolData[]} toolDataList Array of rectangle annotation toolData + * @param {ToolGroupSpecificSegmentationData} segmentationData - The segmentation data to be modified * @param {IImageVolume} labelmap segmentation volume * @param {ThresholdRoiStatsOptions} options Options for thresholding */ function thresholdVolumeByRoiStats( + toolGroupUID: string, toolDataList: ToolDataForThresholding[], referenceVolumes: IImageVolume[], - labelmap: IImageVolume, + segmentationData: ToolGroupSpecificSegmentationData, options: ThresholdRoiStatsOptions -): IImageVolume { +): void { if (referenceVolumes.length > 1) { throw new Error('thresholding more than one volumes is not supported yet') } - if (!labelmap) { - throw new Error('labelmap is required') + const globalState = SegmentationState.getGlobalSegmentationDataByUID( + segmentationData.volumeUID + ) + + if (!globalState) { + throw new Error('No Segmentation Found') } + const { volumeUID } = globalState + const segmentation = cache.getVolume(volumeUID) + const { numSlicesToProject, overwrite } = options - const { scalarData } = labelmap + const { scalarData } = segmentation if (overwrite) { for (let i = 0; i < scalarData.length; i++) { scalarData[i] = 0 @@ -103,8 +114,13 @@ function thresholdVolumeByRoiStats( } // Run threshold volume by the new range - thresholdVolumeByRange(toolDataList, referenceVolumes, labelmap, rangeOptions) - return labelmap + thresholdVolumeByRange( + toolGroupUID, + toolDataList, + referenceVolumes, + segmentationData, + rangeOptions + ) } function _worldToIndex(imageData, ain) { diff --git a/packages/cornerstone-tools/src/util/segmentation/triggerLabelmapRender.ts b/packages/cornerstone-tools/src/util/segmentation/triggerLabelmapRender.ts deleted file mode 100644 index 097ab6bc2..000000000 --- a/packages/cornerstone-tools/src/util/segmentation/triggerLabelmapRender.ts +++ /dev/null @@ -1,36 +0,0 @@ -import RenderingEngine from '@precisionmetrics/cornerstone-render/src/RenderingEngine' -import { IImageVolume } from '@precisionmetrics/cornerstone-render/src/types' -import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData' - -/** - * - * @param {RenderingEngine} renderingEngine renderingEngine - * @param {IImageVolume} labelmap labelmapVolume - * @param {vtkImageData} imageData labelmap imageData - * @param {number} modifiedSlices modified slices as an array - */ -export default function triggerLabelmapRender( - renderingEngine: RenderingEngine, - labelmap: IImageVolume, - imageData: vtkImageData, - modifiedSlices?: number -): void { - let modifiedSlicesToUse - - if (!modifiedSlices) { - // Use all slices of the image to update the texture - const numSlices = imageData.getDimensions()[2] - modifiedSlicesToUse = [...Array(numSlices).keys()] - } - - // todo: this renders all viewports, only renders viewports that have the modified labelmap actor - // right now this is needed to update the labelmap on other viewports that have it (pt) - modifiedSlicesToUse.forEach((i) => { - labelmap.vtkOpenGLTexture.setUpdatedFrame(i) - }) - - // Todo: seems like we don't need to set the data back again - // vtkImageData.getPointData().getScalars().setData(scalarData) - imageData.modified() - renderingEngine.render() -} diff --git a/packages/cornerstone-tools/src/util/stackScrollTool/scrollThroughStack.ts b/packages/cornerstone-tools/src/util/stackScrollTool/scrollThroughStack.ts index 2462f2178..ddfadc428 100644 --- a/packages/cornerstone-tools/src/util/stackScrollTool/scrollThroughStack.ts +++ b/packages/cornerstone-tools/src/util/stackScrollTool/scrollThroughStack.ts @@ -54,7 +54,13 @@ export default function scrollThroughStack( return } - const { volumeActor } = viewport.getActor(imageVolume.uid) + const actor = viewport.getActor(imageVolume.uid) + + if (!actor) { + console.warn('No actor found for with actorUID of', imageVolume.uid) + } + + const { volumeActor } = actor const scrollRange = getSliceRange(volumeActor, viewPlaneNormal, focalPoint) const delta = invert ? -deltaFrames : deltaFrames diff --git a/packages/cornerstone-tools/src/util/triggerAnnotationRender.ts b/packages/cornerstone-tools/src/util/triggerAnnotationRender.ts index 33970004e..7f6991171 100644 --- a/packages/cornerstone-tools/src/util/triggerAnnotationRender.ts +++ b/packages/cornerstone-tools/src/util/triggerAnnotationRender.ts @@ -6,7 +6,7 @@ import { import { CornerstoneTools3DEvents as EVENTS, ToolModes } from '../enums' import { draw as drawSvg } from '../drawingSvg' import getToolsWithModesForElement from './getToolsWithModesForElement' -import { getToolState } from '../stateManagement' +import SegmentationDisplayTool from '../tools/displayTools/SegmentationDisplayTool' const { Active, Passive, Enabled } = ToolModes @@ -95,8 +95,11 @@ class AnnotationRenderingEngine { drawSvg(element, (svgDrawingHelper) => { const handleDrawSvg = (tool) => { - // Are there situations where that would be bad (Canvas Overlay Tool?) - if (tool.renderToolData) { + // Todo: we should not have the need to check tool if it is instance + // of SegmentationDisplayTool, but right now SegmentationScissors + // are instance of BaseTool and we cannot simply check if tool is + // instance of BaseAnnotationTool + if (!(tool instanceof SegmentationDisplayTool) && tool.renderToolData) { tool.renderToolData({ detail: eventData }, svgDrawingHelper) triggerEvent(element, EVENTS.ANNOTATION_RENDERED, { ...eventData }) } diff --git a/packages/cornerstone-tools/src/util/triggerSegmentationRender.ts b/packages/cornerstone-tools/src/util/triggerSegmentationRender.ts new file mode 100644 index 000000000..f97b18229 --- /dev/null +++ b/packages/cornerstone-tools/src/util/triggerSegmentationRender.ts @@ -0,0 +1,173 @@ +import { + triggerEvent, + eventTarget, + getRenderingEngine, + EVENTS as csRenderEvents, +} from '@precisionmetrics/cornerstone-render' +import { CornerstoneTools3DEvents as csToolsEvents } from '../enums' +import { + getToolGroupByToolGroupUID, + getToolGroup, +} from '../store/ToolGroupManager' + +import SegmentationDisplayTool from '../tools/displayTools/SegmentationDisplayTool' + +class SegmentationRenderingEngine { + private _needsRender: Set = new Set() + private _animationFrameSet = false + private _animationFrameHandle: number | null = null + public hasBeenDestroyed: boolean + + private _setToolGroupSegmentationToBeRenderedNextFrame( + toolGroupUIDs: string[] + ) { + // Add the viewports to the set of flagged viewports + toolGroupUIDs.forEach((toolGroupUID) => { + this._needsRender.add(toolGroupUID) + }) + + // Render any flagged viewports + this._render() + } + + /** + * @method _render Sets up animation frame if necessary + */ + private _render() { + // If we have viewports that need rendering and we have not already + // set the RAF callback to run on the next frame. + if (this._needsRender.size > 0 && this._animationFrameSet === false) { + this._animationFrameHandle = window.requestAnimationFrame( + this._renderFlaggedToolGroups + ) + + // Set the flag that we have already set up the next RAF call. + this._animationFrameSet = true + } + } + + /** + * @method _throwIfDestroyed Throws an error if trying to interact with the `RenderingEngine` + * instance after its `destroy` method has been called. + */ + private _throwIfDestroyed() { + if (this.hasBeenDestroyed) { + throw new Error( + 'this.destroy() has been manually called to free up memory, can not longer use this instance. Instead make a new one.' + ) + } + } + + _triggerRender(toolGroupUID) { + const toolGroup = getToolGroupByToolGroupUID(toolGroupUID) + + if (!toolGroup) { + console.warn(`No tool group found with toolGroupUID: ${toolGroupUID}`) + return + } + + const { viewportsInfo } = toolGroup + const viewports = [] + + viewportsInfo.forEach(({ viewportUID, renderingEngineUID }) => { + const renderingEngine = getRenderingEngine(renderingEngineUID) + + if (!renderingEngine) { + console.warn('rendering Engine has been destroyed') + return + } + + viewports.push(renderingEngine.getViewport(viewportUID)) + }) + + const segmentationDisplayToolInstance = toolGroup.getToolInstance( + 'SegmentationDisplay' + ) as SegmentationDisplayTool + + function onSegmentationRender(evt) { + const { element, viewportUID, renderingEngineUID } = evt.detail + + element.removeEventListener( + csRenderEvents.IMAGE_RENDERED, + onSegmentationRender + ) + + const toolGroup = getToolGroup(renderingEngineUID, viewportUID) + + const eventData = { + toolGroupUID: toolGroup.uid, + viewportUID, + } + + triggerEvent(eventTarget, csToolsEvents.SEGMENTATION_RENDERED, { + ...eventData, + }) + } + + // Todo: for other representations we probably need the drawSVG, but right now we are not using it + // drawSvg(element, (svgDrawingHelper) => { + // const handleDrawSvg = (tool) => { + // if (tool instanceof SegmentationDisplayTool && tool.renderToolData) { + // tool.renderToolData({ detail: eventData }) + // triggerEvent(element, csToolsEvents.SEGMENTATION_RENDERED, { ...eventData }) + // } + // } + // enabledTools.forEach(handleDrawSvg) + // }) + + viewports.forEach(({ element }) => { + element.addEventListener( + csRenderEvents.IMAGE_RENDERED, + onSegmentationRender + ) + }) + + segmentationDisplayToolInstance.renderToolData({ toolGroupUID }) + } + + private _renderFlaggedToolGroups = () => { + this._throwIfDestroyed() + + // for each toolGroupUID insides the _needsRender set, render the segmentation + const toolGroupUIDs = Array.from(this._needsRender.values()) + + for (const toolGroupUID of toolGroupUIDs) { + this._triggerRender(toolGroupUID) + + // This viewport has been rendered, we can remove it from the set + this._needsRender.delete(toolGroupUID) + + // If there is nothing left that is flagged for rendering, stop here + // and allow RAF to be called again + if (this._needsRender.size === 0) { + this._animationFrameSet = false + this._animationFrameHandle = null + return + } + } + } + + public renderToolGroupSegmentations(toolGroupUID): void { + this._setToolGroupSegmentationToBeRenderedNextFrame([toolGroupUID]) + } + + /** + * @method _reset Resets the `RenderingEngine` + */ + private _reset() { + window.cancelAnimationFrame(this._animationFrameHandle) + + this._needsRender.clear() + this._animationFrameSet = false + this._animationFrameHandle = null + } +} + +const segmentationRenderingEngine = new SegmentationRenderingEngine() + +export function triggerSegmentationRender(toolGroupUID: string): void { + segmentationRenderingEngine.renderToolGroupSegmentations(toolGroupUID) +} + +export { segmentationRenderingEngine } +export default triggerSegmentationRender diff --git a/packages/cornerstone-tools/src/util/viewportFilters/filterViewportsWithToolEnabled.ts b/packages/cornerstone-tools/src/util/viewportFilters/filterViewportsWithToolEnabled.ts index db19952b0..b8d644a14 100644 --- a/packages/cornerstone-tools/src/util/viewportFilters/filterViewportsWithToolEnabled.ts +++ b/packages/cornerstone-tools/src/util/viewportFilters/filterViewportsWithToolEnabled.ts @@ -20,14 +20,12 @@ export default function filterViewportsWithToolEnabled(viewports, toolName) { for (let vp = 0; vp < numViewports; vp++) { const viewport = viewports[vp] - const toolGroups = ToolGroupManager.getToolGroups( + const toolGroup = ToolGroupManager.getToolGroup( viewport.renderingEngineUID, viewport.uid ) - const hasTool = toolGroups.some((tg) => - _toolGroupHasActiveEnabledOrPassiveTool(tg, toolName) - ) + const hasTool = _toolGroupHasActiveEnabledOrPassiveTool(toolGroup, toolName) if (hasTool) { viewportsWithToolEnabled.push(viewport) diff --git a/packages/cornerstone-tools/src/util/viewportFilters/getViewportUIDsWithLabelmapToRender.ts b/packages/cornerstone-tools/src/util/viewportFilters/getViewportUIDsWithLabelmapToRender.ts deleted file mode 100644 index b98c5335a..000000000 --- a/packages/cornerstone-tools/src/util/viewportFilters/getViewportUIDsWithLabelmapToRender.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - getEnabledElement, - VolumeViewport, - StackViewport, -} from '@precisionmetrics/cornerstone-render' -import filterViewportsWithFrameOfReferenceUID from './filterViewportsWithFrameOfReferenceUID' -import filterViewportsWithToolEnabled from './filterViewportsWithToolEnabled' -import filterViewportsWithSameOrientation from './filterViewportsWithSameOrientation' - -/** - * @function getViewportUIDsWithToolToRender given a cornerstone3D enabled `element`, - * and a `toolName`, find all viewportUIDs looking at the same Frame Of Reference that have - * the tool with the given `toolName` active, passive or enabled. - * - * @param {HTMLElement} element The target cornerstone3D enabled element. - * @param {string} toolName The string toolName. - * @param {boolean=true} requireSameOrientation If true, only return viewports matching the orientation of the original viewport - * - * @returns {string[]} An array of viewportUIDs. - */ -export default function getViewportUIDsWithToolToRender( - element: HTMLElement, - toolName: string, - requireSameOrientation = true -): string[] { - const enabledElement = getEnabledElement(element) - const { renderingEngine, FrameOfReferenceUID } = enabledElement - - let viewports = renderingEngine.getViewports() - - viewports = filterViewportsWithFrameOfReferenceUID( - viewports, - FrameOfReferenceUID - ) - viewports = filterViewportsWithToolEnabled(viewports, toolName) - - const viewport = renderingEngine.getViewport(enabledElement.viewportUID) - - if (requireSameOrientation) { - viewports = filterViewportsWithSameOrientation( - viewports, - viewport.getCamera() - ) - } - - const viewportUIDs = viewports.map((vp) => vp.uid) - - return viewportUIDs -} diff --git a/packages/cornerstone-tools/src/util/viewportFilters/index.ts b/packages/cornerstone-tools/src/util/viewportFilters/index.ts index 46d0c586e..b213f5006 100644 --- a/packages/cornerstone-tools/src/util/viewportFilters/index.ts +++ b/packages/cornerstone-tools/src/util/viewportFilters/index.ts @@ -1,18 +1,15 @@ import filterViewportsWithFrameOfReferenceUID from './filterViewportsWithFrameOfReferenceUID' import filterViewportsWithToolEnabled from './filterViewportsWithToolEnabled' import getViewportUIDsWithToolToRender from './getViewportUIDsWithToolToRender' -import getViewportUIDsWithLabelmapToRender from './getViewportUIDsWithLabelmapToRender' export default { filterViewportsWithToolEnabled, filterViewportsWithFrameOfReferenceUID, getViewportUIDsWithToolToRender, - getViewportUIDsWithLabelmapToRender, } export { filterViewportsWithToolEnabled, filterViewportsWithFrameOfReferenceUID, getViewportUIDsWithToolToRender, - getViewportUIDsWithLabelmapToRender, } diff --git a/packages/cornerstone-tools/test/BidirectionalTool_test.js b/packages/cornerstone-tools/test/BidirectionalTool_test.js index 584077ae3..059694abf 100644 --- a/packages/cornerstone-tools/test/BidirectionalTool_test.js +++ b/packages/cornerstone-tools/test/BidirectionalTool_test.js @@ -106,7 +106,7 @@ describe('Cornerstone Tools: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/CrosshairsTool_test.js b/packages/cornerstone-tools/test/CrosshairsTool_test.js index 4ebe59a74..17ea0b9e8 100644 --- a/packages/cornerstone-tools/test/CrosshairsTool_test.js +++ b/packages/cornerstone-tools/test/CrosshairsTool_test.js @@ -119,7 +119,7 @@ describe('Cornerstone Tools: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('volume') + ToolGroupManager.destroyToolGroupByToolGroupUID('volume') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/EllipseROI_test.js b/packages/cornerstone-tools/test/EllipseROI_test.js index 2bf27720c..414069436 100644 --- a/packages/cornerstone-tools/test/EllipseROI_test.js +++ b/packages/cornerstone-tools/test/EllipseROI_test.js @@ -99,7 +99,7 @@ describe('Ellipse Tool: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { el.parentNode.removeChild(el) @@ -348,7 +348,7 @@ describe('Ellipse Tool: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/LengthTool_test.js b/packages/cornerstone-tools/test/LengthTool_test.js index f86722e56..ce5dd57ea 100644 --- a/packages/cornerstone-tools/test/LengthTool_test.js +++ b/packages/cornerstone-tools/test/LengthTool_test.js @@ -111,7 +111,7 @@ describe('LengthTool:', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { @@ -826,7 +826,7 @@ describe('LengthTool:', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { @@ -988,7 +988,7 @@ describe('LengthTool:', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/ProbeTool_test.js b/packages/cornerstone-tools/test/ProbeTool_test.js index 0334fb860..3d151b25c 100644 --- a/packages/cornerstone-tools/test/ProbeTool_test.js +++ b/packages/cornerstone-tools/test/ProbeTool_test.js @@ -97,7 +97,7 @@ describe('Probe Tool: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { @@ -708,7 +708,7 @@ describe('Probe Tool: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/RectangleROI_test.js b/packages/cornerstone-tools/test/RectangleROI_test.js index c6f8bdc2e..f68d2e3d8 100644 --- a/packages/cornerstone-tools/test/RectangleROI_test.js +++ b/packages/cornerstone-tools/test/RectangleROI_test.js @@ -97,7 +97,7 @@ describe('Rectangle Roi Tool: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { @@ -816,7 +816,7 @@ describe('Rectangle Roi Tool: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/StackScrollToolMouseWheelTool_test.js b/packages/cornerstone-tools/test/StackScrollToolMouseWheelTool_test.js index b6bf47e76..c17398dc5 100644 --- a/packages/cornerstone-tools/test/StackScrollToolMouseWheelTool_test.js +++ b/packages/cornerstone-tools/test/StackScrollToolMouseWheelTool_test.js @@ -88,7 +88,7 @@ describe('Cornerstone Tools Scroll Wheel: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('StackScroll') + ToolGroupManager.destroyToolGroupByToolGroupUID('StackScroll') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/ToolGroupManager_test.js b/packages/cornerstone-tools/test/ToolGroupManager_test.js index 03c3e965b..ff994a76f 100644 --- a/packages/cornerstone-tools/test/ToolGroupManager_test.js +++ b/packages/cornerstone-tools/test/ToolGroupManager_test.js @@ -109,12 +109,12 @@ describe('ToolGroup Manager: ', () => { this.toolGroup.addViewports(this.renderingEngine.uid, viewportUID1) - const tg = ToolGroupManager.getToolGroupById('volume1') + const tg = ToolGroupManager.getToolGroupByToolGroupUID('volume1') expect(tg).toBeDefined() }) }) - describe('Synchronizer Manager: ', () => { + describe('ToolGroup Manager: ', () => { beforeEach(function () { csTools3d.init() csTools3d.addTool(ProbeTool, {}) @@ -136,7 +136,7 @@ describe('ToolGroup Manager: ', () => { afterEach(function () { // Destroy synchronizer manager to test it first since csTools3D also destroy // synchronizers - ToolGroupManager.destroyToolGroupById('volume1') + ToolGroupManager.destroyToolGroupByToolGroupUID('volume1') csTools3d.destroy() cache.purgeCache() this.renderingEngine.destroy() @@ -175,16 +175,15 @@ describe('ToolGroup Manager: ', () => { this.toolGroup.addViewports(this.renderingEngine.uid, viewportUID1) - const tg = ToolGroupManager.getToolGroupById('volume1') + const tg = ToolGroupManager.getToolGroupByToolGroupUID('volume1') expect(tg).toBeDefined() - const tg2 = ToolGroupManager.getToolGroups( + const tg2 = ToolGroupManager.getToolGroup( renderingEngineUID, viewportUID1 ) expect(tg2).toBeDefined() - expect(tg2.length).toBe(1) - expect(tg).toBe(tg2[0]) + expect(tg).toBe(tg2) const tg3 = ToolGroupManager.createToolGroup('volume1') expect(tg3).toBeUndefined() @@ -221,23 +220,23 @@ describe('ToolGroup Manager: ', () => { ]) // Remove viewports - let tg = ToolGroupManager.getToolGroupById('volume1') + let tg = ToolGroupManager.getToolGroupByToolGroupUID('volume1') tg.addViewports(this.renderingEngine.uid, viewportUID1) - expect(tg.viewports.length).toBe(1) + expect(tg.viewportsInfo.length).toBe(1) tg.removeViewports(renderingEngineUID) - tg = ToolGroupManager.getToolGroupById('volume1') - expect(tg.viewports.length).toBe(0) + tg = ToolGroupManager.getToolGroupByToolGroupUID('volume1') + expect(tg.viewportsInfo.length).toBe(0) // tg.addViewports(this.renderingEngine.uid, viewportUID1) - tg = ToolGroupManager.getToolGroupById('volume1') - expect(tg.viewports.length).toBe(1) + tg = ToolGroupManager.getToolGroupByToolGroupUID('volume1') + expect(tg.viewportsInfo.length).toBe(1) tg.removeViewports(renderingEngineUID, viewportUID2) - expect(tg.viewports.length).toBe(1) + expect(tg.viewportsInfo.length).toBe(1) }) it('Should successfully make a tool enabled/disabled/active/passive', function () { @@ -267,12 +266,12 @@ describe('ToolGroup Manager: ', () => { this.toolGroup.addViewports(this.renderingEngine.uid, viewportUID1) // Remove viewports - let tg = ToolGroupManager.getToolGroupById('volume1') - expect(tg._toolInstances['Probe'].mode).toBe('Active') - expect(tg._toolInstances['Length']).toBeUndefined() + let tg = ToolGroupManager.getToolGroupByToolGroupUID('volume1') + expect(tg.getToolInstance('Probe').mode).toBe('Active') + expect(tg.getToolInstance('Length')).toBeUndefined() tg.setToolPassive('Probe') - expect(tg._toolInstances['Probe'].mode).toBe('Passive') + expect(tg.getToolInstance('Probe').mode).toBe('Passive') }) it('Should successfully setTool status', function () { @@ -302,21 +301,21 @@ describe('ToolGroup Manager: ', () => { this.toolGroup.addViewports(this.renderingEngine.uid, viewportUID1) // Remove viewports - let tg = ToolGroupManager.getToolGroupById('volume1') + let tg = ToolGroupManager.getToolGroupByToolGroupUID('volume1') tg.setToolActive() tg.setToolPassive() tg.setToolEnabled() tg.setToolDisabled() - expect(tg._toolInstances['Probe'].mode).toBe('Active') + expect(tg.getToolInstance('Probe').mode).toBe('Active') csTools3d.addTool(LengthTool, {}) tg.addTool('Length') tg.setToolEnabled('Length') - expect(tg._toolInstances['Length'].mode).toBe('Enabled') + expect(tg.getToolInstance('Length').mode).toBe('Enabled') tg.setToolDisabled('Length') - expect(tg._toolInstances['Length'].mode).toBe('Disabled') + expect(tg.getToolInstance('Length').mode).toBe('Disabled') }) }) }) diff --git a/packages/cornerstone-tools/test/cpu_BidirectionalTool_test.js b/packages/cornerstone-tools/test/cpu_BidirectionalTool_test.js index 966fbafe7..ea64133af 100644 --- a/packages/cornerstone-tools/test/cpu_BidirectionalTool_test.js +++ b/packages/cornerstone-tools/test/cpu_BidirectionalTool_test.js @@ -107,7 +107,7 @@ describe('Bidirectional Tool (CPU): ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/cpu_EllipseROI_test.js b/packages/cornerstone-tools/test/cpu_EllipseROI_test.js index 845c6134c..8319c09bb 100644 --- a/packages/cornerstone-tools/test/cpu_EllipseROI_test.js +++ b/packages/cornerstone-tools/test/cpu_EllipseROI_test.js @@ -104,7 +104,7 @@ describe('EllipticalRoiTool (CPU):', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/cpu_LengthTool_test.js b/packages/cornerstone-tools/test/cpu_LengthTool_test.js index 67a847fdd..d05fb1012 100644 --- a/packages/cornerstone-tools/test/cpu_LengthTool_test.js +++ b/packages/cornerstone-tools/test/cpu_LengthTool_test.js @@ -108,7 +108,7 @@ describe('Length Tool (CPU):', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/cpu_ProbeTool_test.js b/packages/cornerstone-tools/test/cpu_ProbeTool_test.js index 0b4effa3a..b6171b865 100644 --- a/packages/cornerstone-tools/test/cpu_ProbeTool_test.js +++ b/packages/cornerstone-tools/test/cpu_ProbeTool_test.js @@ -101,7 +101,7 @@ describe('ProbeTool (CPU):', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/cpu_RectangleROI_test.js b/packages/cornerstone-tools/test/cpu_RectangleROI_test.js index 195ff7e7d..f9aaf2a83 100644 --- a/packages/cornerstone-tools/test/cpu_RectangleROI_test.js +++ b/packages/cornerstone-tools/test/cpu_RectangleROI_test.js @@ -101,7 +101,7 @@ describe('RectangleRoiTool (CPU):', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + ToolGroupManager.destroyToolGroupByToolGroupUID('stack') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_2SEGs_AX.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_2SEGs_AX.png new file mode 100644 index 000000000..a1c638504 Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_2SEGs_AX.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_AX.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_AX.png new file mode 100644 index 000000000..e98dc960f Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_AX.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_AX_Custom.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_AX_Custom.png new file mode 100644 index 000000000..02ea77b75 Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_AX_Custom.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_COR.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_COR.png new file mode 100644 index 000000000..11b560aab Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_COR.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_GlobalConfig.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_GlobalConfig.png new file mode 100644 index 000000000..12fe8267b Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_GlobalConfig.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_RectangleScissor.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_RectangleScissor.png new file mode 100644 index 000000000..a6a55918f Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_RectangleScissor.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SAG.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SAG.png new file mode 100644 index 000000000..e0b68cb85 Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SAG.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SAG_RectangleScissor.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SAG_RectangleScissor.png new file mode 100644 index 000000000..596182b1f Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SAG_RectangleScissor.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_AX.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_AX.png new file mode 100644 index 000000000..4f55baf8a Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_AX.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_COR.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_COR.png new file mode 100644 index 000000000..13c3339ab Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_COR.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_SAG.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_SAG.png new file mode 100644 index 000000000..c908c0678 Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_SAG.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_ToolGroupPrioritize.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_ToolGroupPrioritize.png new file mode 100644 index 000000000..2d3707136 Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_ToolGroupPrioritize.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_activeInactive.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_activeInactive.png new file mode 100644 index 000000000..65dd87817 Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_activeInactive.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_controller_1.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_controller_1.png new file mode 100644 index 000000000..aeb3cc388 Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_controller_1.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_customColorLUT.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_customColorLUT.png new file mode 100644 index 000000000..f03e9657d Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_customColorLUT.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_indexController.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_indexController.png new file mode 100644 index 000000000..1f523a29a Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_indexController.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_indexLocked.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_indexLocked.png new file mode 100644 index 000000000..a75d8dc33 Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_indexLocked.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_initialConfig.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_initialConfig.png new file mode 100644 index 000000000..44d8fd068 Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_initialConfig.png differ diff --git a/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_visiblity.png b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_visiblity.png new file mode 100644 index 000000000..7302430e7 Binary files /dev/null and b/packages/cornerstone-tools/test/groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_visiblity.png differ diff --git a/packages/cornerstone-tools/test/segmentationConfigCotroller_test.js b/packages/cornerstone-tools/test/segmentationConfigCotroller_test.js new file mode 100644 index 000000000..dcd1c59d3 --- /dev/null +++ b/packages/cornerstone-tools/test/segmentationConfigCotroller_test.js @@ -0,0 +1,383 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index' +import * as csTools3d from '../src/index' + +import * as volumeURI_100_100_10_1_1_1_0_SEG_initialConfig from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_initialConfig.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_GlobalConfig from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_GlobalConfig.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_ToolGroupPrioritize from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_ToolGroupPrioritize.png' + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + unregisterAllImageLoaders, + metaData, + registerVolumeLoader, + createAndCacheVolume, + Utilities, + setVolumesOnViewports, + eventTarget, +} = cornerstone3D + +const { + ToolGroupManager, + SegmentationDisplayTool, + addSegmentationsForToolGroup, + CornerstoneTools3DEvents: EVENTS, + SegmentationRepresentations, + SegmentationModule, + RectangleScissorsTool, +} = csTools3d + +const { + fakeVolumeLoader, + fakeMetaDataProvider, + createNormalizedMouseEvent, + compareImages, +} = Utilities.testUtils + +const renderingEngineUID = Utilities.uuidv4() + +const viewportUID1 = 'AXIAL' +const viewportUID2 = 'SAGITTAL' +const viewportUID3 = 'CORONAL' + +const LABELMAP = SegmentationRepresentations.Labelmap + +const AXIAL = 'AXIAL' +const SAGITTAL = 'SAGITTAL' +const CORONAL = 'CORONAL' + +const TOOL_GROUP_UID = 'segToolGroup' + +const DOMElements = [] + +function createViewport( + renderingEngine, + orientation, + viewportUID = viewportUID1 +) { + const element = document.createElement('div') + + element.style.width = '250px' + element.style.height = '250px' + document.body.appendChild(element) + DOMElements.push(element) + + renderingEngine.enableElement({ + viewportUID: viewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element, + defaultOptions: { + orientation: ORIENTATION[orientation], + background: [1, 0, 1], // pinkish background + }, + }) + return element +} + +describe('Segmentation Controller --', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + + describe('Config Controller', function () { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(SegmentationDisplayTool, {}) + csTools3d.addTool(RectangleScissorsTool, {}) + cache.purgeCache() + this.segToolGroup = ToolGroupManager.createToolGroup(TOOL_GROUP_UID) + this.segToolGroup.addTool('SegmentationDisplay', {}) + this.segToolGroup.addTool('RectangleScissor', {}) + this.segToolGroup.setToolEnabled('SegmentationDisplay', {}) + this.segToolGroup.setToolActive('RectangleScissor', { + bindings: [{ mouseButton: 1 }], + }) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + // Note: since on toolGroup destroy, all segmentations are removed + // from the toolGroups, and that triggers a state_updated event, we + // need to make sure we remove the listeners before we destroy the + // toolGroup + eventTarget.reset() + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupByToolGroupUID(TOOL_GROUP_UID) + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should be able to load a segmentation with a toolGroup specific config', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + const toolGroupSpecificConfig = { + representations: { + [SegmentationRepresentations.Labelmap]: { + renderOutline: false, + fillAlpha: 0.999, + }, + }, + } + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const seg1VolumeID = + 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_20_20_3_60_60_6' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + + const compareImageCallback = () => { + const canvas1 = vp1.getCanvas() + const image1 = canvas1.toDataURL('image/png') + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_initialConfig, + 'volumeURI_100_100_10_1_1_1_0_SEG_initialConfig' + ) + + const representationConfig = + SegmentationModule.segmentationConfigController.getRepresentationConfig( + TOOL_GROUP_UID, + SegmentationRepresentations.Labelmap + ) + + const segmentationConfig = + SegmentationModule.segmentationConfigController.getSegmentationConfig( + TOOL_GROUP_UID + ) + + const representationConfigFromSegmentationConfig = + segmentationConfig.representations[ + SegmentationRepresentations.Labelmap + ] + expect(representationConfigFromSegmentationConfig.fillAlpha).toEqual( + representationConfig.fillAlpha + ) + expect( + representationConfigFromSegmentationConfig.renderOutline + ).toEqual(representationConfig.renderOutline) + + const globalRepresentationConfig = + SegmentationModule.segmentationConfigController.getGlobalRepresentationConfig( + SegmentationRepresentations.Labelmap + ) + + const globalSegmentationConfig = + SegmentationModule.segmentationConfigController.getGlobalSegmentationConfig() + + expect(globalRepresentationConfig).toBeDefined() + expect(globalRepresentationConfig.renderOutline).toBe(true) + + const globalRepresentationConfigFromSegmentationConfig = + globalSegmentationConfig.representations[ + SegmentationRepresentations.Labelmap + ] + + expect( + globalRepresentationConfigFromSegmentationConfig.renderOutline + ).toEqual(globalRepresentationConfig.renderOutline) + + done() + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + try { + createAndCacheVolume(seg1VolumeID, { imageIds: [] }).then(() => { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1] + ).then(() => { + vp1.render() + + // add two volumes on the segmentation + addSegmentationsForToolGroup( + TOOL_GROUP_UID, + [ + { + volumeUID: seg1VolumeID, + }, + ], + toolGroupSpecificConfig + ) + }) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should be able to set a global representation configuration', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + const globalRepresentationConfig = { + renderOutline: false, + fillAlpha: 0.996, + } + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const seg1VolumeID = + 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_30_30_3_80_80_6' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + + const compareImageCallback = () => { + const canvas1 = vp1.getCanvas() + const image1 = canvas1.toDataURL('image/png') + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_GlobalConfig, + 'volumeURI_100_100_10_1_1_1_0_SEG_GlobalConfig' + ).then(done, done.fail) + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + try { + createAndCacheVolume(seg1VolumeID, { imageIds: [] }).then(() => { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1] + ).then(() => { + vp1.render() + + SegmentationModule.segmentationConfigController.setGlobalRepresentationConfig( + SegmentationRepresentations.Labelmap, + globalRepresentationConfig + ) + const colorLUTIndex = 1 + SegmentationModule.segmentationColorController.addColorLut( + [ + [0, 0, 0, 0], + [0, 0, 255, 255], + ], + colorLUTIndex + ) + + // add two volumes on the segmentation + addSegmentationsForToolGroup(TOOL_GROUP_UID, [ + { + volumeUID: seg1VolumeID, + colorLUTIndex: 1, + }, + ]) + }) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should prioritize the toolGroup specific config over global config ', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + const globalRepresentationConfig = { + renderOutline: false, + fillAlpha: 0.996, + } + + const toolGroupSpecificConfig = { + representations: { + [SegmentationRepresentations.Labelmap]: { + renderOutline: true, + fillAlpha: 0.5, + }, + }, + } + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const seg1VolumeID = + 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_70_30_3_80_80_6' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + + const compareImageCallback = () => { + const canvas1 = vp1.getCanvas() + const image1 = canvas1.toDataURL('image/png') + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_ToolGroupPrioritize, + 'volumeURI_100_100_10_1_1_1_0_SEG_ToolGroupPrioritize' + ).then(done, done.fail) + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + try { + createAndCacheVolume(seg1VolumeID, { imageIds: [] }).then(() => { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1] + ).then(() => { + vp1.render() + + SegmentationModule.segmentationConfigController.setGlobalRepresentationConfig( + SegmentationRepresentations.Labelmap, + globalRepresentationConfig + ) + const colorLUTIndex = 1 + SegmentationModule.segmentationColorController.addColorLut( + [ + [0, 0, 0, 0], + [0, 255, 255, 255], + ], + colorLUTIndex + ) + + // add two volumes on the segmentation + addSegmentationsForToolGroup( + TOOL_GROUP_UID, + [ + { + volumeUID: seg1VolumeID, + colorLUTIndex: 1, + }, + ], + toolGroupSpecificConfig + ) + }) + }) + }) + } catch (e) { + done.fail(e) + } + }) + }) +}) diff --git a/packages/cornerstone-tools/test/segmentationRectangleScissor_test.js b/packages/cornerstone-tools/test/segmentationRectangleScissor_test.js new file mode 100644 index 000000000..8b95e011f --- /dev/null +++ b/packages/cornerstone-tools/test/segmentationRectangleScissor_test.js @@ -0,0 +1,452 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index' +import * as csTools3d from '../src/index' + +import * as volumeURI_100_100_10_1_1_1_0_SEG_RectangleScissor from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_RectangleScissor.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_SAG_RectangleScissor from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SAG_RectangleScissor.png' + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + unregisterAllImageLoaders, + metaData, + registerVolumeLoader, + createAndCacheVolume, + Utilities, + setVolumesOnViewports, + eventTarget, +} = cornerstone3D + +const { + ToolGroupManager, + SegmentationDisplayTool, + addSegmentationsForToolGroup, + CornerstoneTools3DEvents: EVENTS, + SegmentationRepresentations, + SegmentationModule, + RectangleScissorsTool, +} = csTools3d + +const { + fakeVolumeLoader, + fakeMetaDataProvider, + createNormalizedMouseEvent, + compareImages, +} = Utilities.testUtils + +const renderingEngineUID = Utilities.uuidv4() + +const viewportUID1 = 'AXIAL' +const viewportUID2 = 'SAGITTAL' +const viewportUID3 = 'CORONAL' + +const LABELMAP = SegmentationRepresentations.Labelmap + +const AXIAL = 'AXIAL' +const SAGITTAL = 'SAGITTAL' +const CORONAL = 'CORONAL' + +const DOMElements = [] + +function createViewport( + renderingEngine, + orientation, + viewportUID = viewportUID1 +) { + const element = document.createElement('div') + + element.style.width = '250px' + element.style.height = '250px' + document.body.appendChild(element) + DOMElements.push(element) + + renderingEngine.enableElement({ + viewportUID: viewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element, + defaultOptions: { + orientation: ORIENTATION[orientation], + background: [1, 0, 1], // pinkish background + }, + }) + return element +} + +describe('Segmentation Tools --', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + + describe('Rectangle Scissor:', function () { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(SegmentationDisplayTool, {}) + csTools3d.addTool(RectangleScissorsTool, {}) + cache.purgeCache() + this.segToolGroup = ToolGroupManager.createToolGroup('segToolGroup') + this.segToolGroup.addTool('SegmentationDisplay', {}) + this.segToolGroup.addTool('RectangleScissor', {}) + this.segToolGroup.setToolEnabled('SegmentationDisplay', {}) + this.segToolGroup.setToolActive('RectangleScissor', { + bindings: [{ mouseButton: 1 }], + }) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + // Note: since on toolGroup destroy, all segmentations are removed + // from the toolGroups, and that triggers a state_updated event, we + // need to make sure we remove the listeners before we destroy the + // toolGroup + eventTarget.reset() + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupByToolGroupUID('segToolGroup') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should be able to create a new segmentation from a viewport', function (done) { + createViewport(this.renderingEngine, AXIAL) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID1) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_GLOBAL_STATE_MODIFIED, + (evt) => { + const { segmentationUIDs } = evt.detail + expect(segmentationUIDs.length).toBe(1) + expect(segmentationUIDs[0].includes(volumeId)).toBe(true) + } + ) + + // wait until the render loop is done before we say done + eventTarget.addEventListener(EVENTS.SEGMENTATION_RENDERED, (evt) => { + done() + }) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp.uid) + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1] + ).then(() => { + vp.render() + + SegmentationModule.createNewSegmentationForViewport(vp).then( + (segmentationUID) => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segmentationUID }, + ]) + } + ) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should be able to edit the sementation data with the rectangle scissor', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID1) + + const drawRectangle = () => { + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + + const index1 = [11, 5, 0] + const index2 = [80, 80, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, element, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, element, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: element, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + element.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: element, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + document.dispatchEvent(evt) + } + + const newSegRenderedCallback = () => { + eventTarget.removeEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + // Since we need some time after the first render so that the + // request animation frame is done and is ready for the next frame. + setTimeout(() => { + drawRectangle() + }, 500) + } + + const compareImageCallback = () => { + const canvas = vp.getCanvas() + const image = canvas.toDataURL('image/png') + + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_SEG_RectangleScissor, + 'volumeURI_100_100_10_1_1_1_0_SEG_RectangleScissor' + ).then(done, done.fail) + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_GLOBAL_STATE_MODIFIED, + (evt) => { + const { segmentationUIDs } = evt.detail + expect(segmentationUIDs.length).toBe(1) + expect(segmentationUIDs[0].includes(volumeId)).toBe(true) + } + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp.uid) + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1] + ).then(() => { + vp.render() + + SegmentationModule.createNewSegmentationForViewport(vp).then( + (segmentationUID) => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segmentationUID }, + ]) + } + ) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should be able to edit the sementation data with the rectangle scissor with two viewports to render', function (done) { + const element1 = createViewport(this.renderingEngine, AXIAL) + const element2 = createViewport( + this.renderingEngine, + SAGITTAL, + viewportUID2 + ) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + const vp2 = this.renderingEngine.getViewport(viewportUID2) + + const drawRectangle = () => { + eventTarget.removeEventListener( + EVENTS.SEGMENTATION_RENDERED, + drawRectangle + ) + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + + const index1 = [11, 5, 0] + const index2 = [80, 80, 0] + + const { imageData } = vp1.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, element1, vp1) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, element1, vp1) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: element1, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + element1.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: element1, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + document.dispatchEvent(evt) + } + + let newSegRenderCount = 0 + const newSegRenderedCallback = () => { + newSegRenderCount++ + + if (newSegRenderCount !== 2) { + return + } + + eventTarget.removeEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + // Since we need some time after the first render so that the + // request animation frame is done and is ready for the next frame. + setTimeout(() => { + drawRectangle() + }, 500) + } + + let compareCount = 0 + const compareImageCallback = () => { + compareCount++ + + // since we are triggering segmentationRendered on each element, + // until both are rendered, we should not be comparing the images + if (compareCount !== 2) { + return + } + + const canvas1 = vp1.getCanvas() + const canvas2 = vp2.getCanvas() + + const image1 = canvas1.toDataURL('image/png') + const image2 = canvas2.toDataURL('image/png') + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_RectangleScissor, + 'volumeURI_100_100_10_1_1_1_0_SEG_RectangleScissor' + ) + + compareImages( + image2, + volumeURI_100_100_10_1_1_1_0_SEG_SAG_RectangleScissor, + 'volumeURI_100_100_10_1_1_1_0_SEG_SAG_RectangleScissor' + ).then(done, done.fail) + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_GLOBAL_STATE_MODIFIED, + (evt) => { + const { segmentationUIDs } = evt.detail + expect(segmentationUIDs.length).toBe(1) + expect(segmentationUIDs[0].includes(volumeId)).toBe(true) + } + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + this.segToolGroup.addViewports(this.renderingEngine.uid, vp2.uid) + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1, viewportUID2] + ).then(() => { + vp1.render() + vp2.render() + + SegmentationModule.createNewSegmentationForViewport(vp1).then( + (segmentationUID) => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segmentationUID }, + ]) + } + ) + }) + }) + } catch (e) { + done.fail(e) + } + }) + }) +}) diff --git a/packages/cornerstone-tools/test/segmentationRender_test.js b/packages/cornerstone-tools/test/segmentationRender_test.js new file mode 100644 index 000000000..78e438e04 --- /dev/null +++ b/packages/cornerstone-tools/test/segmentationRender_test.js @@ -0,0 +1,359 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index' +import * as csTools3d from '../src/index' + +import * as volumeURI_100_100_10_1_1_1_0_SEG_AX from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_AX.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_SAG from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SAG.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_COR from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_COR.png' +import * as volumeURI_100_100_10_1_1_1_0_2SEGs_AX from './groundTruth/volumeURI_100_100_10_1_1_1_0_2SEGs_AX.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_AX_Custom from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_AX_Custom.png' + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + unregisterAllImageLoaders, + metaData, + registerVolumeLoader, + createAndCacheVolume, + Utilities, + setVolumesOnViewports, + eventTarget, +} = cornerstone3D + +const { + ToolGroupManager, + SegmentationDisplayTool, + addSegmentationsForToolGroup, + CornerstoneTools3DEvents: EVENTS, + SegmentationRepresentations, + SegmentationState, + SegmentationModule, +} = csTools3d + +const { fakeMetaDataProvider, compareImages, fakeVolumeLoader } = + Utilities.testUtils + +const renderingEngineUID = Utilities.uuidv4() + +const viewportUID1 = 'AXIAL' +const viewportUID2 = 'SAGITTAL' +const viewportUID3 = 'CORONAL' + +const LABELMAP = SegmentationRepresentations.Labelmap + +const AXIAL = 'AXIAL' +const SAGITTAL = 'SAGITTAL' +const CORONAL = 'CORONAL' + +const DOMElements = [] + +function createViewport( + renderingEngine, + orientation, + viewportUID = viewportUID1 +) { + const element = document.createElement('div') + + element.style.width = '250px' + element.style.height = '250px' + document.body.appendChild(element) + DOMElements.push(element) + + renderingEngine.enableElement({ + viewportUID: viewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element, + defaultOptions: { + orientation: ORIENTATION[orientation], + background: [1, 0, 1], // pinkish background + }, + }) + return element +} + +describe('Segmentation Render -- ', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + + describe('Rendering', function () { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(SegmentationDisplayTool, {}) + cache.purgeCache() + this.segToolGroup = ToolGroupManager.createToolGroup('segToolGroup') + this.segToolGroup.addTool('SegmentationDisplay', {}) + this.segToolGroup.setToolEnabled('SegmentationDisplay', {}) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + // Note: since on toolGroup destroy, all segmentations are removed + // from the toolGroups, and that triggers a state_updated event, we + // need to make sure we remove the listeners before we destroy the + // toolGroup + eventTarget.reset() + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupByToolGroupUID('segToolGroup') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should successfully render a segmentation on a volume', function (done) { + createViewport(this.renderingEngine, AXIAL) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const segVolumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID1) + + eventTarget.addEventListener(EVENTS.SEGMENTATION_RENDERED, (evt) => { + const canvas = vp.getCanvas() + const image = canvas.toDataURL('image/png') + + expect(evt.detail.toolGroupUID).toBe('segToolGroup') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_SEG_AX, + 'volumeURI_100_100_10_1_1_1_0_SEG_AX' + ).then(done, done.fail) + }) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp.uid) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId, callback }], + [viewportUID1] + ) + vp.render() + createAndCacheVolume(segVolumeId, { imageIds: [] }).then(() => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segVolumeId }, + ]) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully render a segmentation on a volume with more than one viewport', function (done) { + createViewport(this.renderingEngine, AXIAL, viewportUID1) + createViewport(this.renderingEngine, SAGITTAL, viewportUID2) + createViewport(this.renderingEngine, CORONAL, viewportUID3) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const segVolumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + const vp2 = this.renderingEngine.getViewport(viewportUID2) + const vp3 = this.renderingEngine.getViewport(viewportUID3) + + let renderedViewportCounts = 0 + eventTarget.addEventListener(EVENTS.SEGMENTATION_RENDERED, (evt) => { + renderedViewportCounts++ + + if (renderedViewportCounts !== 3) { + return + } + + const canvas1 = vp1.getCanvas() + const canvas2 = vp2.getCanvas() + const canvas3 = vp3.getCanvas() + const image1 = canvas1.toDataURL('image/png') + const image2 = canvas2.toDataURL('image/png') + const image3 = canvas3.toDataURL('image/png') + + expect(evt.detail.toolGroupUID).toBe('segToolGroup') + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_AX, + 'volumeURI_100_100_10_1_1_1_0_AX' + ).then(() => { + compareImages( + image2, + volumeURI_100_100_10_1_1_1_0_SEG_SAG, + 'volumeURI_100_100_10_1_1_1_0_SAG' + ).then(() => { + compareImages( + image3, + volumeURI_100_100_10_1_1_1_0_SEG_COR, + 'volumeURI_100_100_10_1_1_1_0_COR' + ).then(done, done.fail) + }) + }) + }) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + this.segToolGroup.addViewports(this.renderingEngine.uid, vp2.uid) + this.segToolGroup.addViewports(this.renderingEngine.uid, vp3.uid) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId, callback }], + [viewportUID1, viewportUID2, viewportUID3] + ) + this.renderingEngine.render() + createAndCacheVolume(segVolumeId, { imageIds: [] }).then(() => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segVolumeId }, + ]) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully render two segmentations on a viewport', function (done) { + createViewport(this.renderingEngine, AXIAL, viewportUID1) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const segVolumeId = + 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_20_20_3_50_50_6' + const segVolumeId2 = + 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_60_60_2_80_80_7' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + + eventTarget.addEventListener(EVENTS.SEGMENTATION_RENDERED, (evt) => { + const canvas1 = vp1.getCanvas() + const image1 = canvas1.toDataURL('image/png') + + expect(evt.detail.toolGroupUID).toBe('segToolGroup') + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_2SEGs_AX, + 'volumeURI_100_100_10_1_1_1_0_2SEGs_AX' + ).then(done, done.fail) + }) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId, callback }], + [viewportUID1] + ) + this.renderingEngine.render() + createAndCacheVolume(segVolumeId, { imageIds: [] }).then(() => { + createAndCacheVolume(segVolumeId2, { imageIds: [] }).then(() => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segVolumeId }, + { volumeUID: segVolumeId2 }, + ]) + }) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully render a segmentation with toolGroup specific config', function (done) { + createViewport(this.renderingEngine, AXIAL, viewportUID1) + + const customToolGroupSeConfig = { + representations: { + [SegmentationRepresentations.Labelmap]: { + renderOutline: false, + fillAlpha: 0.99, + }, + }, + } + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const segVolumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + + eventTarget.addEventListener(EVENTS.SEGMENTATION_RENDERED, (evt) => { + const canvas1 = vp1.getCanvas() + const image1 = canvas1.toDataURL('image/png') + expect(evt.detail.toolGroupUID).toBe('segToolGroup') + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_AX_Custom, + 'volumeURI_100_100_10_1_1_1_0_SEG_AX_Custom' + ).then(done, done.fail) + }) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_STATE_MODIFIED, + (evt) => { + const toolGroupState = SegmentationState.getSegmentationState( + this.segToolGroup.uid + ) + + expect(toolGroupState).toBeDefined() + + const toolGroupConfig = + SegmentationModule.segmentationConfigController.getSegmentationConfig( + this.segToolGroup.uid + ) + + expect(toolGroupConfig).toBeDefined() + expect(toolGroupConfig.renderInactiveSegmentations).toBe(true) + expect(toolGroupConfig.representations[LABELMAP]).toEqual( + customToolGroupSeConfig.representations[LABELMAP] + ) + } + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId, callback }], + [viewportUID1] + ) + this.renderingEngine.render() + createAndCacheVolume(segVolumeId, { imageIds: [] }).then(() => { + addSegmentationsForToolGroup( + this.segToolGroup.uid, + [{ volumeUID: segVolumeId }], + { + ...customToolGroupSeConfig, + } + ) + }) + }) + } catch (e) { + done.fail(e) + } + }) + }) +}) diff --git a/packages/cornerstone-tools/test/segmentationSegmentIndexCotroller_test.js b/packages/cornerstone-tools/test/segmentationSegmentIndexCotroller_test.js new file mode 100644 index 000000000..d25c89440 --- /dev/null +++ b/packages/cornerstone-tools/test/segmentationSegmentIndexCotroller_test.js @@ -0,0 +1,576 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index' +import * as csTools3d from '../src/index' + +import * as volumeURI_100_100_10_1_1_1_0_SEG_controller_1 from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_controller_1.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_indexController from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_indexController.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_indexLocked from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_indexLocked.png' + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + unregisterAllImageLoaders, + metaData, + registerVolumeLoader, + createAndCacheVolume, + Utilities, + setVolumesOnViewports, + eventTarget, +} = cornerstone3D + +const { + ToolGroupManager, + SegmentationDisplayTool, + addSegmentationsForToolGroup, + CornerstoneTools3DEvents: EVENTS, + SegmentationRepresentations, + SegmentationModule, + RectangleScissorsTool, +} = csTools3d + +const { + fakeVolumeLoader, + fakeMetaDataProvider, + createNormalizedMouseEvent, + compareImages, +} = Utilities.testUtils + +const renderingEngineUID = Utilities.uuidv4() + +const viewportUID1 = 'AXIAL' +const viewportUID2 = 'SAGITTAL' +const viewportUID3 = 'CORONAL' + +const LABELMAP = SegmentationRepresentations.Labelmap + +const AXIAL = 'AXIAL' +const SAGITTAL = 'SAGITTAL' +const CORONAL = 'CORONAL' + +const TOOL_GROUP_UID = 'segToolGroup' + +const DOMElements = [] + +function createViewport( + renderingEngine, + orientation, + viewportUID = viewportUID1 +) { + const element = document.createElement('div') + + element.style.width = '250px' + element.style.height = '250px' + document.body.appendChild(element) + DOMElements.push(element) + + renderingEngine.enableElement({ + viewportUID: viewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element, + defaultOptions: { + orientation: ORIENTATION[orientation], + background: [1, 0, 1], // pinkish background + }, + }) + return element +} + +describe('Segmentation Index Controller --', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + + describe('Index/Lock Controller', function () { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(SegmentationDisplayTool, {}) + csTools3d.addTool(RectangleScissorsTool, {}) + cache.purgeCache() + this.segToolGroup = ToolGroupManager.createToolGroup(TOOL_GROUP_UID) + this.segToolGroup.addTool('SegmentationDisplay', {}) + this.segToolGroup.addTool('RectangleScissor', {}) + this.segToolGroup.setToolEnabled('SegmentationDisplay', {}) + this.segToolGroup.setToolActive('RectangleScissor', { + bindings: [{ mouseButton: 1 }], + }) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + // Note: since on toolGroup destroy, all segmentations are removed + // from the toolGroups, and that triggers a state_updated event, we + // need to make sure we remove the listeners before we destroy the + // toolGroup + eventTarget.reset() + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupByToolGroupUID(TOOL_GROUP_UID) + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should be able to segment different indices using rectangle scissor', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + + const drawRectangle = (index1, index2) => { + const { imageData } = vp1.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, element, vp1) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, element, vp1) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: element, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + element.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: element, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + document.dispatchEvent(evt) + } + + const newSegRenderedCallback = () => { + eventTarget.removeEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + // Since we need some time after the first render so that the + // request animation frame is done and is ready for the next frame. + setTimeout(() => { + drawRectangle([20, 20, 0], [40, 40, 0]) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + drawRectangle([30, 30, 0], [50, 50, 0]) + }, 500) + } + + const compareImageCallback = () => { + const canvas1 = vp1.getCanvas() + const image1 = canvas1.toDataURL('image/png') + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_controller_1, + 'volumeURI_100_100_10_1_1_1_0_SEG_controller_1' + ).then(done, done.fail) + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_GLOBAL_STATE_MODIFIED, + (evt) => { + const { segmentationUIDs } = evt.detail + expect(segmentationUIDs.length).toBe(1) + expect(segmentationUIDs[0].includes(volumeId)).toBe(true) + } + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1] + ).then(() => { + vp1.render() + + SegmentationModule.createNewSegmentationForViewport(vp1).then( + (segmentationUID) => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segmentationUID }, + ]) + } + ) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should be able to change the segment index when drawing segmentations', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + + const drawRectangle = (index1, index2) => { + const { imageData } = vp1.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, element, vp1) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, element, vp1) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: element, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + element.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: element, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + document.dispatchEvent(evt) + } + + const newSegRenderedCallback = () => { + eventTarget.removeEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + // Since we need some time after the first render so that the + // request animation frame is done and is ready for the next frame. + setTimeout(() => { + drawRectangle([20, 20, 0], [40, 40, 0]) + + SegmentationModule.segmentIndexController.setActiveSegmentIndex( + TOOL_GROUP_UID, + 2 + ) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + drawRectangle([30, 30, 0], [50, 50, 0]) + }, 500) + } + + const compareImageCallback = () => { + const canvas1 = vp1.getCanvas() + const image1 = canvas1.toDataURL('image/png') + + const activeSegmentIndex = + SegmentationModule.segmentIndexController.getActiveSegmentIndex( + TOOL_GROUP_UID + ) + + expect(activeSegmentIndex).toBe(2) + + // active segmentation + const segmentationInfo = + SegmentationModule.activeSegmentationController.getActiveSegmentationInfo( + TOOL_GROUP_UID + ) + + expect(segmentationInfo.segmentationDataUID).toBeDefined() + expect(segmentationInfo.volumeUID).toBeDefined() + + const anotherWayActiveSegmentIndex = + SegmentationModule.segmentIndexController.getActiveSegmentIndexForSegmentation( + segmentationInfo.volumeUID + ) + + expect(anotherWayActiveSegmentIndex).toBe(2) + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_indexController, + 'volumeURI_100_100_10_1_1_1_0_SEG_indexController' + ).then(done, done.fail) + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_GLOBAL_STATE_MODIFIED, + (evt) => { + const { segmentationUIDs } = evt.detail + expect(segmentationUIDs.length).toBe(1) + expect(segmentationUIDs[0].includes(volumeId)).toBe(true) + } + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1] + ).then(() => { + vp1.render() + + SegmentationModule.createNewSegmentationForViewport(vp1).then( + (segmentationUID) => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segmentationUID }, + ]) + } + ) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should be able to lock a segment', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + + const drawRectangle = (index1, index2) => { + const { imageData } = vp1.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, element, vp1) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, element, vp1) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: element, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + element.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: element, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + document.dispatchEvent(evt) + } + + const newSegRenderedCallback = () => { + eventTarget.removeEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + // Since we need some time after the first render so that the + // request animation frame is done and is ready for the next frame. + setTimeout(() => { + drawRectangle([20, 20, 0], [40, 40, 0]) + + SegmentationModule.segmentIndexController.setActiveSegmentIndex( + TOOL_GROUP_UID, + 2 + ) + + SegmentationModule.lockedSegmentController.setSegmentIndexLockedStatus( + TOOL_GROUP_UID, + 1, + true + ) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + drawRectangle([30, 30, 0], [50, 50, 0]) + }, 500) + } + + const compareImageCallback = () => { + const canvas1 = vp1.getCanvas() + const image1 = canvas1.toDataURL('image/png') + + const activeSegmentIndex = + SegmentationModule.segmentIndexController.getActiveSegmentIndex( + TOOL_GROUP_UID + ) + + expect(activeSegmentIndex).toBe(2) + + // active segmentation + const segmentationInfo = + SegmentationModule.activeSegmentationController.getActiveSegmentationInfo( + TOOL_GROUP_UID + ) + + expect(segmentationInfo.segmentationDataUID).toBeDefined() + expect(segmentationInfo.volumeUID).toBeDefined() + + const anotherWayActiveSegmentIndex = + SegmentationModule.segmentIndexController.getActiveSegmentIndexForSegmentation( + segmentationInfo.volumeUID + ) + + expect(anotherWayActiveSegmentIndex).toBe(2) + + const locked1 = + SegmentationModule.lockedSegmentController.getLockedSegmentsForSegmentation( + segmentationInfo.volumeUID + ) + + expect(locked1.length).toBe(1) + expect(locked1[0]).toBe(1) + + const lockedStatus1 = + SegmentationModule.lockedSegmentController.getSegmentIndexLockedStatus( + TOOL_GROUP_UID, + 1 + ) + + expect(lockedStatus1).toBe(true) + + const lockedStatus2 = + SegmentationModule.lockedSegmentController.getSegmentIndexLockedStatusForSegmentation( + segmentationInfo.volumeUID, + 2 + ) + expect(lockedStatus2).toBe(false) + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_indexLocked, + 'volumeURI_100_100_10_1_1_1_0_SEG_indexLocked' + ).then(done, done.fail) + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_GLOBAL_STATE_MODIFIED, + (evt) => { + const { segmentationUIDs } = evt.detail + expect(segmentationUIDs.length).toBe(1) + expect(segmentationUIDs[0].includes(volumeId)).toBe(true) + } + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1] + ).then(() => { + vp1.render() + + SegmentationModule.createNewSegmentationForViewport(vp1).then( + (segmentationUID) => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segmentationUID }, + ]) + } + ) + }) + }) + } catch (e) { + done.fail(e) + } + }) + }) +}) diff --git a/packages/cornerstone-tools/test/segmentationSphereScissor_test.js b/packages/cornerstone-tools/test/segmentationSphereScissor_test.js new file mode 100644 index 000000000..ff2bb2a81 --- /dev/null +++ b/packages/cornerstone-tools/test/segmentationSphereScissor_test.js @@ -0,0 +1,290 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index' +import * as csTools3d from '../src/index' + +import * as volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_AX from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_AX.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_SAG from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_SAG.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_COR from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_COR.png' +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + unregisterAllImageLoaders, + metaData, + registerVolumeLoader, + createAndCacheVolume, + Utilities, + setVolumesOnViewports, + eventTarget, +} = cornerstone3D + +const { + ToolGroupManager, + SegmentationDisplayTool, + addSegmentationsForToolGroup, + CornerstoneTools3DEvents: EVENTS, + SegmentationRepresentations, + SegmentationModule, + SphereScissorsTool, +} = csTools3d + +const { + fakeVolumeLoader, + fakeMetaDataProvider, + createNormalizedMouseEvent, + compareImages, +} = Utilities.testUtils + +const renderingEngineUID = Utilities.uuidv4() + +const viewportUID1 = 'AXIAL' +const viewportUID2 = 'SAGITTAL' +const viewportUID3 = 'CORONAL' + +const LABELMAP = SegmentationRepresentations.Labelmap + +const AXIAL = 'AXIAL' +const SAGITTAL = 'SAGITTAL' +const CORONAL = 'CORONAL' + +const DOMElements = [] + +function createViewport( + renderingEngine, + orientation, + viewportUID = viewportUID1 +) { + const element = document.createElement('div') + + element.style.width = '250px' + element.style.height = '250px' + document.body.appendChild(element) + DOMElements.push(element) + + renderingEngine.enableElement({ + viewportUID: viewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element, + defaultOptions: { + orientation: ORIENTATION[orientation], + background: [1, 0, 1], // pinkish background + }, + }) + return element +} + +describe('Segmentation Tools --', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + + describe('Sphere Scissor', function () { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(SegmentationDisplayTool, {}) + csTools3d.addTool(SphereScissorsTool, {}) + cache.purgeCache() + this.segToolGroup = ToolGroupManager.createToolGroup('segToolGroup') + this.segToolGroup.addTool('SegmentationDisplay', {}) + this.segToolGroup.addTool('SphereScissor', {}) + this.segToolGroup.setToolEnabled('SegmentationDisplay', {}) + this.segToolGroup.setToolActive('SphereScissor', { + bindings: [{ mouseButton: 1 }], + }) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + // Note: since on toolGroup destroy, all segmentations are removed + // from the toolGroups, and that triggers a state_updated event, we + // need to make sure we remove the listeners before we destroy the + // toolGroup + eventTarget.reset() + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupByToolGroupUID('segToolGroup') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should be able to edit the sementation data with the sphere scissor', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + const element2 = createViewport( + this.renderingEngine, + SAGITTAL, + viewportUID2 + ) + const element3 = createViewport( + this.renderingEngine, + CORONAL, + viewportUID3 + ) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + const vp2 = this.renderingEngine.getViewport(viewportUID2) + const vp3 = this.renderingEngine.getViewport(viewportUID3) + + const drawSphere = () => { + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + + const index1 = [50, 50, 0] + const index2 = [60, 60, 0] + + const { imageData } = vp1.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, element, vp1) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, element, vp1) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: element, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + element.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: element, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + document.dispatchEvent(evt) + } + + let renderCount = 0 + const newSegRenderedCallback = () => { + renderCount++ + + if (renderCount === 3) { + return + } + + eventTarget.removeEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + // Since we need some time after the first render so that the + // request animation frame is done and is ready for the next frame. + setTimeout(() => { + drawSphere() + }, 500) + } + + let compareCount = 0 + const compareImageCallback = () => { + compareCount++ + + if (compareCount !== 3) { + return + } + + const canvas1 = vp1.getCanvas() + const canvas2 = vp2.getCanvas() + const canvas3 = vp3.getCanvas() + const image1 = canvas1.toDataURL('image/png') + const image2 = canvas2.toDataURL('image/png') + const image3 = canvas3.toDataURL('image/png') + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_AX, + 'volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_AX' + ) + + compareImages( + image2, + volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_SAG, + 'volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_SAG' + ) + + compareImages( + image3, + volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_COR, + 'volumeURI_100_100_10_1_1_1_0_SEG_SphereScissor_COR' + ).then(done, done.fail) + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + newSegRenderedCallback + ) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_GLOBAL_STATE_MODIFIED, + (evt) => { + const { segmentationUIDs } = evt.detail + expect(segmentationUIDs.length).toBe(1) + expect(segmentationUIDs[0].includes(volumeId)).toBe(true) + } + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + this.segToolGroup.addViewports(this.renderingEngine.uid, vp2.uid) + this.segToolGroup.addViewports(this.renderingEngine.uid, vp3.uid) + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1, viewportUID2, viewportUID3] + ).then(() => { + vp1.render() + vp2.render() + vp3.render() + + SegmentationModule.createNewSegmentationForViewport(vp1).then( + (segmentationUID) => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segmentationUID }, + ]) + } + ) + }) + }) + } catch (e) { + done.fail(e) + } + }) + }) +}) diff --git a/packages/cornerstone-tools/test/segmentationState_test.js b/packages/cornerstone-tools/test/segmentationState_test.js new file mode 100644 index 000000000..5fbf4204f --- /dev/null +++ b/packages/cornerstone-tools/test/segmentationState_test.js @@ -0,0 +1,247 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index' +import * as csTools3d from '../src/index' + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + unregisterAllImageLoaders, + metaData, + registerVolumeLoader, + createAndCacheVolume, + Utilities, + setVolumesOnViewports, + eventTarget, +} = cornerstone3D + +const { + ToolGroupManager, + SegmentationDisplayTool, + addSegmentationsForToolGroup, + CornerstoneTools3DEvents: EVENTS, + SegmentationState, + Utilities: { segmentation: segUtils }, + SegmentationRepresentations, +} = csTools3d + +const { fakeMetaDataProvider, fakeVolumeLoader } = Utilities.testUtils + +const renderingEngineUID = Utilities.uuidv4() + +const viewportUID = 'VIEWPORT' + +const AXIAL = 'AXIAL' +const SAGITTAL = 'SAGITTAL' +const CORONAL = 'CORONAL' + +const LABELMAP = SegmentationRepresentations.Labelmap + +const DOMElements = [] + +function createViewport(renderingEngine, orientation) { + const element = document.createElement('div') + + element.style.width = '250px' + element.style.height = '250px' + document.body.appendChild(element) + DOMElements.push(element) + + renderingEngine.setViewports([ + { + viewportUID: viewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element, + defaultOptions: { + orientation: ORIENTATION[orientation], + background: [1, 0, 1], // pinkish background + }, + }, + ]) + return element +} + +describe('Segmentation State -- ', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + + describe('State', function () { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(SegmentationDisplayTool, {}) + cache.purgeCache() + this.segToolGroup = ToolGroupManager.createToolGroup('segToolGroup') + this.segToolGroup.addTool('SegmentationDisplay', {}) + this.segToolGroup.setToolEnabled('SegmentationDisplay', {}) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + // Note: since on toolGroup destroy, all segmentations are removed + // from the toolGroups, and that triggers a state_updated event, we + // need to make sure we remove the listeners before we destroy the + // toolGroup + eventTarget.reset() + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupByToolGroupUID('segToolGroup') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should successfully create a global and toolGroup state when segmentation is added', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const segVolumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_GLOBAL_STATE_MODIFIED, + (evt) => { + const globalState = + SegmentationState.getGlobalSegmentationDataByUID(segVolumeId) + + expect(evt.detail.segmentationUIDs.length).toBe(1) + expect(evt.detail.segmentationUIDs[0]).toBe(segVolumeId) + + expect(globalState).toBeDefined() + + expect(globalState.volumeUID).toBe(segVolumeId) + expect(globalState.label).toBe(segVolumeId) + expect(globalState.activeSegmentIndex).toBe(1) + } + ) + eventTarget.addEventListener( + EVENTS.SEGMENTATION_STATE_MODIFIED, + (evt) => { + const stateManager = + SegmentationState.getDefaultSegmentationStateManager(segVolumeId) + + const state = stateManager.getState() + + expect(evt.detail.toolGroupUID).toBe('segToolGroup') + expect(state).toBeDefined() + expect(state.toolGroups).toBeDefined() + + const toolGroupSegmentationState = + state.toolGroups[this.segToolGroup.uid] + + expect(toolGroupSegmentationState).toBeDefined() + expect(toolGroupSegmentationState.segmentations.length).toBe(1) + + const segState = SegmentationState.getSegmentationState( + this.segToolGroup.uid + ) + + expect(toolGroupSegmentationState.segmentations).toEqual(segState) + + const segData = segState[0] + + expect(segData.active).toBe(true) + expect(segData.visibility).toBe(true) + expect(segData.segmentationDataUID).toBeDefined() + expect(segData.volumeUID).toBe(segVolumeId) + expect(segData.representation).toBeDefined() + expect(segData.representation.type).toBe(LABELMAP) + expect(segData.representation.config).toBeDefined() + + done() + } + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp.uid) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId, callback }], + [viewportUID] + ) + vp.render() + createAndCacheVolume(segVolumeId, { imageIds: [] }).then(() => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segVolumeId }, + ]) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully create a global default representation configuration', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const segVolumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_GLOBAL_STATE_MODIFIED, + (evt) => { + const { segmentationUIDs } = evt.detail + const globalConfig = SegmentationState.getGlobalSegmentationConfig() + + expect(globalConfig.renderInactiveSegmentations).toBe(true) + expect(globalConfig.representations).toBeDefined() + expect(globalConfig.representations[LABELMAP]).toBeDefined() + + const representationConfig = + segUtils.getDefaultRepresentationConfig(LABELMAP) + + const stateConfig = globalConfig.representations[LABELMAP] + + expect(Object.keys(stateConfig)).toEqual( + Object.keys(representationConfig) + ) + + expect(Object.values(stateConfig)).toEqual( + Object.values(representationConfig) + ) + + done() + } + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp.uid) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId, callback }], + [viewportUID] + ) + vp.render() + createAndCacheVolume(segVolumeId, { imageIds: [] }).then(() => { + addSegmentationsForToolGroup(this.segToolGroup.uid, [ + { volumeUID: segVolumeId }, + ]) + }) + }) + } catch (e) { + done.fail(e) + } + }) + }) +}) diff --git a/packages/cornerstone-tools/test/segmentationVisibilityCotroller_test.js b/packages/cornerstone-tools/test/segmentationVisibilityCotroller_test.js new file mode 100644 index 000000000..f8aabc387 --- /dev/null +++ b/packages/cornerstone-tools/test/segmentationVisibilityCotroller_test.js @@ -0,0 +1,325 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index' +import * as csTools3d from '../src/index' + +import * as volumeURI_100_100_10_1_1_1_0_SEG_activeInactive from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_activeInactive.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_customColorLUT from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_customColorLUT.png' +import * as volumeURI_100_100_10_1_1_1_0_SEG_visiblity from './groundTruth/volumeURI_100_100_10_1_1_1_0_SEG_visiblity.png' + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + unregisterAllImageLoaders, + metaData, + registerVolumeLoader, + createAndCacheVolume, + Utilities, + setVolumesOnViewports, + eventTarget, +} = cornerstone3D + +const { + ToolGroupManager, + SegmentationDisplayTool, + addSegmentationsForToolGroup, + CornerstoneTools3DEvents: EVENTS, + SegmentationRepresentations, + SegmentationModule, + RectangleScissorsTool, +} = csTools3d + +const { fakeVolumeLoader, fakeMetaDataProvider, compareImages } = + Utilities.testUtils + +const renderingEngineUID = Utilities.uuidv4() + +const viewportUID1 = 'AXIAL' +const viewportUID2 = 'SAGITTAL' +const viewportUID3 = 'CORONAL' + +const LABELMAP = SegmentationRepresentations.Labelmap + +const AXIAL = 'AXIAL' +const SAGITTAL = 'SAGITTAL' +const CORONAL = 'CORONAL' + +const TOOL_GROUP_UID = 'segToolGroup' + +const DOMElements = [] + +function createViewport( + renderingEngine, + orientation, + viewportUID = viewportUID1 +) { + const element = document.createElement('div') + + element.style.width = '250px' + element.style.height = '250px' + document.body.appendChild(element) + DOMElements.push(element) + + renderingEngine.enableElement({ + viewportUID: viewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element, + defaultOptions: { + orientation: ORIENTATION[orientation], + background: [1, 0, 1], // pinkish background + }, + }) + return element +} + +describe('Segmentation Controller --', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + + describe('Visibility/Color Controller', function () { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(SegmentationDisplayTool, {}) + csTools3d.addTool(RectangleScissorsTool, {}) + cache.purgeCache() + this.segToolGroup = ToolGroupManager.createToolGroup(TOOL_GROUP_UID) + this.segToolGroup.addTool('SegmentationDisplay', {}) + this.segToolGroup.addTool('RectangleScissor', {}) + this.segToolGroup.setToolEnabled('SegmentationDisplay', {}) + this.segToolGroup.setToolActive('RectangleScissor', { + bindings: [{ mouseButton: 1 }], + }) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + // Note: since on toolGroup destroy, all segmentations are removed + // from the toolGroups, and that triggers a state_updated event, we + // need to make sure we remove the listeners before we destroy the + // toolGroup + eventTarget.reset() + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupByToolGroupUID(TOOL_GROUP_UID) + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should be able to load two segmentations on the toolGroup', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const seg1VolumeID = + 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_20_20_3_60_60_6' + const seg2VolumeID = + 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_35_20_2_80_60_7_2' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + + const compareImageCallback = () => { + const canvas1 = vp1.getCanvas() + const image1 = canvas1.toDataURL('image/png') + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_activeInactive, + 'volumeURI_100_100_10_1_1_1_0_SEG_activeInactive' + ).then(done, done.fail) + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + try { + createAndCacheVolume(seg1VolumeID, { imageIds: [] }).then(() => { + createAndCacheVolume(seg2VolumeID, { imageIds: [] }).then(() => { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1] + ).then(() => { + vp1.render() + + // add two volumes on the segmentation + addSegmentationsForToolGroup(TOOL_GROUP_UID, [ + { + volumeUID: seg1VolumeID, + }, + { + volumeUID: seg2VolumeID, + }, + ]) + }) + }) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + it('should be able to load two segmentations on the toolGroup with different colorIndices', function (done) { + const element = createViewport(this.renderingEngine, AXIAL) + + // fake volume generator follows the pattern of + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + const seg1VolumeID = + 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_20_20_3_60_60_6' + const seg2VolumeID = + 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_35_20_2_80_60_7_2' + const vp1 = this.renderingEngine.getViewport(viewportUID1) + + const compareImageCallback = () => { + const canvas1 = vp1.getCanvas() + const image1 = canvas1.toDataURL('image/png') + + compareImages( + image1, + volumeURI_100_100_10_1_1_1_0_SEG_customColorLUT, + 'volumeURI_100_100_10_1_1_1_0_SEG_customColorLUT' + ).then(done, done.fail) + } + + eventTarget.addEventListener( + EVENTS.SEGMENTATION_RENDERED, + compareImageCallback + ) + + this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + try { + createAndCacheVolume(seg1VolumeID, { imageIds: [] }).then(() => { + createAndCacheVolume(seg2VolumeID, { imageIds: [] }).then(() => { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesOnViewports( + this.renderingEngine, + [{ volumeUID: volumeId }], + [viewportUID1] + ).then(() => { + vp1.render() + + const colorLUTIndex = 1 + SegmentationModule.segmentationColorController.addColorLut( + [ + [0, 0, 0, 0], + [245, 209, 145, 255], + ], + colorLUTIndex + ) + + // add two volumes on the segmentation + addSegmentationsForToolGroup(TOOL_GROUP_UID, [ + { + volumeUID: seg1VolumeID, + colorLUTIndex: 1, + }, + { + volumeUID: seg2VolumeID, + }, + ]) + }) + }) + }) + }) + } catch (e) { + done.fail(e) + } + }) + + // fit('should be able to load two segmentations on the toolGroup and make one invisible', function (done) { + // const element = createViewport(this.renderingEngine, AXIAL) + + // // fake volume generator follows the pattern of + // const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + // const seg1VolumeID = + // 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_20_20_3_60_60_6' + // const seg2VolumeID = + // 'fakeVolumeLoader:volumeURIExact_100_100_10_1_1_1_0_35_20_2_80_60_7_2' + // const vp1 = this.renderingEngine.getViewport(viewportUID1) + + // const compareImageCallback = () => { + // console.log('calling compare ************') + // const canvas1 = vp1.getCanvas() + // const image1 = canvas1.toDataURL('image/png') + + // compareImages( + // image1, + // volumeURI_100_100_10_1_1_1_0_SEG_visiblity, + // 'volumeURI_100_100_10_1_1_1_0_SEG_visiblity' + // ) + + // const segmentationState = + // csTools3d.SegmentationState.getSegmentationState(TOOL_GROUP_UID) + + // // expect(segmentationState.length).toBe(2) + // // expect(segmentationState[0].visibility).toBe(true) + // // expect(segmentationState[1].visibility).toBe(false) + // // expect(segmentationState[0].active).toBe(true) + // // expect(segmentationState[1].active).toBe(false) + + // // done() + // } + + // eventTarget.addEventListener( + // EVENTS.SEGMENTATION_RENDERED, + // compareImageCallback + // ) + + // this.segToolGroup.addViewports(this.renderingEngine.uid, vp1.uid) + + // try { + // createAndCacheVolume(seg1VolumeID, { imageIds: [] }).then(() => { + // createAndCacheVolume(seg2VolumeID, { imageIds: [] }).then(() => { + // createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + // setVolumesOnViewports( + // this.renderingEngine, + // [{ volumeUID: volumeId }], + // [viewportUID1] + // ).then(() => { + // vp1.render() + + // // add two volumes on the segmentation + // addSegmentationsForToolGroup(TOOL_GROUP_UID, [ + // { + // volumeUID: seg1VolumeID, + // }, + // { + // volumeUID: seg2VolumeID, + // }, + // ]).then(() => { + // const segmentationData = + // SegmentationModule.activeSegmentationController.getActiveSegmentationInfo( + // TOOL_GROUP_UID + // ) + + // SegmentationModule.segmentationVisibilityController.setSegmentationVisibility( + // TOOL_GROUP_UID, + // segmentationData.segmentationDataUID, + // false + // ) + // }) + // }) + // }) + // }) + // }) + // } catch (e) { + // done.fail(e) + // } + // }, ) + }) +}) diff --git a/packages/cornerstone-tools/test/synchronizerManager_test.js b/packages/cornerstone-tools/test/synchronizerManager_test.js index f3f6f01ad..c86d547f1 100644 --- a/packages/cornerstone-tools/test/synchronizerManager_test.js +++ b/packages/cornerstone-tools/test/synchronizerManager_test.js @@ -90,7 +90,7 @@ describe('Synchronizer Manager: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('volume1') + ToolGroupManager.destroyToolGroupByToolGroupUID('volume1') DOMElements.forEach((el) => { if (el.parentNode) { @@ -230,7 +230,7 @@ describe('Synchronizer Manager: ', () => { this.renderingEngine.destroy() metaData.removeProvider(fakeMetaDataProvider) unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('volume1') + ToolGroupManager.destroyToolGroupByToolGroupUID('volume1') DOMElements.forEach((el) => { if (el.parentNode) { diff --git a/packages/demo/src/ExampleSegmentationRender.tsx b/packages/demo/src/ExampleSegmentationRender.tsx index d3f14c385..5c2de7ff5 100644 --- a/packages/demo/src/ExampleSegmentationRender.tsx +++ b/packages/demo/src/ExampleSegmentationRender.tsx @@ -8,21 +8,30 @@ import { createAndCacheVolume, createAndCacheDerivedVolume, init as cs3dInit, - getVolumeViewportsContatiningSameVolumes, + eventTarget, } from '@precisionmetrics/cornerstone-render' import { // Segmentation SegmentationModule, lockedSegmentController, segmentIndexController, - activeLabelmapController, - hideSegmentController, + activeSegmentationController, + segmentationVisibilityController, synchronizers, ToolBindings, ToolModes, CornerstoneTools3DEvents, toolDataSelection, Utilities as csToolsUtils, + SegmentationRepresentations, + getSegmentationState, + getGlobalSegmentationDataByUID, + getSegmentationDataByUID, + addSegmentationsForToolGroup, + removeSegmentationsForToolGroup, + getSegmentationsForToolGroup, + getGlobalSegmentationState, + segmentationConfigController, } from '@precisionmetrics/cornerstone-tools' import * as csTools3d from '@precisionmetrics/cornerstone-tools' @@ -54,7 +63,10 @@ let ctSceneToolGroup, ctVRSceneToolGroup, ctObliqueToolGroup, ptTypesSceneToolGroup, - ptCtLayoutTools + ptCtLayoutTools, + ctAxialSagittalSegmentationToolGroup, + ptCoronalSegmentationToolGroup, + axialPTCTSegmentationToolGroup const { createCameraPositionSynchronizer, createVOISynchronizer } = synchronizers @@ -94,8 +106,10 @@ class SegmentationExample extends Component { layoutIndex: 0, destroyed: false, // segmentation state - renderOutline: true, - renderInactiveLabelmaps: true, + renderOutlineGlobal: true, + renderOutlineToolGroup: true, + renderInactiveSegmentationsGlobal: true, + renderInactiveSegmentationsToolGroup: true, // viewportGrid: { numCols: 4, @@ -119,37 +133,29 @@ class SegmentationExample extends Component { ], }, ptCtLeftClickTool: 'WindowLevel', - viewportUIDs: [ - 'ctAxial', - 'ctSagittal', - 'ctCoronal', - 'ptAxial', - 'ptSagittal', - 'ptCoronal', - 'fusionAxial', - 'fusionSagittal', - 'fusionCoronal', - 'ptMipAxial', - ], - toolGroups: {}, ctWindowLevelDisplay: { ww: 0, wc: 0 }, ptThresholdDisplay: 5, // Segmentation segmentationStatus: '', segmentationToolActive: false, - activeViewportUID: VIEWPORT_IDS.CT.AXIAL, - selectedLabelmapUID: '', - availableLabelmaps: [], - activeSegmentIndex: 1, - fillAlpha: 0.9, - fillAlphaInactive: 0.8, + selectedsegmentationUID: '', + availableSegmentations: [], + fillAlphaGlobal: 0.9, + fillAlphaToolGroup: 0.9, + fillAlphaInactiveGlobal: 0.8, + fillAlphaInactiveToolGroup: 0.8, segmentLocked: false, thresholdMin: 0, thresholdMax: 100, numSlicesForThreshold: 1, - selectedStrategy: '', - selectedSegmentationFromAll: '', - allSegmentations: [], + chosenToolStrategy: '', + // toolGroup + toolGroups: {}, + selectedToolGroupName: '', + selectedToolGroupSegmentationDataUIDs: [], + // all segmentations + allSegmentationUIDs: [], + selectedSegmentationUIDFromAll: '', tmtv: null, } @@ -205,9 +211,6 @@ class SegmentationExample extends Component { ptSceneToolGroup, fusionSceneToolGroup, ptMipSceneToolGroup, - ctVRSceneToolGroup, - ctObliqueToolGroup, - ptTypesSceneToolGroup, } = initToolGroups()) this.ctVolumeUID = ctVolumeUID @@ -242,24 +245,6 @@ class SegmentationExample extends Component { ptSceneToolGroup, fusionSceneToolGroup, ptMipSceneToolGroup, - ctVRSceneToolGroup, - ctObliqueToolGroup, - ptTypesSceneToolGroup, - }) - - this.setState({ - toolGroups: { - ctAxial: ctSceneToolGroup, - ctSagittal: ctSceneToolGroup, - ctCoronal: ctSceneToolGroup, - ptAxial: ptSceneToolGroup, - ptSagittal: ptSceneToolGroup, - ptCoronal: ptSceneToolGroup, - fusionAxial: fusionSceneToolGroup, - fusionSagittal: fusionSceneToolGroup, - fusionCoronal: fusionSceneToolGroup, - ptMipAxial: ptMipSceneToolGroup, - }, }) // Create volumes @@ -305,6 +290,11 @@ class SegmentationExample extends Component { this.setState({ metadataLoaded: true, ctWindowLevelDisplay: { ww: windowWidth, wc: windowCenter }, + toolGroups: { + ctSceneToolGroup, + ptSceneToolGroup, + }, + selectedToolGroupName: 'ctSceneToolGroup', }) // This will initialize volumes in GPU memory @@ -315,56 +305,42 @@ class SegmentationExample extends Component { componentDidUpdate(prevProps, prevState) { const { layoutIndex } = this.state - const { renderingEngine } = this + this._removeEventListeners() + this._addEventListeners() + } - const layout = LAYOUTS[layoutIndex] + _removeEventListeners() { + eventTarget.removeEventListener( + CornerstoneTools3DEvents.SEGMENTATION_STATE_MODIFIED, + this.onSegmentationStateModified + ) - this._elementNodes.forEach((element) => { - element.addEventListener( - CornerstoneTools3DEvents.LABELMAP_STATE_UPDATED, - this.onLabelmapStateUpdated - ) - }) + eventTarget.removeEventListener( + CornerstoneTools3DEvents.SEGMENTATION_GLOBAL_STATE_MODIFIED, + this.onGlobalSegmentationStateUpdated + ) - this._elementNodes.forEach((element) => { - element.addEventListener( - CornerstoneTools3DEvents.LABELMAP_REMOVED, - this.onLabelmapRemoved - ) - }) + eventTarget.removeEventListener( + CornerstoneTools3DEvents.SEGMENTATION_REMOVED, + this.onSegmentationRemoved + ) + } - if (prevState.layoutIndex !== layoutIndex) { - if (layout === 'FusionMIP') { - // FusionMIP + _addEventListeners() { + eventTarget.addEventListener( + CornerstoneTools3DEvents.SEGMENTATION_STATE_MODIFIED, + this.onSegmentationStateModified + ) - ptCtFusion.setLayout( - renderingEngine, - this._elementNodes, - { - ctSceneToolGroup, - ptSceneToolGroup, - fusionSceneToolGroup, - ptMipSceneToolGroup, - }, - { - axialSynchronizers: [], - sagittalSynchronizers: [], - coronalSynchronizers: [], - ptThresholdSynchronizer: this.ptThresholdSync, - ctWLSynchronizer: this.ctWLSync, - } - ) + eventTarget.addEventListener( + CornerstoneTools3DEvents.SEGMENTATION_GLOBAL_STATE_MODIFIED, + this.onGlobalSegmentationStateUpdated + ) - ptCtFusion.setVolumes( - renderingEngine, - ctVolumeUID, - ptVolumeUID, - colormaps[this.state.petColorMapIndex] - ) - } else { - throw new Error('Unrecognised layout index') - } - } + eventTarget.addEventListener( + CornerstoneTools3DEvents.SEGMENTATION_REMOVED, + this.onSegmentationRemoved + ) } componentWillUnmount() { @@ -378,59 +354,105 @@ class SegmentationExample extends Component { this.renderingEngine.destroy() } - onLabelmapStateUpdated = (evt) => { - const { element, viewportUID } = evt.detail - if (viewportUID !== this.state.activeViewportUID) { + onGlobalSegmentationStateUpdated = (evt) => { + const { segmentationUIDs } = evt.detail + const allSegmentationUIDs = getGlobalSegmentationState().map( + ({ volumeUID }) => volumeUID + ) + + let newSelectedSegmentationUID = this.state.selectedSegmentationUIDFromAll + if (newSelectedSegmentationUID === '') { + newSelectedSegmentationUID = allSegmentationUIDs[0] + } + + this.setState({ + allSegmentationUIDs: allSegmentationUIDs, + selectedSegmentationUIDFromAll: newSelectedSegmentationUID, + }) + } + + onSegmentationStateModified = (evt) => { + const { toolGroupUID } = evt.detail + + if (toolGroupUID !== this.state.selectedToolGroupName) { return } - const labelmapUIDs = SegmentationModule.getLabelmapUIDsForElement(element) + const activeSegmentationInfo = + activeSegmentationController.getActiveSegmentationInfo(toolGroupUID) - console.debug(labelmapUIDs) - const activeLabelmapUID = - activeLabelmapController.getActiveLabelmapUID(element) - this.setState((prevState) => { - // merge the segmentations - const newSegmentations = [...prevState.allSegmentations] + let selectedsegmentationUID, segmentLocked, activeSegmentIndex - let newSelectedFromAll = this.state.selectedSegmentationFromAll - if (!newSegmentations.includes(activeLabelmapUID)) { - newSegmentations.push(activeLabelmapUID) - newSelectedFromAll = activeLabelmapUID - } + if (activeSegmentationInfo) { + activeSegmentIndex = activeSegmentationInfo.activeSegmentIndex + selectedsegmentationUID = activeSegmentationInfo.segmentationDataUID - return { - availableLabelmaps: labelmapUIDs, - selectedLabelmapUID: activeLabelmapUID, - allSegmentations: newSegmentations, - selectedSegmentationFromAll: newSelectedFromAll, - } + segmentLocked = + lockedSegmentController.getSegmentIndexLockedStatusForSegmentation( + activeSegmentationInfo.volumeUID, + activeSegmentIndex + ) + } + + const toolGroupSegmentations = getSegmentationState(toolGroupUID) + + let segmentationDataUIDs + + if (toolGroupSegmentations) { + segmentationDataUIDs = toolGroupSegmentations.map( + (segData) => segData.segmentationDataUID + ) + } + + this.setState({ + selectedToolGroupSegmentationDataUIDs: segmentationDataUIDs, + selectedsegmentationUID: selectedsegmentationUID, + selectedViewportActiveSegmentIndex: activeSegmentIndex ?? 1, + segmentLocked: segmentLocked ?? false, }) } - onLabelmapRemoved = (evt) => { + + onSegmentationRemoved = (evt) => { const { element } = evt.detail - const labelmapUIDs = SegmentationModule.getLabelmapUIDsForElement(element) - const activeLabelmapUID = - activeLabelmapController.getActiveLabelmapUID(element) + const segmentationUIDs = + SegmentationModule.getsegmentationUIDsForElement(element) + const activesegmentationUID = + activeSegmentationController.getActivesegmentationUID(element) this.setState({ - availableLabelmaps: labelmapUIDs, - selectedLabelmapUID: activeLabelmapUID, + availableSegmentations: segmentationUIDs, + selectedsegmentationUID: activesegmentationUID, }) } - createNewLabelmapForScissors = async (evt) => { - const viewportUID = evt.target.value + createNewLabelmapForScissors = async () => { + const toolGroup = this.state.toolGroups[this.state.selectedToolGroupName] + + const { viewportsInfo } = toolGroup + const { viewportUID, renderingEngineUID } = viewportsInfo[0] const viewport = this.renderingEngine.getViewport(viewportUID) - await SegmentationModule.addEmptySegmentationVolumeForViewport(viewport) + + SegmentationModule.createNewSegmentationForViewport(viewport).then( + (segmentationUID) => { + addSegmentationsForToolGroup(this.state.selectedToolGroupName, [ + { + volumeUID: segmentationUID, + // default representation which is labelmap + }, + ]) + } + ) } setToolMode = (toolMode) => { const toolName = this.state.ptCtLeftClickTool + + const toolGroups = this.state.toolGroups + if (SEGMENTATION_TOOLS.includes(toolName)) { this.setState({ segmentationToolActive: true }) } - const toolGroup = this.state.toolGroups[this.state.activeViewportUID] + const toolGroup = toolGroups[this.state.selectedToolGroupName] if (toolMode === ToolModes.Active) { const activeTool = toolGroup.getActivePrimaryButtonTools() if (activeTool) { @@ -525,75 +547,103 @@ class SegmentationExample extends Component { this.setState({ segmentationStatus: 'done' }) } - loadSegmentation = async (viewportUID, labelmapUID) => { - const { element } = this.renderingEngine.getViewport(viewportUID) - - const labelmapIndex = activeLabelmapController.getNextLabelmapIndex(element) - const labelmap = cache.getVolume(labelmapUID) - - await SegmentationModule.setLabelmapForElement({ - element, - labelmap, - labelmapIndex, - }) + loadSegmentation = async (segmentationUID, initialConfig) => { + const toolGroupUID = this.state.selectedToolGroupName - const activeSegmentIndex = - segmentIndexController.getActiveSegmentIndex(element) - const segmentLocked = - lockedSegmentController.getSegmentIndexLockedStatusForElement( - element, - activeSegmentIndex + if (!initialConfig) { + await addSegmentationsForToolGroup(toolGroupUID, [ + { + volumeUID: segmentationUID, + active: true, + representation: { + type: SegmentationRepresentations.Labelmap, + }, + }, + ]) + } else { + await addSegmentationsForToolGroup( + toolGroupUID, + [ + { + volumeUID: segmentationUID, + active: true, + representation: { + type: SegmentationRepresentations.Labelmap, + }, + }, + ], + { + representations: { + [SegmentationRepresentations.Labelmap]: { + renderOutline: false, + }, + }, + } ) + } this.setState({ segmentationToolActive: true, - selectedLabelmapUID: - activeLabelmapController.getActiveLabelmapUID(element), - activeSegmentIndex, - segmentLocked, }) } toggleLockedSegmentIndex = (evt) => { const checked = evt.target.checked - // Todo: Don't have active viewport concept - const viewportUID = this.state.activeViewportUID - const { element } = this.renderingEngine.getViewport(viewportUID) - const activeLabelmapUID = - activeLabelmapController.getActiveLabelmapUID(element) - lockedSegmentController.toggleSegmentIndexLockedForLabelmapUID( - activeLabelmapUID, - this.state.activeSegmentIndex + + const activesegmentationInfo = + activeSegmentationController.getActiveSegmentationInfo( + this.state.selectedToolGroupName + ) + + const { volumeUID, activeSegmentIndex } = activesegmentationInfo + + const activeSegmentLockedStatus = + lockedSegmentController.getSegmentIndexLockedStatusForSegmentation( + volumeUID, + activeSegmentIndex + ) + + lockedSegmentController.setSegmentIndexLockedStatusForSegmentation( + volumeUID, + activeSegmentIndex, + !activeSegmentLockedStatus ) - this.setState({ segmentLocked: checked }) + + this.setState({ segmentLocked: !activeSegmentLockedStatus }) } changeActiveSegmentIndex = (direction) => { - // Todo: Don't have active viewport concept - const viewportUID = this.state.activeViewportUID - const { element } = this.renderingEngine.getViewport(viewportUID) - const currentIndex = segmentIndexController.getActiveSegmentIndex(element) - let newIndex = currentIndex + direction + const toolGroupUID = this.state.selectedToolGroupName + const activeSegmentationInfo = + activeSegmentationController.getActiveSegmentationInfo(toolGroupUID) + + const { activeSegmentIndex } = activeSegmentationInfo + let newIndex = activeSegmentIndex + direction if (newIndex < 0) { newIndex = 0 } - segmentIndexController.setActiveSegmentIndex(element, newIndex) - const segmentLocked = - lockedSegmentController.getSegmentIndexLockedStatusForElement( - element, - newIndex - ) - this.setState({ activeSegmentIndex: newIndex, segmentLocked }) + segmentIndexController.setActiveSegmentIndex(toolGroupUID, newIndex) + + const segmentIsLocked = lockedSegmentController.getSegmentIndexLockedStatus( + toolGroupUID, + newIndex + ) + + this.setState({ + selectedViewportActiveSegmentIndex: newIndex, + segmentLocked: segmentIsLocked, + }) } calculateTMTV = () => { - const viewportUID = this.state.activeViewportUID + const viewportUID = this.state.selectedToolGroupName const { element } = this.renderingEngine.getViewport(viewportUID) - const labelmapUIDs = SegmentationModule.getLabelmapUIDsForElement(element) + const segmentationUIDs = + SegmentationModule.getsegmentationUIDsForElement(element) - const labelmaps = labelmapUIDs.map((uid) => cache.getVolume(uid)) + const labelmaps = segmentationUIDs.map((uid) => cache.getVolume(uid)) const segmentationIndex = 1 const tmtv = csToolsUtils.segmentation.calculateTMTV( labelmaps, @@ -606,17 +656,17 @@ class SegmentationExample extends Component { } calculateSuvPeak = () => { - const viewportUID = this.state.activeViewportUID + const viewportUID = this.state.selectedToolGroupName const viewport = this.renderingEngine.getViewport(viewportUID) const { uid } = viewport.getDefaultActor() const referenceVolume = cache.getVolume(uid) - const labelmapUIDs = SegmentationModule.getLabelmapUIDsForElement( + const segmentationUIDs = SegmentationModule.getsegmentationUIDsForElement( viewport.element ) - const labelmaps = labelmapUIDs.map((uid) => cache.getVolume(uid)) + const labelmaps = segmentationUIDs.map((uid) => cache.getVolume(uid)) const segmentationIndex = 1 const suvPeak = csToolsUtils.segmentation.calculateSuvPeak( viewport, @@ -627,15 +677,42 @@ class SegmentationExample extends Component { } setEndSlice = () => { - const viewportUID = this.state.activeViewportUID - const viewport = this.renderingEngine.getViewport(viewportUID) + if (this.state.ptCtLeftClickTool !== RECTANGLE_ROI_THRESHOLD_MANUAL) { + throw new Error('cannot apply start slice') + } - const selectedToolDataList = - toolDataSelection.getSelectedToolDataByToolName( - RECTANGLE_ROI_THRESHOLD_MANUAL - ) + let toolData = toolDataSelection.getSelectedToolDataByToolName( + this.state.ptCtLeftClickTool + ) + + if (!toolData) { + throw new Error('No annotation selected ') + } - const toolData = selectedToolDataList[0] + toolData = toolData[0] + + const { + metadata: { + enabledElement: { viewport }, + }, + } = toolData // assuming they are all overlayed on the same toolGroup + + const volumeActorInfo = viewport.getDefaultActor() + + // Todo: this only works for volumeViewport + const { uid } = volumeActorInfo + + const segmentationData = getSegmentationDataByUID( + this.state.selectedToolGroupName, + this.state.selectedsegmentationUID + ) + const globalState = getGlobalSegmentationDataByUID( + segmentationData.volumeUID + ) + + if (!globalState) { + throw new Error('No Segmentation Found') + } // get the current slice Index const sliceIndex = viewport.getCurrentImageIdIndex() @@ -646,17 +723,33 @@ class SegmentationExample extends Component { } setStartSlice = () => { - const viewportUID = this.state.activeViewportUID - const viewport = this.renderingEngine.getViewport(viewportUID) + if (this.state.ptCtLeftClickTool !== RECTANGLE_ROI_THRESHOLD_MANUAL) { + throw new Error('cannot apply start slice') + } + + let toolData = toolDataSelection.getSelectedToolDataByToolName( + this.state.ptCtLeftClickTool + ) + + if (!toolData) { + throw new Error('No annotation selected ') + } + + toolData = toolData[0] + + const { + metadata: { + enabledElement: { viewport }, + }, + } = toolData // assuming they are all overlayed on the same toolGroup const { focalPoint, viewPlaneNormal } = viewport.getCamera() const selectedToolDataList = toolDataSelection.getSelectedToolDataByToolName( - RECTANGLE_ROI_THRESHOLD_MANUAL + this.state.ptCtLeftClickTool ) - const toolData = selectedToolDataList[0] const { handles } = toolData.data const { points } = handles @@ -683,23 +776,51 @@ class SegmentationExample extends Component { viewport.render() } - executeThresholding = (mode, activeTool) => { - const ptVolume = cache.getVolume(ptVolumeUID) - const labelmapVolume = cache.getVolume(this.state.selectedLabelmapUID) + executeThresholding = (mode) => { + let toolData = toolDataSelection.getSelectedToolDataByToolName( + this.state.ptCtLeftClickTool + ) + + if (!toolData) { + throw new Error('No annotation selected ') + } + + toolData = toolData[0] + + const { + metadata: { + enabledElement: { viewport }, + }, + } = toolData // assuming they are all overlayed on the same toolGroup + + const volumeActorInfo = viewport.getDefaultActor() + + // Todo: this only works for volumeViewport + const { uid } = volumeActorInfo + const referenceVolume = cache.getVolume(uid) + + const segmentationData = getSegmentationDataByUID( + this.state.selectedToolGroupName, + this.state.selectedsegmentationUID + ) + const numSlices = this.state.numSlicesForThreshold const selectedToolDataList = - toolDataSelection.getSelectedToolDataByToolName(activeTool) + toolDataSelection.getSelectedToolDataByToolName( + this.state.ptCtLeftClickTool + ) if (mode === 'max') { csToolsUtils.segmentation.thresholdVolumeByRoiStats( + this.state.selectedToolGroupName, selectedToolDataList, - [ptVolume], - labelmapVolume, + [referenceVolume], + segmentationData, { statistic: 'max', weight: 0.41, numSlicesToProject: numSlices, - overwrite: true, + overwrite: false, } ) @@ -707,14 +828,15 @@ class SegmentationExample extends Component { } csToolsUtils.segmentation.thresholdVolumeByRange( + this.state.selectedToolGroupName, selectedToolDataList, - [ptVolume], - labelmapVolume, + [referenceVolume], + segmentationData, { lowerThreshold: Number(this.state.thresholdMin), higherThreshold: Number(this.state.thresholdMax), numSlicesToProject: numSlices, - overwrite: true, + overwrite: false, } ) } @@ -791,17 +913,13 @@ class SegmentationExample extends Component { /> @@ -828,42 +946,55 @@ class SegmentationExample extends Component { return ( <> ) } - deleteLabelmap = () => { - const labelmapUID = this.state.selectedLabelmapUID - const { element } = this.renderingEngine.getViewport( - this.state.activeViewportUID + deleteSegmentation = () => { + const segmentationDataUID = this.state.selectedsegmentationUID + removeSegmentationsForToolGroup(this.state.selectedToolGroupName, [ + segmentationDataUID, + ]) + } + + toggleSegmentationVisibility = (segmentDataUID) => { + const visibilityStatus = + segmentationVisibilityController.getSegmentationVisibility( + this.state.selectedToolGroupName, + segmentDataUID + ) + + segmentationVisibilityController.setSegmentationVisibility( + this.state.selectedToolGroupName, + segmentDataUID, + !visibilityStatus ) - SegmentationModule.removeLabelmapForElement(element, labelmapUID) + } - this.renderingEngine.render() + hideAllSegmentations = () => { + this.state.toolGroups[this.state.selectedToolGroupName].setToolDisabled( + 'SegmentationDisplay' + ) } - hideSegmentation = (segmentUID) => { - const viewportUID = this.state.activeViewportUID - const { element } = this.renderingEngine.getViewport(viewportUID) - hideSegmentController.toggleSegmentationVisibility(element, segmentUID) + showAllSegmentations = () => { + this.state.toolGroups[this.state.selectedToolGroupName].setToolEnabled( + 'SegmentationDisplay' + ) } getToolStrategyUI = () => { - const toolGroup = this.state.toolGroups[this.state.activeViewportUID] + const toolGroup = this.state.toolGroups[this.state.selectedToolGroupName] if (!toolGroup) { return null } - const toolInstance = toolGroup._toolInstances[this.state.ptCtLeftClickTool] + const toolInstance = toolGroup.getToolInstance(this.state.ptCtLeftClickTool) if (!toolInstance) { return @@ -903,52 +1034,62 @@ class SegmentationExample extends Component { Strategies - for this viewport + for this toolGroup @@ -987,8 +1128,8 @@ class SegmentationExample extends Component {

Segmentation Example ({this.state.progressText})

- For Segmentation tools, you need to click on "Create New Labelmap" - button (when the options appear) + For Segmentation tools, you need to click on "Create New + Segmentation" button (when the options appear)

{!window.crossOriginIsolated ? (

@@ -1005,64 +1146,53 @@ class SegmentationExample extends Component { {this.state.ptCtLeftClickTool.includes('RectangleRoi') ? this.getThresholdUID() : this.getScissorsUI()} - All segmentations + All global segmentations
- Viewport Segmentations (active is selected) + + {' '} + ToolGroup Segmentations (selected is active Segmentation){' '} + + )}
-

Segmentation Rendering Config

+

ToolGroup-specific Labelmap Config

{ - const renderOutline = !this.state.renderOutline - SegmentationModule.setGlobalConfig({ renderOutline }) + const renderOutline = !this.state.renderOutlineToolGroup + this.setState({ - renderOutline, + renderOutlineToolGroup: renderOutline, }) }} /> @@ -1213,13 +1341,14 @@ class SegmentationExample extends Component { type="checkbox" style={{ marginLeft: '10px' }} name="toggle" - defaultChecked={this.state.renderInactiveLabelmaps} + defaultChecked={this.state.renderInactiveSegmentationsToolGroup} onChange={() => { - const renderInactiveLabelmaps = - !this.state.renderInactiveLabelmaps - SegmentationModule.setGlobalConfig({ renderInactiveLabelmaps }) + const renderInactiveSegmentations = + !this.state.renderInactiveSegmentationsToolGroup + this.setState({ - renderInactiveLabelmaps, + renderInactiveSegmentationsToolGroup: + renderInactiveSegmentations, }) }} /> @@ -1234,14 +1363,16 @@ class SegmentationExample extends Component { type="range" id="fillAlpha" name="fillAlpha" - value={this.state.fillAlpha} + value={this.state.fillAlphaToolGroup} min="0.8" max="0.999" step="0.001" onChange={(evt) => { const fillAlpha = Number(evt.target.value) - this.setState({ fillAlpha }) - SegmentationModule.setGlobalConfig({ fillAlpha }) + const representationType = + SegmentationRepresentations.Labelmap + + this.setState({ fillAlphaToolGroup: fillAlpha }) }} />
@@ -1252,37 +1383,171 @@ class SegmentationExample extends Component { type="range" id="fillAlphaInactive" name="fillAlphaInactive" - value={this.state.fillAlphaInactive} + value={this.state.fillAlphaInactiveToolGroup} min="0.8" max="0.999" step="0.001" onChange={(evt) => { const fillAlphaInactive = Number(evt.target.value) - this.setState({ fillAlphaInactive }) - SegmentationModule.setGlobalConfig({ fillAlphaInactive }) + this.setState({ + fillAlphaInactiveToolGroup: fillAlphaInactive, + }) }} />

-
- - + +
+ +
+

Global Labelmap Config

+ { + const renderOutline = !this.state.renderOutlineGlobal + + const representationType = SegmentationRepresentations.Labelmap + segmentationConfigController.updateGlobalRepresentationConfig( + representationType, + { renderOutline } + ) + + this.setState({ + renderOutlineGlobal: renderOutline, + }) + }} + /> + + { + const renderInactiveSegmentations = + !this.state.renderInactiveSegmentationsGlobal + + segmentationConfigController.updateGlobalSegmentationConfig({ + renderInactiveSegmentations, + }) + + this.setState({ + renderInactiveSegmentationsGlobal: + renderInactiveSegmentations, + }) + }} + /> + +
+
+ + { + const fillAlpha = Number(evt.target.value) + const representationType = + SegmentationRepresentations.Labelmap + + segmentationConfigController.updateGlobalRepresentationConfig( + representationType, + { + fillAlpha, + } + ) + this.setState({ fillAlphaGlobal: fillAlpha }) + }} + /> +
+
+ + { + const fillAlphaInactive = Number(evt.target.value) + const representationType = + SegmentationRepresentations.Labelmap + + segmentationConfigController.updateGlobalRepresentationConfig( + representationType, + { + fillAlphaInactive, + } + ) + this.setState({ + fillAlphaInactiveGlobal: fillAlphaInactive, + }) + }} + />
+
+ + + +