From 9ed44254c21455ec14b4ee0019784d4d73bd9cd3 Mon Sep 17 00:00:00 2001 From: Alireza Date: Mon, 7 Feb 2022 12:49:06 -0500 Subject: [PATCH] feat: cpu fallback for rendering stack viewports (#315) * Stub out all paths and move things around to get us set up for CPU rendering * Basic skeleton framework set up for cpu fallback * WIP seperate enableing/resizing for non VTK driven vps * Feat/dev cpu fallback resizing (#296) * WIP splitting up viewports * First itteration seems solid * Make suggested changes * Feat/dev cpu fallback cs2d pipeline (#297) * WIP splitting up viewports * First itteration seems solid * Grayscale pipeline working for default image * feat: Add world to canvas and imageData computations for CPU fallback (#298) * feat: add world2canvas and canvas2world * renamed files from js to ts * feat: make annotation tool work for length * feat: make other annotation tools work Co-authored-by: Alireza * feat: Add detect webgl context (#294) * feat: Add detect webgl context * feat: add init for cs-render * remaining files * fix: demos with init * feat: add manually setting cpu rendering * feat: add set and get methods to init * Feat/dev cpu fallback rgb (#299) * WIP color stack * Fix web image loaders usage with the StackViewport * Get RGB images rendering using CPU Fallback * WIP pseudocolor * Working false color * remove all layers stuff * Add remaining file which I didn't save Co-authored-by: Alireza * Fix/annotations cpu (#300) * fix: tests for initialize cornerstone and vtkimagedata change * yarn lock * fix: worldToCanvas error for pixel calculation (#301) * Feat/cpu maniputlation canvas (#302) * fix: performance issue with cpu getImageData * feat: breaks setProperties into cpu and gpu implementation * wip for refactoring window level * feat: Add window level to cpu * feat: Add rotation and invert * feat: Add reset properties * feat: Add zoom tool for cpu * feat: Add pan tool for cpu * fix: pan and zoom tool sensitivity * fix: worldToCanvas for cpu * feat: fix interoplationType * fix: maintain viewport on same stack only * removed unnecessary flag for camera * trying to fix the ci while not breaking dcmjs * Revert new properties added to ICamera and update adapter to work with existing interface Co-authored-by: James A. Petts * Split example and WIP cleanup types (#303) * Cleanup rendering engine (#304) * bump minor versions (#305) * Feat/cpu fallback tests (#306) * renamed files * feat: add cpu rendering tests and promisify stackViewport * fix: windowlevel bug for volumeUID * feat: Add tests for cornerstoneTools and cpu * feat: Add flip to cpu viewport * feat: Add camera resize * feat: Add camera reset * fix: tests * apply review comments * apply review comments * feat: Add resetPanZoom for resetCamera for cpu * stack viewport set properties api * fix: rotation and interpolation * fix: horizontal flip for cpu * fix: horizontal flip for gpu * fix: tests for stack viewport * Feat/dev cpu fallback build (#309) * fix various build errors * fix default voi * finished types * fix: tests * Fix/gpu flip (#310) * work in progress * fix: fixed transformation for flipTx * feat: Add tests for gpu flip * fix: build error * fix: voluem viewport flip * apply review comments * PET rendering with CPU fallback using LUT function instead of LUT table (#308) * Sets PET WL to 0-5 SUV, adds PET to list on CPU stack viewport fallback * Super simple POC for rendering images * Add todo comments for alireza * Add comments * remaining fixes from rebase * fix: petThreshold tool for window leveling * fix: probe tool for scaling pt cpu * fix: demo to reset tool with different stack * feat: add invert for pt cpu * feat: add false colormap to pt * apply review comments * fix tests Co-authored-by: James A. Petts * feat: add detect-gpu to init (#311) * feat: add detect-gpu to init * reducing the tier for testing * fix tests * fix: remove offCanvas div in cpu demo * bump docusaurus version * fix docusaurus broken links Co-authored-by: James A. Petts --- package.json | 97 +- .../src/StreamingImageVolume.ts | 4 +- .../src/registerWebImageLoader.ts | 11 +- .../test/StreamingImageVolume_test.js | 465 +- packages/cornerstone-render/package.json | 6 +- .../src/RenderingEngine/RenderingEngine.ts | 941 ++- .../src/RenderingEngine/StackViewport.ts | 1187 ++- .../src/RenderingEngine/Viewport.ts | 270 +- .../src/RenderingEngine/VolumeViewport.ts | 68 +- .../helpers/cpuFallback/colors/colormap.ts | 343 + .../helpers/cpuFallback/colors/colormaps.ts | 1537 ++++ .../helpers/cpuFallback/colors/index.ts | 12 + .../helpers/cpuFallback/colors/lookupTable.ts | 469 ++ .../helpers/cpuFallback/drawImageSync.ts | 61 + .../rendering/calculateTransform.ts | 137 + .../cpuFallback/rendering/canvasToPixel.ts | 26 + .../cpuFallback/rendering/computeAutoVoi.ts | 50 + .../cpuFallback/rendering/createViewport.ts | 64 + .../rendering/doesImageNeedToBeRendered.ts | 37 + .../cpuFallback/rendering/fitToWindow.ts | 23 + .../cpuFallback/rendering/generateColorLut.ts | 61 + .../cpuFallback/rendering/generateLut.ts | 62 + .../rendering/getDefaultViewport.ts | 86 + .../cpuFallback/rendering/getImageFitScale.ts | 53 + .../cpuFallback/rendering/getImageSize.ts | 55 + .../helpers/cpuFallback/rendering/getLut.ts | 53 + .../cpuFallback/rendering/getModalityLUT.ts | 55 + .../cpuFallback/rendering/getTransform.ts | 17 + .../cpuFallback/rendering/getVOILut.ts | 74 + .../rendering/initializeRenderCanvas.ts | 37 + .../cpuFallback/rendering/lutMatches.ts | 21 + .../helpers/cpuFallback/rendering/now.ts | 13 + .../cpuFallback/rendering/pixelToCanvas.ts | 22 + .../cpuFallback/rendering/renderColorImage.ts | 193 + .../rendering/renderGrayscaleImage.ts | 166 + .../rendering/renderPseudoColorImage.ts | 203 + .../cpuFallback/rendering/resetCamera.ts | 29 + .../helpers/cpuFallback/rendering/resize.ts | 109 + .../cpuFallback/rendering/saveLastRendered.ts | 36 + .../rendering/setDefaultViewport.ts | 17 + .../rendering/setToPixelCoordinateSystem.ts | 32 + .../storedColorPixelDataToCanvasImageData.ts | 58 + .../storedPixelDataToCanvasImageData.ts | 76 + ...toredPixelDataToCanvasImageDataColorLUT.ts | 60 + .../storedPixelDataToCanvasImageDataPET.ts | 50 + ...ixelDataToCanvasImageDataPseudocolorLUT.ts | 66 + ...lDataToCanvasImageDataPseudocolorLUTPET.ts | 68 + .../storedPixelDataToCanvasImageDataRGBA.ts | 81 + .../storedRGBAPixelDataToCanvasImageData.ts | 56 + .../cpuFallback/rendering/transform.ts | 126 + .../cpuFallback/rendering/validator.ts | 31 + .../helpers/createVolumeActor.ts | 4 +- .../helpers/createVolumeMapper.ts | 6 +- .../helpers/viewportTypeToViewportClass.ts | 12 + ...viewportTypeUsesCustomRenderingPipeline.ts | 7 + .../src/cache/classes/ImageVolume.ts | 4 +- .../src/enums/flipDirection.ts | 7 - packages/cornerstone-render/src/index.ts | 17 +- packages/cornerstone-render/src/init.ts | 76 + .../src/types/CPUFallbackColormap.ts | 22 + .../src/types/CPUFallbackColormapData.ts | 12 + .../src/types/CPUFallbackColormapsData.ts | 7 + .../src/types/CPUFallbackEnabledElement.ts | 68 + .../src/types/CPUFallbackLUT.ts | 5 + .../src/types/CPUFallbackLookupTable.ts | 17 + .../src/types/CPUFallbackRenderingTools.ts | 33 + .../src/types/CPUFallbackTransform.ts | 16 + .../src/types/CPUFallbackViewport.ts | 27 + .../types/CPUFallbackViewportDisplayedArea.ts | 15 + .../src/types/CPUIImageData.ts | 20 + .../src/types/FlipDirection.ts | 7 + .../cornerstone-render/src/types/IImage.ts | 31 +- .../src/types/IImageData.ts | 6 +- .../src/types/IImageVolume.ts | 2 +- .../cornerstone-render/src/types/IViewport.ts | 2 - .../cornerstone-render/src/types/IVolume.ts | 2 +- .../cornerstone-render/src/types/Point4.ts | 6 + .../src/types/StackProperties.ts | 2 + .../src/types/TransformMatrix2D.ts | 3 + .../cornerstone-render/src/types/index.ts | 31 + .../src/utilities/getRuntimeId.ts | 1 + .../cornerstone-render/src/utilities/index.ts | 2 + .../src/utilities/testUtils.js | 27 +- .../src/utilities/windowLevel.ts | 30 + .../cornerstone-render/src/volumeLoader.ts | 6 +- .../test/RenderingEngineAPI_test.js | 506 +- .../cornerstone-render/test/cache_test.js | 1099 +-- ...ibrated_1_5_imageURI_11_11_4_1_1_1_0_1.png | Bin 2248 -> 3027 bytes .../cpu_imageURI_256_256_100_100_1_1_0.png | Bin 0 -> 2995 bytes ...imageURI_256_256_100_100_1_1_0_hotIron.png | Bin 0 -> 3094 bytes ...cpu_imageURI_256_256_100_100_1_1_0_voi.png | Bin 0 -> 2998 bytes .../cpu_imageURI_256_256_50_10_1_1_0.png | Bin 0 -> 2996 bytes ...pu_imageURI_256_256_50_10_1_1_0_invert.png | Bin 0 -> 3120 bytes ...pu_imageURI_256_256_50_10_1_1_0_rotate.png | Bin 0 -> 2680 bytes .../cpu_imageURI_64_33_20_5_1_1_0.png | Bin 0 -> 3064 bytes .../cpu_imageURI_64_64_0_10_5_5_0.png | Bin 0 -> 3097 bytes .../cpu_imageURI_64_64_20_5_1_1_0.png | Bin 0 -> 9251 bytes .../cpu_imageURI_64_64_30_10_5_5_0.png | Bin 0 -> 3118 bytes .../cpu_imageURI_64_64_54_10_5_5_0.png | Bin 0 -> 3100 bytes ...ageURI_100_100_0_10_1_1_1_linear_color.png | Bin 13246 -> 7893 bytes .../groundTruth/imageURI_11_11_4_1_1_1_0.png | Bin 2553 -> 2937 bytes ...I_11_11_4_1_1_1_0_nearest_invert_90deg.png | Bin 1689 -> 2774 bytes .../imageURI_256_256_50_10_1_1_0.png | Bin 2241 -> 2694 bytes ...imageURI_64_64_20_5_1_1_0_nearestFlipH.png | Bin 0 -> 3021 bytes ..._64_64_20_5_1_1_0_nearestFlipHRotate90.png | Bin 0 -> 3066 bytes .../test/groundTruth/test.png | Bin 2393 -> 0 bytes .../test/imageLoader_test.js | 245 +- .../test/metaDataProvider_test.js | 5 + .../test/renderingCore_stack_test.js | 749 -- .../test/renderingCore_volume_test.js | 639 -- .../test/stackViewport_cpu_render_test.js | 457 ++ .../test/stackViewport_gpu_render_test.js | 927 +++ .../test/volumeViewport_gpu_render_test.js | 646 ++ .../src/cursors/MouseCursor.ts | 2 + .../src/tools/MIPJumpToClickTool.ts | 8 +- .../src/tools/PetThresholdTool.ts | 52 +- .../src/tools/WindowLevelTool.ts | 113 +- .../cornerstone-tools/src/tools/ZoomTool.ts | 5 +- .../src/tools/annotation/BidirectionalTool.ts | 15 +- .../src/tools/annotation/EllipticalRoiTool.ts | 28 +- .../src/tools/annotation/LengthTool.ts | 9 +- .../src/tools/annotation/ProbeTool.ts | 12 +- .../src/tools/annotation/RectangleRoiTool.ts | 16 +- .../getVoxelPositionBasedOnIntensity.ts | 4 +- .../test/BidirectionalTool_test.js | 49 +- .../test/CrosshairsTool_test.js | 31 +- .../cornerstone-tools/test/EllipseROI_test.js | 762 +- ...OfReferenceSpecificToolStateManger_test.js | 2 + .../cornerstone-tools/test/LengthTool_test.js | 1878 ++--- .../cornerstone-tools/test/ProbeTool_test.js | 1510 ++-- .../test/RectangleROI_test.js | 1745 ++-- .../StackScrollToolMouseWheelTool_test.js | 25 +- .../test/ToolGroupManager_test.js | 536 +- .../test/cpu_BidirectionalTool_test.js | 848 ++ .../test/cpu_EllipseROI_test.js | 363 + .../test/cpu_LengthTool_test.js | 718 ++ .../test/cpu_ProbeTool_test.js | 766 ++ .../test/cpu_RectangleROI_test.js | 927 +++ .../test/synchronizerManager_test.js | 529 +- packages/demo/package.json | 19 +- packages/demo/src/App.tsx | 25 +- packages/demo/src/ExampleCacheDecache.tsx | 6 +- packages/demo/src/ExampleCalibration.tsx | 16 +- packages/demo/src/ExampleCanvasResize.tsx | 78 +- packages/demo/src/ExampleColor.tsx | 42 +- packages/demo/src/ExampleEnableDisableAPI.tsx | 20 +- packages/demo/src/ExampleFlipViewport.tsx | 74 +- packages/demo/src/ExampleModifierKeys.tsx | 4 +- .../demo/src/ExampleNineStackViewport.tsx | 8 +- packages/demo/src/ExampleOneStack.tsx | 151 +- packages/demo/src/ExampleOneStackCPU.tsx | 508 ++ packages/demo/src/ExampleOneVolume.tsx | 33 +- packages/demo/src/ExamplePriorityLoad.tsx | 8 +- packages/demo/src/ExampleSetVolumes.tsx | 8 +- packages/demo/src/ExampleStackViewport.tsx | 15 +- packages/demo/src/ExampleTestUtils.tsx | 31 +- packages/demo/src/ExampleTestUtilsVolume.tsx | 26 +- .../src/ExampleToolDisplayConfiguration.tsx | 30 +- packages/demo/src/ExampleTwentyFiveCanvas.tsx | 87 +- packages/demo/src/ExampleVTKMPR.tsx | 10 +- .../helpers/createImageIdsAndCacheMetaData.js | 16 +- .../docs/docs/concepts/renderingEngine.md | 5 +- packages/docs/docusaurus.config.js | 2 +- packages/docs/package.json | 18 +- yarn.lock | 7263 +++++++++-------- 165 files changed, 22497 insertions(+), 10639 deletions(-) create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/colormap.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/colormaps.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/index.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/lookupTable.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/drawImageSync.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/calculateTransform.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/canvasToPixel.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/computeAutoVoi.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/createViewport.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/doesImageNeedToBeRendered.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/fitToWindow.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/generateColorLut.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/generateLut.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getDefaultViewport.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getImageFitScale.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getImageSize.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getLut.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getModalityLUT.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getTransform.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getVOILut.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/initializeRenderCanvas.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/lutMatches.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/now.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/pixelToCanvas.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderColorImage.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderGrayscaleImage.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderPseudoColorImage.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/resetCamera.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/resize.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/saveLastRendered.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/setDefaultViewport.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/setToPixelCoordinateSystem.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedColorPixelDataToCanvasImageData.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageData.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataColorLUT.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPET.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPseudocolorLUT.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPseudocolorLUTPET.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataRGBA.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedRGBAPixelDataToCanvasImageData.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/transform.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/validator.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/viewportTypeToViewportClass.ts create mode 100644 packages/cornerstone-render/src/RenderingEngine/helpers/viewportTypeUsesCustomRenderingPipeline.ts delete mode 100644 packages/cornerstone-render/src/enums/flipDirection.ts create mode 100644 packages/cornerstone-render/src/init.ts create mode 100644 packages/cornerstone-render/src/types/CPUFallbackColormap.ts create mode 100644 packages/cornerstone-render/src/types/CPUFallbackColormapData.ts create mode 100644 packages/cornerstone-render/src/types/CPUFallbackColormapsData.ts create mode 100644 packages/cornerstone-render/src/types/CPUFallbackEnabledElement.ts create mode 100644 packages/cornerstone-render/src/types/CPUFallbackLUT.ts create mode 100644 packages/cornerstone-render/src/types/CPUFallbackLookupTable.ts create mode 100644 packages/cornerstone-render/src/types/CPUFallbackRenderingTools.ts create mode 100644 packages/cornerstone-render/src/types/CPUFallbackTransform.ts create mode 100644 packages/cornerstone-render/src/types/CPUFallbackViewport.ts create mode 100644 packages/cornerstone-render/src/types/CPUFallbackViewportDisplayedArea.ts create mode 100644 packages/cornerstone-render/src/types/CPUIImageData.ts create mode 100644 packages/cornerstone-render/src/types/FlipDirection.ts create mode 100644 packages/cornerstone-render/src/types/Point4.ts create mode 100644 packages/cornerstone-render/src/types/TransformMatrix2D.ts create mode 100644 packages/cornerstone-render/src/utilities/windowLevel.ts create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_100_100_1_1_0.png create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_100_100_1_1_0_hotIron.png create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_100_100_1_1_0_voi.png create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_50_10_1_1_0.png create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_50_10_1_1_0_invert.png create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_50_10_1_1_0_rotate.png create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_33_20_5_1_1_0.png create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_64_0_10_5_5_0.png create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_64_20_5_1_1_0.png create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_64_30_10_5_5_0.png create mode 100644 packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_64_54_10_5_5_0.png create mode 100644 packages/cornerstone-render/test/groundTruth/imageURI_64_64_20_5_1_1_0_nearestFlipH.png create mode 100644 packages/cornerstone-render/test/groundTruth/imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90.png delete mode 100644 packages/cornerstone-render/test/groundTruth/test.png delete mode 100644 packages/cornerstone-render/test/renderingCore_stack_test.js delete mode 100644 packages/cornerstone-render/test/renderingCore_volume_test.js create mode 100644 packages/cornerstone-render/test/stackViewport_cpu_render_test.js create mode 100644 packages/cornerstone-render/test/stackViewport_gpu_render_test.js create mode 100644 packages/cornerstone-render/test/volumeViewport_gpu_render_test.js create mode 100644 packages/cornerstone-tools/test/cpu_BidirectionalTool_test.js create mode 100644 packages/cornerstone-tools/test/cpu_EllipseROI_test.js create mode 100644 packages/cornerstone-tools/test/cpu_LengthTool_test.js create mode 100644 packages/cornerstone-tools/test/cpu_ProbeTool_test.js create mode 100644 packages/cornerstone-tools/test/cpu_RectangleROI_test.js create mode 100644 packages/demo/src/ExampleOneStackCPU.tsx diff --git a/package.json b/package.json index c1af20d85b..b161d7287f 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,10 @@ "docs:watch": "lerna run docs:watch", "preinstall": "node preinstall.js", "start": "yarn run dev", - "test": "NODE_ENV=test karma start", "test:firefox": "karma start ./karma.conf.js --browsers Firefox", + "test:dev": "karma start", "test:ci": "karma start --single-run", + "test": "karma start", "lint-staged": "lint-staged", "lint": "eslint --quiet -c .eslintrc.json packages/**/src", "predeploy": "yarn install && yarn run build:release", @@ -30,75 +31,75 @@ "lodash.isequal": "^4.5.0" }, "devDependencies": { - "@babel/core": "^7.13.10", - "@babel/plugin-external-helpers": "^7.12.13", - "@babel/plugin-proposal-class-properties": "^7.13.0", - "@babel/plugin-transform-runtime": "^7.13.10", - "@babel/preset-env": "^7.13.12", - "@babel/preset-react": "^7.12.13", - "@babel/preset-typescript": "^7.13.0", - "@babel/runtime": "^7.13.10", - "@types/node": "^14.14.36", - "@types/react": "^17.0.3", - "@types/react-dom": "^17.0.3", - "@typescript-eslint/eslint-plugin": "^4.19.0", - "@typescript-eslint/parser": "^4.19.0", - "autoprefixer": "^10.2.5", - "babel-loader": "8.2.2", - "babel-plugin-istanbul": "^6.0.0", + "@babel/core": "^7.16.12", + "@babel/plugin-external-helpers": "^7.16.7", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-transform-runtime": "^7.16.10", + "@babel/preset-env": "^7.16.11", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.16.7", + "@babel/runtime": "^7.16.7", + "@types/node": "^14.18.9", + "@types/react": "^17.0.38", + "@types/react-dom": "^17.0.11", + "@typescript-eslint/eslint-plugin": "^4.33.0", + "@typescript-eslint/parser": "^4.33.0", + "autoprefixer": "^10.4.2", + "babel-loader": "8.2.3", + "babel-plugin-istanbul": "^6.1.1", "calculate-suv": "git+ssh://git@github.com/PrecisionMetrics/calculate-suv.git#main", - "chai": "^4.3.4", + "chai": "^4.3.6", "clean-webpack-plugin": "^3.0.0", - "copy-webpack-plugin": "^8.1.0", + "copy-webpack-plugin": "^8.1.1", "cornerstone-wado-image-loader": "^3.3.2", "cross-env": "^7.0.3", - "css-loader": "^5.2.0", - "cssnano": "^4.1.10", - "eslint": "7.22.0", - "eslint-config-prettier": "^8.1.0", - "eslint-plugin-import": "^2.22.1", + "css-loader": "^5.2.7", + "cssnano": "^4.1.11", + "eslint": "7.32.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.25.4", "eslint-plugin-jsx-a11y": "6.x", - "eslint-plugin-prettier": "^3.3.1", - "eslint-plugin-tsdoc": "^0.2.11", - "eslint-webpack-plugin": "^2.5.3", + "eslint-plugin-prettier": "^3.4.1", + "eslint-plugin-tsdoc": "^0.2.14", + "eslint-webpack-plugin": "^2.6.0", "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.3.1", - "husky": "^4.3.6", + "html-webpack-plugin": "^5.5.0", + "husky": "^4.3.8", "istanbul-instrumenter-loader": "^3.0.1", - "jasmine": "^3.7.0", - "karma": "^6.3.2", + "jasmine": "^3.99.0", + "karma": "^6.3.12", "karma-chrome-launcher": "^3.1.0", - "karma-coverage": "^2.0.3", + "karma-coverage": "^2.1.0", "karma-coverage-istanbul-reporter": "^3.0.3", "karma-jasmine": "^4.0.1", "karma-junit-reporter": "^2.0.1", - "karma-spec-reporter": "0.0.32", + "karma-spec-reporter": "0.0.33", "karma-webpack": "^5.0.0", "lerna": "^4.0.0", "lint-staged": "^10.5.4", "path-browserify": "^1.0.1", - "postcss": "^8.2.8", - "postcss-import": "^14.0.0", - "postcss-loader": "^5.2.0", + "postcss": "^8.4.5", + "postcss-import": "^14.0.2", + "postcss-loader": "^5.3.0", "postcss-preset-env": "^6.7.0", - "prettier": "^2.2.1", - "prop-types": "^15.7.2", - "puppeteer": "^10.1.0", + "prettier": "^2.5.1", + "prop-types": "^15.8.1", + "puppeteer": "^10.4.0", "shader-loader": "^1.3.1", "sinon": "^10.0.0", "style-loader": "^2.0.0", - "stylelint": "^13.12.0", + "stylelint": "^13.13.1", "stylelint-config-recommended": "^4.0.0", - "ts-loader": "^9.1.2", - "typedoc": "^0.21.4", + "ts-loader": "^9.2.6", + "typedoc": "^0.22.11", "typedoc-plugin-pages-fork": "^0.0.1", - "typescript": "4.1.5", + "typescript": "4.5.5", "url-loader": "^4.1.1", - "webpack": "5.28.0", - "webpack-bundle-analyzer": "^4.4.0", - "webpack-cli": "^4.6.0", - "webpack-dev-server": "^3.11.2", - "webpack-merge": "5.7.3", + "webpack": "5.67.0", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^3.11.3", + "webpack-merge": "5.8.0", "worker-loader": "^3.0.8" }, "husky": { diff --git a/packages/cornerstone-image-loader-streaming-volume/src/StreamingImageVolume.ts b/packages/cornerstone-image-loader-streaming-volume/src/StreamingImageVolume.ts index 14df2c7396..96ff2616ab 100644 --- a/packages/cornerstone-image-loader-streaming-volume/src/StreamingImageVolume.ts +++ b/packages/cornerstone-image-loader-streaming-volume/src/StreamingImageVolume.ts @@ -176,7 +176,7 @@ export default class StreamingImageVolume extends ImageVolume { const { imageIds, vtkOpenGLTexture, - vtkImageData, + imageData, metadata, uid: volumeUID, } = this @@ -243,7 +243,7 @@ export default class StreamingImageVolume extends ImageVolume { framesProcessed++ vtkOpenGLTexture.setUpdatedFrame(imageIdIndex) - vtkImageData.modified() + imageData.modified() const eventData = { FrameOfReferenceUID, diff --git a/packages/cornerstone-image-loader-streaming-volume/src/registerWebImageLoader.ts b/packages/cornerstone-image-loader-streaming-volume/src/registerWebImageLoader.ts index 37b8c397b3..8b5aaabd97 100644 --- a/packages/cornerstone-image-loader-streaming-volume/src/registerWebImageLoader.ts +++ b/packages/cornerstone-image-loader-streaming-volume/src/registerWebImageLoader.ts @@ -73,8 +73,8 @@ function createImage(image, imageId) { width: columns, color: true, rgba: false, - columnPixelSpacing: undefined, - rowPixelSpacing: undefined, + columnPixelSpacing: 1, // for web it's always 1 + rowPixelSpacing: 1, // for web it's always 1 invert: false, sizeInBytes: rows * columns * 4, } @@ -218,7 +218,12 @@ function _loadImageIntoBuffer( loadImage(uri, imageId) .promise.then( (image: cornerstone.Types.IImage) => { - if (!options || !options.targetBuffer) { + if ( + !options || + !options.targetBuffer || + !options.targetBuffer.length || + !options.targetBuffer.offset + ) { resolve(image) return } diff --git a/packages/cornerstone-image-loader-streaming-volume/test/StreamingImageVolume_test.js b/packages/cornerstone-image-loader-streaming-volume/test/StreamingImageVolume_test.js index b25da595d2..d6a3bc0907 100644 --- a/packages/cornerstone-image-loader-streaming-volume/test/StreamingImageVolume_test.js +++ b/packages/cornerstone-image-loader-streaming-volume/test/StreamingImageVolume_test.js @@ -132,304 +132,311 @@ function setupLoaders() { } } -describe('StreamingImageVolume', function () { - beforeAll(function () { - const { imageIds, imageLoader } = setupLoaders() - - this.imageIds = imageIds - this.imageLoader = imageLoader +describe('renderingCore -- volume', () => { + beforeAll(() => { + // initialize the library + cornerstone.init() }) - it('load: correctly streams pixel data from Images into Volume via a SharedArrayBuffer', async function () { - const volumeId = 'fakeVolumeLoader:VOLUME' + describe('StreamingImageVolume', function () { + beforeAll(function () { + const { imageIds, imageLoader } = setupLoaders() - await cornerstone.createAndCacheVolume(volumeId, { - imageIds: this.imageIds, + this.imageIds = imageIds + this.imageLoader = imageLoader }) - const volume = cornerstone.getVolume(volumeId) - - let framesLoaded = 0 - const callback = (evt) => { - framesLoaded++ - - if (framesLoaded === this.imageIds.length) { - // Getting the volume to check for voxel intensities - const volumeLoadObject = cache.getVolumeLoadObject(volumeId) - volumeLoadObject.promise.then((volume) => { - const volumeImage = volume.vtkImageData - // first slice (z=0) voxels to be all 1 - let worldPos = volumeImage.indexToWorld([0, 0, 0]) - let intensity = volume.vtkImageData.getScalarValueFromWorld(worldPos) - expect(intensity).toBe(1) - // 4th slice (z=3) voxels to be all 4 - worldPos = volumeImage.indexToWorld([0, 0, 3]) - intensity = volume.vtkImageData.getScalarValueFromWorld(worldPos) - expect(intensity).toBe(4) - }) - } - } - volume.load(callback) - }) - - it('load: leverages images already in the cache for loading a volume', async function () { - const volumeId = 'fakeVolumeLoader:VOLUME' + it('load: correctly streams pixel data from Images into Volume via a SharedArrayBuffer', async function () { + const volumeId = 'fakeVolumeLoader:VOLUME' - const imageIds = [ - 'fakeImageLoader:imageId1', - 'fakeImageLoader:imageId2', - 'fakeImageLoader:imageId3', - 'fakeImageLoader:imageId4', - 'fakeImageLoader:imageId5', - ] + await cornerstone.createAndCacheVolume(volumeId, { + imageIds: this.imageIds, + }) + const volume = cornerstone.getVolume(volumeId) - // loading the images first - await cornerstone.loadAndCacheImages(imageIds) + let framesLoaded = 0 + const callback = (evt) => { + framesLoaded++ - // only cached images so far - expect(cache.getCacheSize()).toBe(50000) - expect(cache.getImageLoadObject(imageIds[0])).toBeDefined() + if (framesLoaded === this.imageIds.length) { + // Getting the volume to check for voxel intensities + const volumeLoadObject = cache.getVolumeLoadObject(volumeId) + volumeLoadObject.promise.then((volume) => { + const volumeImage = volume.imageData + // first slice (z=0) voxels to be all 1 + let worldPos = volumeImage.indexToWorld([0, 0, 0]) + let intensity = volume.imageData.getScalarValueFromWorld(worldPos) + expect(intensity).toBe(1) + // 4th slice (z=3) voxels to be all 4 + worldPos = volumeImage.indexToWorld([0, 0, 3]) + intensity = volume.imageData.getScalarValueFromWorld(worldPos) + expect(intensity).toBe(4) + }) + } + } - // caching volume - await cornerstone.createAndCacheVolume('fakeVolumeLoader:VOLUME', { - imageIds: this.imageIds, + volume.load(callback) }) - expect(cache.getCacheSize()).toBe(100000) + it('load: leverages images already in the cache for loading a volume', async function () { + const volumeId = 'fakeVolumeLoader:VOLUME' - // loading the volume - const volume = cornerstone.getVolume(volumeId) - const prefetch = false - const callback = undefined - // adding requests to the pool manager - volume.load(callback, prefetch) + const imageIds = [ + 'fakeImageLoader:imageId1', + 'fakeImageLoader:imageId2', + 'fakeImageLoader:imageId3', + 'fakeImageLoader:imageId4', + 'fakeImageLoader:imageId5', + ] - // awaiting all promises for images after requested to be copied over - for (let imageId of imageIds) { - const cachedImage = cornerstone.cache.getImageLoadObject(imageId) - const image = await cachedImage.promise - } - const pool = cornerstone.requestPoolManager.getRequestPool() + // loading the images first + await cornerstone.loadAndCacheImages(imageIds) - // expect no requests to be added to the request manager, since images - // were already cached in the image cache - let requests = Object.values(pool['prefetch']).flat() - expect(requests.length).toBe(0) + // only cached images so far + expect(cache.getCacheSize()).toBe(50000) + expect(cache.getImageLoadObject(imageIds[0])).toBeDefined() - // Getting the volume to check for voxel intensities - const volumeImage = volume.vtkImageData + // caching volume + await cornerstone.createAndCacheVolume('fakeVolumeLoader:VOLUME', { + imageIds: this.imageIds, + }) - // first slice (z=0) voxels to be all 1 - let worldPos = volumeImage.indexToWorld([0, 0, 0]) - let intensity = volume.vtkImageData.getScalarValueFromWorld(worldPos) - expect(intensity).toBe(1) + expect(cache.getCacheSize()).toBe(100000) - // 5th slice (z=4) voxels to be all 5 - worldPos = volumeImage.indexToWorld([0, 0, 4]) - intensity = volume.vtkImageData.getScalarValueFromWorld(worldPos) + // loading the volume + const volume = cornerstone.getVolume(volumeId) + const prefetch = false + const callback = undefined + // adding requests to the pool manager + volume.load(callback, prefetch) + + // awaiting all promises for images after requested to be copied over + for (let imageId of imageIds) { + const cachedImage = cornerstone.cache.getImageLoadObject(imageId) + const image = await cachedImage.promise + } + const pool = cornerstone.requestPoolManager.getRequestPool() - expect(intensity).toBe(5) - }) + // expect no requests to be added to the request manager, since images + // were already cached in the image cache + let requests = Object.values(pool['prefetch']).flat() + expect(requests.length).toBe(0) - it('load: leverages volume that are in the cache already for the image loading', async function () { - const spyedImageLoader = jasmine.createSpy(this.imageLoader) + // Getting the volume to check for voxel intensities + const volumeImage = volume.imageData - const volumeId = 'fakeVolumeLoader:VOLUME' + // first slice (z=0) voxels to be all 1 + let worldPos = volumeImage.indexToWorld([0, 0, 0]) + let intensity = volume.imageData.getScalarValueFromWorld(worldPos) + expect(intensity).toBe(1) - const imageIds = [ - 'fakeImageLoader:imageId1', - 'fakeImageLoader:imageId2', - 'fakeImageLoader:imageId3', - 'fakeImageLoader:imageId4', - 'fakeImageLoader:imageId5', - ] + // 5th slice (z=4) voxels to be all 5 + worldPos = volumeImage.indexToWorld([0, 0, 4]) + intensity = volume.imageData.getScalarValueFromWorld(worldPos) - // caching volume - await cornerstone.createAndCacheVolume('fakeVolumeLoader:VOLUME', { - imageIds: this.imageIds, + expect(intensity).toBe(5) }) - expect(cache.getCacheSize()).toBe(50000) + it('load: leverages volume that are in the cache already for the image loading', async function () { + const spyedImageLoader = jasmine.createSpy(this.imageLoader) - // loading the volume - const volume = cornerstone.getVolume(volumeId) - const callback = undefined - // adding requests to the pool manager - volume.load(callback) + const volumeId = 'fakeVolumeLoader:VOLUME' - expect(cache.getImageLoadObject(imageIds[0])).not.toBeDefined() + const imageIds = [ + 'fakeImageLoader:imageId1', + 'fakeImageLoader:imageId2', + 'fakeImageLoader:imageId3', + 'fakeImageLoader:imageId4', + 'fakeImageLoader:imageId5', + ] - // loading the images - await cornerstone.loadAndCacheImages(imageIds) + // caching volume + await cornerstone.createAndCacheVolume('fakeVolumeLoader:VOLUME', { + imageIds: this.imageIds, + }) - // imageLoader is not being called for any imageIds - expect(spyedImageLoader).not.toHaveBeenCalled() + expect(cache.getCacheSize()).toBe(50000) - // Images are copied over from the volume, check for the fourth image (imageId4) - // which has pixel data of 4 - const imageLoadObject = cache.getImageLoadObject(imageIds[3]) - expect(cache.getCacheSize()).toBe(100000) - expect(imageLoadObject).toBeDefined() + // loading the volume + const volume = cornerstone.getVolume(volumeId) + const callback = undefined + // adding requests to the pool manager + volume.load(callback) - imageLoadObject.promise.then((image) => { - const pixelData = image.getPixelData() - expect(pixelData[0]).toBe(4) - }) - }) + expect(cache.getImageLoadObject(imageIds[0])).not.toBeDefined() - // it('cancelLoading: ', async function () { - // await cornerstone.createAndCacheVolume('fakeVolumeLoader:VOLUME', { - // imageIds: this.imageIds, - // }) + // loading the images + await cornerstone.loadAndCacheImages(imageIds) - // const volumeId = 'fakeVolumeLoader:VOLUME' - // const volume = cornerstone.getVolume(volumeId) + // imageLoader is not being called for any imageIds + expect(spyedImageLoader).not.toHaveBeenCalled() - // const callback = undefined - // const prefetch = false - // volume.load(callback, prefetch) + // Images are copied over from the volume, check for the fourth image (imageId4) + // which has pixel data of 4 + const imageLoadObject = cache.getImageLoadObject(imageIds[3]) + expect(cache.getCacheSize()).toBe(100000) + expect(imageLoadObject).toBeDefined() - // let pool = cornerstone.requestPoolManager.getRequestPool() + imageLoadObject.promise.then((image) => { + const pixelData = image.getPixelData() + expect(pixelData[0]).toBe(4) + }) + }) - // let numImagesInPool = Object.values(pool['prefetch']).flat().length - // expect(numImagesInPool).toEqual(5) - // expect(volume.loadStatus.loading).toEqual(true) + // it('cancelLoading: ', async function () { + // await cornerstone.createAndCacheVolume('fakeVolumeLoader:VOLUME', { + // imageIds: this.imageIds, + // }) - // volume.cancelLoading() + // const volumeId = 'fakeVolumeLoader:VOLUME' + // const volume = cornerstone.getVolume(volumeId) - // pool = cornerstone.requestPoolManager.getRequestPool() + // const callback = undefined + // const prefetch = false + // volume.load(callback, prefetch) - // const requests = Object.values(pool['prefetch']).flat() + // let pool = cornerstone.requestPoolManager.getRequestPool() - // numImagesInPool = requests.length - // expect(numImagesInPool).toEqual(0) + // let numImagesInPool = Object.values(pool['prefetch']).flat().length + // expect(numImagesInPool).toEqual(5) + // expect(volume.loadStatus.loading).toEqual(true) - // expect(volume.loadStatus.loaded).toEqual(false) - // expect(volume.loadStatus.loading).toEqual(false) - // expect(volume.loadStatus.callbacks.length).toEqual(0) - // }) + // volume.cancelLoading() - it('decache: properly decaches the Volume into a set of Images', async function () { - await cornerstone.createAndCacheVolume('fakeVolumeLoader:VOLUME', { - imageIds: this.imageIds, - }) + // pool = cornerstone.requestPoolManager.getRequestPool() + + // const requests = Object.values(pool['prefetch']).flat() + + // numImagesInPool = requests.length + // expect(numImagesInPool).toEqual(0) - const volumeId = 'fakeVolumeLoader:VOLUME' - const volume = cornerstone.getVolume(volumeId) - const completelyRemove = false + // expect(volume.loadStatus.loaded).toEqual(false) + // expect(volume.loadStatus.loading).toEqual(false) + // expect(volume.loadStatus.callbacks.length).toEqual(0) + // }) - volume.load() + it('decache: properly decaches the Volume into a set of Images', async function () { + await cornerstone.createAndCacheVolume('fakeVolumeLoader:VOLUME', { + imageIds: this.imageIds, + }) - const cacheSizeBeforeDecache = cache.getCacheSize() + const volumeId = 'fakeVolumeLoader:VOLUME' + const volume = cornerstone.getVolume(volumeId) + const completelyRemove = false - // turn volume into images - volume.decache(completelyRemove) + volume.load() - const cacheSizeAfterDecache = cache.getCacheSize() + const cacheSizeBeforeDecache = cache.getCacheSize() - // Gets the volume - const volAfterDecache = cornerstone.getVolume(volumeId) - expect(volAfterDecache).not.toBeDefined() + // turn volume into images + volume.decache(completelyRemove) - expect(cacheSizeBeforeDecache - cacheSizeAfterDecache).toBe(50000) + const cacheSizeAfterDecache = cache.getCacheSize() - for (let imageId of this.imageIds) { - const cachedImage = cornerstone.cache.getImageLoadObject(imageId) + // Gets the volume + const volAfterDecache = cornerstone.getVolume(volumeId) + expect(volAfterDecache).not.toBeDefined() - expect(cachedImage).toBeDefined() + expect(cacheSizeBeforeDecache - cacheSizeAfterDecache).toBe(50000) - const image = await cachedImage.promise - expect(image.columns).toBe(100) - expect(image.rows).toBe(100) - expect(image.sizeInBytes).toBe(10000) - expect(image.invert).toBe(true) - } - }) + for (let imageId of this.imageIds) { + const cachedImage = cornerstone.cache.getImageLoadObject(imageId) - it('decache: completely removes the Volume from the cache', async function () { - await cornerstone.createAndCacheVolume('fakeVolumeLoader:VOLUME', { - imageIds: this.imageIds, - }) + expect(cachedImage).toBeDefined() - const volumeId = 'fakeVolumeLoader:VOLUME' - const volume = cornerstone.getVolume(volumeId) + const image = await cachedImage.promise + expect(image.columns).toBe(100) + expect(image.rows).toBe(100) + expect(image.sizeInBytes).toBe(10000) + expect(image.invert).toBe(true) + } + }) - const completelyRemove = true + it('decache: completely removes the Volume from the cache', async function () { + await cornerstone.createAndCacheVolume('fakeVolumeLoader:VOLUME', { + imageIds: this.imageIds, + }) - volume.load() + const volumeId = 'fakeVolumeLoader:VOLUME' + const volume = cornerstone.getVolume(volumeId) - const cacheSizeBeforePurge = cache.getCacheSize() - expect(cacheSizeBeforePurge).toBe(50000) + const completelyRemove = true - volume.decache(completelyRemove) + volume.load() - // Gets the volume - const volAfterDecache = cornerstone.getVolume(volumeId) - expect(volAfterDecache).not.toBeDefined() + const cacheSizeBeforePurge = cache.getCacheSize() + expect(cacheSizeBeforePurge).toBe(50000) - const cacheSizeAfterPurge = cache.getCacheSize() - expect(cacheSizeAfterPurge).toBe(0) + volume.decache(completelyRemove) - const cachedImage0 = cache.getImageLoadObject(this.imageIds[0]) + // Gets the volume + const volAfterDecache = cornerstone.getVolume(volumeId) + expect(volAfterDecache).not.toBeDefined() - expect(cachedImage0).not.toBeDefined() - }) + const cacheSizeAfterPurge = cache.getCacheSize() + expect(cacheSizeAfterPurge).toBe(0) - afterEach(function () { - cache.purgeCache() - }) -}) + const cachedImage0 = cache.getImageLoadObject(this.imageIds[0]) -describe('CornerstoneVolumeStreaming Streaming --- ', function () { - beforeEach(function () { - cache.purgeCache() - metaData.addProvider(testUtils.fakeMetaDataProvider, 10000) - cornerstone.registerImageLoader( - 'fakeSharedBufferImageLoader', - fakeSharedBufferImageLoader - ) - registerVolumeLoader( - 'fakeSharedBufferImageLoader', - testUtils.fakeImageLoader - ) - }) + expect(cachedImage0).not.toBeDefined() + }) - afterEach(function () { - cache.purgeCache() - metaData.removeProvider(testUtils.fakeMetaDataProvider) - unregisterAllImageLoaders() + afterEach(function () { + cache.purgeCache() + }) }) - it('should successfully use metadata for streaming image volume', async function (done) { - const imageIds = [ - 'fakeSharedBufferImageLoader:myImag1_256_256_0_20_1_1_0', - 'fakeSharedBufferImageLoader:myImage2_256_256_0_20_1_1_0', - 'fakeSharedBufferImageLoader:myImage3_256_256_0_20_1_1_0', - 'fakeSharedBufferImageLoader:myImage4_256_256_0_20_1_1_0', - 'fakeSharedBufferImageLoader:myImage5_256_256_0_20_1_1_0', - ] - - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'cornerstoneStreamingImageVolume:volume' + describe('CornerstoneVolumeStreaming Streaming --- ', function () { + beforeEach(function () { + cache.purgeCache() + metaData.addProvider(testUtils.fakeMetaDataProvider, 10000) + cornerstone.registerImageLoader( + 'fakeSharedBufferImageLoader', + fakeSharedBufferImageLoader + ) + registerVolumeLoader( + 'fakeSharedBufferImageLoader', + testUtils.fakeImageLoader + ) + }) - try { - await cornerstone.createAndCacheVolume(volumeId, { - imageIds: imageIds, - }) - const volume = cornerstone.getVolume(volumeId) + afterEach(function () { + cache.purgeCache() + metaData.removeProvider(testUtils.fakeMetaDataProvider) + unregisterAllImageLoaders() + }) - let framesLoaded = 0 - const callback = (evt) => { - framesLoaded++ - if (framesLoaded === imageIds.length) { - // Getting the volume to check for voxel intensities - done() + it('should successfully use metadata for streaming image volume', async function (done) { + const imageIds = [ + 'fakeSharedBufferImageLoader:myImag1_256_256_0_20_1_1_0', + 'fakeSharedBufferImageLoader:myImage2_256_256_0_20_1_1_0', + 'fakeSharedBufferImageLoader:myImage3_256_256_0_20_1_1_0', + 'fakeSharedBufferImageLoader:myImage4_256_256_0_20_1_1_0', + 'fakeSharedBufferImageLoader:myImage5_256_256_0_20_1_1_0', + ] + + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'cornerstoneStreamingImageVolume:volume' + + try { + await cornerstone.createAndCacheVolume(volumeId, { + imageIds: imageIds, + }) + const volume = cornerstone.getVolume(volumeId) + + let framesLoaded = 0 + const callback = (evt) => { + framesLoaded++ + if (framesLoaded === imageIds.length) { + // Getting the volume to check for voxel intensities + done() + } } + volume.load(callback) + } catch (e) { + done.fail(e) } - volume.load(callback) - } catch (e) { - done.fail(e) - } + }) }) }) diff --git a/packages/cornerstone-render/package.json b/packages/cornerstone-render/package.json index 93aacf49bd..0b91970062 100644 --- a/packages/cornerstone-render/package.json +++ b/packages/cornerstone-render/package.json @@ -18,11 +18,13 @@ "start": "yarn run dev" }, "peerDependencies": { - "gl-matrix": "^3.3.0", + "gl-matrix": "^3.4.3", "vtk.js": "git+https://github.com/jadh4v/vtk-js.git#image-volume-half-voxel" }, "devDependencies": { - "gl-matrix": "^3.3.0", + "gl-matrix": "^3.4.3", + "resemblejs": "^3.2.5", + "detect-gpu": "^4.0.7", "vtk.js": "git+https://github.com/jadh4v/vtk-js.git#image-volume-half-voxel" }, "author": "", diff --git a/packages/cornerstone-render/src/RenderingEngine/RenderingEngine.ts b/packages/cornerstone-render/src/RenderingEngine/RenderingEngine.ts index e2378592e0..d82cce9414 100644 --- a/packages/cornerstone-render/src/RenderingEngine/RenderingEngine.ts +++ b/packages/cornerstone-render/src/RenderingEngine/RenderingEngine.ts @@ -4,11 +4,13 @@ import VIEWPORT_TYPE from '../constants/viewportType' import eventTarget from '../eventTarget' import { triggerEvent, uuidv4 } from '../utilities' import { vtkOffscreenMultiRenderWindow } from './vtkClasses' -import { IViewport, PublicViewportInput, ViewportInput } from '../types' +import { PublicViewportInput, ViewportInput } from '../types' import VolumeViewport from './VolumeViewport' import StackViewport from './StackViewport' import Scene from './Scene' import isEqual from 'lodash.isequal' +import viewportTypeUsesCustomRenderingPipeline from './helpers/viewportTypeUsesCustomRenderingPipeline' +import { getShouldUseCPURendering, isCornerstoneInitialized } from '../init' interface IRenderingEngine { uid: string @@ -70,6 +72,7 @@ class RenderingEngine implements IRenderingEngine { private _needsRender: Set = new Set() private _animationFrameSet = false private _animationFrameHandle: number | null = null + private useCPURendering: boolean /** * @@ -77,12 +80,23 @@ class RenderingEngine implements IRenderingEngine { */ constructor(uid: string) { this.uid = uid ? uid : uuidv4() + this.useCPURendering = getShouldUseCPURendering() + renderingEngineCache.set(this) - this.offscreenMultiRenderWindow = - vtkOffscreenMultiRenderWindow.newInstance() - this.offScreenCanvasContainer = document.createElement('div') - this.offscreenMultiRenderWindow.setContainer(this.offScreenCanvasContainer) + if (!isCornerstoneInitialized()) { + throw new Error('Cornerstone-render is not initialized, run init() first') + } + + if (!this.useCPURendering) { + this.offscreenMultiRenderWindow = + vtkOffscreenMultiRenderWindow.newInstance() + this.offScreenCanvasContainer = document.createElement('div') + this.offscreenMultiRenderWindow.setContainer( + this.offScreenCanvasContainer + ) + } + this._scenes = new Map() this._viewports = new Map() this.hasBeenDestroyed = false @@ -109,7 +123,7 @@ class RenderingEngine implements IRenderingEngine { */ public enableElement(viewportInputEntry: PublicViewportInput): void { this._throwIfDestroyed() - const { canvas, viewportUID, sceneUID } = viewportInputEntry + const { canvas, viewportUID } = viewportInputEntry // Throw error if no canvas if (!canvas) { @@ -117,7 +131,7 @@ class RenderingEngine implements IRenderingEngine { } // 1. Get the viewport from the list of available viewports. - let viewport = this.getViewport(viewportUID) + const viewport = this.getViewport(viewportUID) // 1.a) If there is a found viewport, and the scene Id has changed, we // remove the viewport and create a new viewport @@ -127,45 +141,22 @@ class RenderingEngine implements IRenderingEngine { // this._removeViewport(viewportUID) } - // 2. Retrieving the list of viewports for calculation of the new size for - // offScreen canvas. - const viewports = this._getViewportsAsArray() - const canvases = viewports.map((vp) => vp.canvas) - canvases.push(viewportInputEntry.canvas) + // 2.a) See if viewport uses a custom rendering pipeline. + const { type } = viewportInputEntry - // 2.a Calculating the new size for offScreen Canvas - const { offScreenCanvasWidth, offScreenCanvasHeight } = - this._resizeOffScreenCanvas(canvases) + const viewportUsesCustomRenderingPipeline = + viewportTypeUsesCustomRenderingPipeline(type) - // 2.b Re-position previous viewports on the offScreen Canvas based on the new - // offScreen canvas size - const _xOffset = this._resize( - viewports, - offScreenCanvasWidth, - offScreenCanvasHeight - ) - - // 3 Add the requested viewport to rendering Engine - this._addViewport( - viewportInputEntry, - offScreenCanvasWidth, - offScreenCanvasHeight, - _xOffset - ) - - // 4. Check if the viewport is part of a scene, if yes, add the available - // volume Actors to the viewport too - viewport = this.getViewport(viewportUID) + // 2.b) Retrieving the list of viewports for calculation of the new size for + // offScreen canvas. - // 4.a Only volumeViewports have scenes - if (viewport instanceof VolumeViewport) { - const scene = viewport.getScene() - const volActors = scene.getVolumeActors() - const viewportActors = viewport.getActors() - // add the volume actor if not the same as the viewport actor - if (!isEqual(volActors, viewportActors)) { - scene.addVolumeActors(viewportUID) - } + // If the viewport being added uses a custom pipeline, or we aren't using + // GPU rendering, we don't need to resize the offscreen canvas. + if (!this.useCPURendering && !viewportUsesCustomRenderingPipeline) { + this.enableVTKjsDrivenViewport(viewportInputEntry) + } else { + // 3 Add the requested viewport to rendering Engine + this.addCustomViewport(viewportInputEntry) } // 5. Add the new viewport to the queue to be rendered @@ -199,7 +190,12 @@ class RenderingEngine implements IRenderingEngine { this._resetViewport(viewport) // 4. Remove the related renderer from the offScreenMultiRenderWindow - this.offscreenMultiRenderWindow.removeRenderer(viewportUID) + if ( + !viewportTypeUsesCustomRenderingPipeline(viewport.type) && + !this.useCPURendering + ) { + this.offscreenMultiRenderWindow.removeRenderer(viewportUID) + } // 5. Remove the requested viewport from the rendering engine this._removeViewport(viewportUID) @@ -217,6 +213,377 @@ class RenderingEngine implements IRenderingEngine { this.resize() } + /** + * Creates `Scene`s containing `Viewport`s and sets up the offscreen render + * window to allow offscreen rendering and transmission back to the target + * canvas in each viewport. + * + * @param viewportInputEntries An array of viewport definitions to construct the rendering engine + * /todo: if don't want scene don't' give uid + */ + public setViewports(viewportInputEntries: Array): void { + this._throwIfDestroyed() + this._reset() + + // 1. Split viewports based on whether they use vtk.js or a custom pipeline. + + const vtkDrivenViewportInputEntries: PublicViewportInput[] = [] + const customRenderingViewportInputEntries: PublicViewportInput[] = [] + + viewportInputEntries.forEach((vpie) => { + if ( + !this.useCPURendering && + !viewportTypeUsesCustomRenderingPipeline(vpie.type) + ) { + vtkDrivenViewportInputEntries.push(vpie) + } else { + customRenderingViewportInputEntries.push(vpie) + } + }) + + this.setVtkjsDrivenViewports(vtkDrivenViewportInputEntries) + this.setCustomViewports(customRenderingViewportInputEntries) + } + + /** + * @method resize Resizes the offscreen viewport and recalculates translations to on screen canvases. + * It is up to the parent app to call the size of the on-screen canvas changes. + * This is left as an app level concern as one might want to debounce the changes, or the like. + * + * @param {boolean} [immediate=true] Whether all of the viewports should be rendered immediately. + * @param {boolean} [resetPanZoomForViewPlane=true] Whether each viewport gets centered (reset pan) and + * its zoom gets reset upon resize. + * + */ + public resize(immediate = true, resetPanZoomForViewPlane = true): void { + this._throwIfDestroyed() + + // 1. Get the viewports' canvases + const viewports = this._getViewportsAsArray() + + const vtkDrivenViewports = [] + const customRenderingViewports = [] + + viewports.forEach((vpie) => { + if (!viewportTypeUsesCustomRenderingPipeline(vpie.type)) { + vtkDrivenViewports.push(vpie) + } else { + customRenderingViewports.push(vpie) + } + }) + + this._resizeVTKViewports( + vtkDrivenViewports, + immediate, + resetPanZoomForViewPlane + ) + + this._resizeUsingCustomResizeHandler( + customRenderingViewports, + immediate, + resetPanZoomForViewPlane + ) + } + + /** + * @method getScene Returns the scene, only scenes with SceneUID (not internal) + * are returned + * @param {string} sceneUID The UID of the scene to fetch. + * + * @returns {Scene} The scene object. + */ + public getScene(sceneUID: string): Scene { + this._throwIfDestroyed() + + // Todo: should the volume be decached? + return this._scenes.get(sceneUID) + } + + /** + * @method getScenes Returns an array of all `Scene`s on the `RenderingEngine` instance. + * + * @returns {Scene} The scene object. + */ + public getScenes(): Array { + this._throwIfDestroyed() + + return Array.from(this._scenes.values()).filter((s) => { + // Do not return Scenes not explicitly created by the user + return s.getIsInternalScene() === false + }) + } + + /** + * @method getScenes Returns an array of all `Scene`s on the `RenderingEngine` instance. + * + * @returns {Scene} The scene object. + */ + public removeScene(sceneUID: string): void { + this._throwIfDestroyed() + + this._scenes.delete(sceneUID) + } + + /** + * @method getViewport Returns the viewport by UID + * + * @returns {StackViewport | VolumeViewport} viewport + */ + public getViewport(uid: string): StackViewport | VolumeViewport { + return this._viewports.get(uid) + } + + /** + * @method getViewportsContainingVolumeUID Returns the viewport containing the volumeUID + * + * @returns {VolumeViewport} viewports + */ + public getViewportsContainingVolumeUID(uid: string): Array { + const viewports = this._getViewportsAsArray() as Array + return viewports.filter((vp) => { + const volActors = vp.getDefaultActor() + return volActors.volumeActor && volActors.uid === uid + }) + } + + /** + * @method getScenesContainingVolume Returns the scenes containing the volumeUID + * + * @returns {Scene} scenes + */ + public getScenesContainingVolume(uid: string): Array { + const scenes = this.getScenes() + return scenes.filter((scene) => { + const volumeActors = scene.getVolumeActors() + const firstActor = volumeActors[0] + return firstActor.volumeActor && firstActor.uid === uid + }) + } + + /** + * @method getViewports Returns an array of all `Viewport`s on the `RenderingEngine` instance. + * + * @returns {Viewport} The scene object. + */ + public getViewports(): Array { + this._throwIfDestroyed() + + return this._getViewportsAsArray() + } + + /** + * Filters all the available viewports and return the stack viewports + * @returns stack viewports registered on the rendering Engine + */ + public getStackViewports(): Array { + this._throwIfDestroyed() + + const viewports = this.getViewports() + + const isStackViewport = ( + viewport: StackViewport | VolumeViewport + ): viewport is StackViewport => { + return viewport instanceof StackViewport + } + + return viewports.filter(isStackViewport) + } + + /** + * @method render Renders all viewports on the next animation frame. + */ + public render(): void { + const viewports = this.getViewports() + const viewportUIDs = viewports.map((vp) => vp.uid) + + this._setViewportsToBeRenderedNextFrame(viewportUIDs) + } + + /** + * @method renderScene Renders only a specific `Scene` on the next animation frame. + * + * @param {string} sceneUID The UID of the scene to render. + */ + public renderScene(sceneUID: string): void { + const scene = this.getScene(sceneUID) + const viewportUIDs = scene.getViewportUIDs() + + this._setViewportsToBeRenderedNextFrame(viewportUIDs) + } + + /** + * @method renderFrameOfReference Renders any viewports viewing the + * given Frame Of Reference. + * + * @param {string} FrameOfReferenceUID The unique identifier of the + * Frame Of Reference. + */ + public renderFrameOfReference = (FrameOfReferenceUID: string): void => { + const viewports = this._getViewportsAsArray() + const viewportUidsWithSameFrameOfReferenceUID = viewports.map((vp) => { + if (vp.getFrameOfReferenceUID() === FrameOfReferenceUID) { + return vp.uid + } + }) + + return this.renderViewports(viewportUidsWithSameFrameOfReferenceUID) + } + + /** + * @method renderScenes Renders the provided Scene UIDs. + * + * @returns{void} + */ + public renderScenes(sceneUIDs: Array): void { + const scenes = sceneUIDs.map((sUid) => this.getScene(sUid)) + this._renderScenes(scenes) + } + + /** + * @method renderViewports Renders the provided Viewport UIDs. + * + * @returns{void} + */ + public renderViewports(viewportUIDs: Array): void { + this._setViewportsToBeRenderedNextFrame(viewportUIDs) + } + + /** + * @method renderViewport Renders only a specific `Viewport` on the next animation frame. + * + * @param {string} viewportUID The UID of the viewport. + */ + public renderViewport(viewportUID: string): void { + this._setViewportsToBeRenderedNextFrame([viewportUID]) + } + + /** + * @method destroy the rendering engine + */ + public destroy(): void { + if (this.hasBeenDestroyed) { + return + } + + this._reset() + renderingEngineCache.delete(this.uid) + + if (!this.useCPURendering) { + // Free up WebGL resources + this.offscreenMultiRenderWindow.delete() + + // Make sure all references go stale and are garbage collected. + delete this.offscreenMultiRenderWindow + } + + this.hasBeenDestroyed = true + } + + private _resizeUsingCustomResizeHandler( + customRenderingViewports: StackViewport[], + immediate = true, + resetPanZoomForViewPlane = true + ) { + // 1. If viewport has a custom resize method, call it here. + customRenderingViewports.forEach((vp) => { + if (typeof vp.resize === 'function') vp.resize() + }) + + // 3. Reset viewport cameras + customRenderingViewports.forEach((vp) => { + vp.resetCamera(resetPanZoomForViewPlane) + }) + + // 2. If render is immediate: Render all + if (immediate === true) { + this.render() + } + } + + private _resizeVTKViewports( + vtkDrivenViewports: (StackViewport | VolumeViewport)[], + resetPanZoomForViewPlane = true, + immediate = true + ) { + const canvasesDrivenByVtkJs = vtkDrivenViewports.map((vp) => vp.canvas) + + if (canvasesDrivenByVtkJs.length) { + // 1. Recalculate and resize the offscreen canvas size + const { offScreenCanvasWidth, offScreenCanvasHeight } = + this._resizeOffScreenCanvas(canvasesDrivenByVtkJs) + + // 2. Recalculate the viewports location on the off screen canvas + this._resize( + vtkDrivenViewports, + offScreenCanvasWidth, + offScreenCanvasHeight + ) + } + + // 3. Reset viewport cameras + vtkDrivenViewports.forEach((vp) => { + vp.resetCamera(resetPanZoomForViewPlane) + }) + + // 4. If render is immediate: Render all + if (immediate === true) { + this.render() + } + } + + /** + * @method enableVTKjsDrivenViewport Enables a viewport to be driven by the + * offscreen vtk.js rendering engine. + * + * @param {PublicViewportInput} viewportInputEntry Information object used to + * construct and enable the viewport. + */ + private enableVTKjsDrivenViewport(viewportInputEntry: PublicViewportInput) { + const viewports = this._getViewportsAsArray() + const viewportsDrivenByVtkJs = viewports.filter( + (vp) => viewportTypeUsesCustomRenderingPipeline(vp.type) === false + ) + + const canvasesDrivenByVtkJs = viewportsDrivenByVtkJs.map((vp) => vp.canvas) + + canvasesDrivenByVtkJs.push(viewportInputEntry.canvas) + + // 2.c Calculating the new size for offScreen Canvas + const { offScreenCanvasWidth, offScreenCanvasHeight } = + this._resizeOffScreenCanvas(canvasesDrivenByVtkJs) + + // 2.d Re-position previous viewports on the offScreen Canvas based on the new + // offScreen canvas size + const xOffset = this._resize( + viewportsDrivenByVtkJs, + offScreenCanvasWidth, + offScreenCanvasHeight + ) + + // 3 Add the requested viewport to rendering Engine + this.addVtkjsDrivenViewport(viewportInputEntry, { + offScreenCanvasWidth, + offScreenCanvasHeight, + xOffset, + }) + + // 4. Check if the viewport is part of a scene, if yes, add the available + // volume Actors to the viewport too + const viewportUID = viewportInputEntry.viewportUID + const viewport = this.getViewport(viewportUID) + + // 4.a Only volumeViewports have scenes + if (viewport instanceof VolumeViewport) { + const scene = viewport.getScene() + const volActors = scene.getVolumeActors() + const viewportActors = viewport.getActors() + // add the volume actor if not the same as the viewport actor + if (!isEqual(volActors, viewportActors)) { + scene.addVolumeActors(viewportUID) + } + } + } + /** * Disables the requested viewportUID from the rendering engine: * 1) It removes the viewport from the the list of viewports @@ -255,25 +622,32 @@ class RenderingEngine implements IRenderingEngine { } /** - * Add viewport at the correct position on the offScreenCanvas - * - * @param {Object} viewportInputEntry viewport definition to construct the viewport - * @param {number} offScreenCanvasWidth offScreen width - * @param {number} offScreenCanvasHeight offScreen height - * @param {number} _xOffset offset from left of offScreen canvas to place the viewport + * @method addVtkjsDrivenViewport Adds a viewport driven by vtk.js to the + * `RenderingEngine`. * - * @returns {void} - * @memberof RenderingEngine + * @param {PublicViewportInput} viewportInputEntry Information object used to + * construct and enable the viewport. + * @param {{ + * offScreenCanvasWidth: number; + * offScreenCanvasHeight: number; + * xOffset: number; + * }} [offscreenCanvasProperties] How the viewport relates to the + * offscreen canvas. */ - private _addViewport( + private addVtkjsDrivenViewport( viewportInputEntry: PublicViewportInput, - offScreenCanvasWidth: number, - offScreenCanvasHeight: number, - _xOffset: number + offscreenCanvasProperties?: { + offScreenCanvasWidth: number + offScreenCanvasHeight: number + xOffset: number + } ): void { const { canvas, sceneUID, viewportUID, type, defaultOptions } = viewportInputEntry + const { offScreenCanvasWidth, offScreenCanvasHeight, xOffset } = + offscreenCanvasProperties + // 1. Calculate the size of location of the viewport on the offScreen canvas const { sxStartDisplayCoords, @@ -288,7 +662,7 @@ class RenderingEngine implements IRenderingEngine { viewportInputEntry, offScreenCanvasWidth, offScreenCanvasHeight, - _xOffset + xOffset ) // 2. Add a renderer to the offScreenMultiRenderWindow @@ -363,68 +737,136 @@ class RenderingEngine implements IRenderingEngine { } /** - * Creates `Scene`s containing `Viewport`s and sets up the offscreen render - * window to allow offscreen rendering and transmission back to the target - * canvas in each viewport. + * @method addCustomViewport Adds a viewport using a custom rendering pipeline + * to the `RenderingEngine`. * - * @param viewportInputEntries An array of viewport definitions to construct the rendering engine - * /todo: if don't want scene don't' give uid + * @param {PublicViewportInput} viewportInputEntry Information object used to + * construct and enable the viewport. */ - public setViewports(viewportInputEntries: Array): void { - this._throwIfDestroyed() - this._reset() + private addCustomViewport(viewportInputEntry: PublicViewportInput): void { + const { canvas, sceneUID, viewportUID, type, defaultOptions } = + viewportInputEntry - // 1. Getting all the canvases from viewports calculation of the new offScreen size - const canvases = viewportInputEntries.map((vp) => vp.canvas) + // Add a viewport with no offset + const { clientWidth, clientHeight } = canvas - // 2. Set canvas size based on height and sum of widths - const { offScreenCanvasWidth, offScreenCanvasHeight } = - this._resizeOffScreenCanvas(canvases) + // Set the canvas to be same resolution as the client. + if (canvas.width !== clientWidth || canvas.height !== clientHeight) { + canvas.width = clientWidth + canvas.height = clientHeight + } + + const viewportInput = { + uid: viewportUID, + renderingEngineUID: this.uid, + type, + canvas, + sx: 0, // No offset, uses own renderer + sy: 0, + sWidth: clientWidth, + sHeight: clientHeight, + defaultOptions: defaultOptions || {}, + } - /* - TODO: Commenting this out until we can mock the Canvas usage in the tests (or use jsdom?) - if (!offScreenCanvasWidth || !offScreenCanvasHeight) { - throw new Error('Invalid offscreen canvas width or height') - }*/ + // 4. Create a proper viewport based on the type of the viewport - // 3. Adding the viewports based on the viewportInputEntry definition to the - // rendering engine. - let _xOffset = 0 - for (let i = 0; i < viewportInputEntries.length; i++) { - const viewportInputEntry = viewportInputEntries[i] + if (type !== VIEWPORT_TYPE.STACK) { + // In the future these will need to be pluggable, but we aren't there yet + // and these are just Stacks for now. + throw new Error('Support for fully custom viewports not yet implemented') + } - const { canvas } = viewportInputEntry - this._addViewport( - viewportInputEntry, - offScreenCanvasWidth, - offScreenCanvasHeight, - _xOffset - ) + // 4.a Create stack viewport + const viewport = new StackViewport(viewportInput) + + // 5. Storing the viewports + this._viewports.set(viewportUID, viewport) + + const eventData = { + canvas, + viewportUID, + sceneUID, + renderingEngineUID: this.uid, + } + + triggerEvent(eventTarget, EVENTS.ELEMENT_ENABLED, eventData) + } - // Incrementing the xOffset which provides the horizontal location of each - // viewport on the offScreen canvas - _xOffset += canvas.clientWidth + /** + * @method setCustomViewports Sets multiple viewports using custom rendering + * pipelines to the `RenderingEngine`. + * + * @param {PublicViewportInput[]} viewportInputEntries An array of information + * objects used to construct and enable the viewports. + */ + private setCustomViewports(viewportInputEntries: PublicViewportInput[]) { + viewportInputEntries.forEach((vpie) => this.addCustomViewport(vpie)) + } + + /** + * @method setCustomViewports Sets multiple vtk.js driven viewports to + * the `RenderingEngine`. + * + * @param {PublicViewportInput[]} viewportInputEntries An array of information + * objects used to construct and enable the viewports. + */ + private setVtkjsDrivenViewports(viewportInputEntries: PublicViewportInput[]) { + // Deal with vtkjs driven viewports + if (viewportInputEntries.length) { + // 1. Getting all the canvases from viewports calculation of the new offScreen size + + const vtkDrivenCanvases = viewportInputEntries.map((vp) => vp.canvas) + + // 2. Set canvas size based on height and sum of widths + const { offScreenCanvasWidth, offScreenCanvasHeight } = + this._resizeOffScreenCanvas(vtkDrivenCanvases) + + /* + TODO: Commenting this out until we can mock the Canvas usage in the tests (or use jsdom?) + if (!offScreenCanvasWidth || !offScreenCanvasHeight) { + throw new Error('Invalid offscreen canvas width or height') + }*/ + + // 3. Adding the viewports based on the viewportInputEntry definition to the + // rendering engine. + let xOffset = 0 + for (let i = 0; i < viewportInputEntries.length; i++) { + const vtkDrivenViewportInputEntry = viewportInputEntries[i] + + const { canvas } = vtkDrivenViewportInputEntry + this.addVtkjsDrivenViewport(vtkDrivenViewportInputEntry, { + offScreenCanvasWidth, + offScreenCanvasHeight, + xOffset, + }) + + // Incrementing the xOffset which provides the horizontal location of each + // viewport on the offScreen canvas + xOffset += canvas.clientWidth + } } } /** - * Resizes the offscreen canvas based on the provided canvases + * Resizes the offscreen canvas based on the provided vtk.js driven canvases. * * @param canvases An array of HTML Canvas */ - private _resizeOffScreenCanvas(canvases: Array) { + private _resizeOffScreenCanvas( + canvasesDrivenByVtkJs: Array + ): { offScreenCanvasWidth: number; offScreenCanvasHeight: number } { const { offScreenCanvasContainer, offscreenMultiRenderWindow } = this // 1. Calculated the height of the offScreen canvas to be the maximum height // between canvases const offScreenCanvasHeight = Math.max( - ...canvases.map((canvas) => canvas.clientHeight) + ...canvasesDrivenByVtkJs.map((canvas) => canvas.clientHeight) ) // 2. Calculating the width of the offScreen canvas to be the sum of all let offScreenCanvasWidth = 0 - canvases.forEach((canvas) => { + canvasesDrivenByVtkJs.forEach((canvas) => { offScreenCanvasWidth += canvas.clientWidth }) @@ -447,15 +889,15 @@ class RenderingEngine implements IRenderingEngine { * @returns {number} _xOffset the final offset which will be used for the next viewport */ private _resize( - viewports: Array, + viewportsDrivenByVtkJs: Array, offScreenCanvasWidth: number, offScreenCanvasHeight: number ): number { // Redefine viewport properties let _xOffset = 0 - - for (let i = 0; i < viewports.length; i++) { - const viewport = viewports[i] + + for (let i = 0; i < viewportsDrivenByVtkJs.length; i++) { + const viewport = viewportsDrivenByVtkJs[i] const { sxStartDisplayCoords, syStartDisplayCoords, @@ -493,41 +935,6 @@ class RenderingEngine implements IRenderingEngine { return _xOffset } - /** - * @method resize Resizes the offscreen viewport and recalculates translations to on screen canvases. - * It is up to the parent app to call the size of the on-screen canvas changes. - * This is left as an app level concern as one might want to debounce the changes, or the like. - * - * @param {boolean} [immediate=true] Whether all of the viewports should be rendered immediately. - * @param {boolean} [resetPanZoomForViewPlane=true] Whether each viewport gets centered (reset pan) and - * its zoom gets reset upon resize. - * - */ - public resize(immediate = true, resetPanZoomForViewPlane = true): void { - this._throwIfDestroyed() - - // 1. Get the viewports' canvases - const viewports = this._getViewportsAsArray() - const canvases = viewports.map((vp) => vp.canvas) - - // 2. Recalculate and resize the offscreen canvas size - const { offScreenCanvasWidth, offScreenCanvasHeight } = - this._resizeOffScreenCanvas(canvases) - - // 3. Recalculate the viewports location on the off screen canvas - this._resize(viewports, offScreenCanvasWidth, offScreenCanvasHeight) - - // 4. Reset viewport cameras - viewports.forEach((vp) => { - vp.resetCamera(resetPanZoomForViewPlane) - }) - - // 5. If render is immediate: Render all - if (immediate === true) { - this.render() - } - } - /** * Calculates the location of the provided viewport on the offScreenCanvas * @@ -578,45 +985,6 @@ class RenderingEngine implements IRenderingEngine { } } - /** - * @method getScene Returns the scene, only scenes with SceneUID (not internal) - * are returned - * @param {string} sceneUID The UID of the scene to fetch. - * - * @returns {Scene} The scene object. - */ - public getScene(sceneUID: string): Scene { - this._throwIfDestroyed() - - // Todo: should the volume be decached? - return this._scenes.get(sceneUID) - } - - /** - * @method getScenes Returns an array of all `Scene`s on the `RenderingEngine` instance. - * - * @returns {Scene} The scene object. - */ - public getScenes(): Array { - this._throwIfDestroyed() - - return Array.from(this._scenes.values()).filter((s) => { - // Do not return Scenes not explicitly created by the user - return s.getIsInternalScene() === false - }) - } - - /** - * @method getScenes Returns an array of all `Scene`s on the `RenderingEngine` instance. - * - * @returns {Scene} The scene object. - */ - public removeScene(sceneUID: string): void { - this._throwIfDestroyed() - - this._scenes.delete(sceneUID) - } - /** * @method _getViewportsAsArray Returns an array of all viewports * @@ -626,71 +994,6 @@ class RenderingEngine implements IRenderingEngine { return Array.from(this._viewports.values()) } - /** - * @method getViewport Returns the viewport by UID - * - * @returns {StackViewport | VolumeViewport} viewport - */ - public getViewport(uid: string): StackViewport | VolumeViewport { - return this._viewports.get(uid) - } - - /** - * @method getViewportsContainingVolumeUID Returns the viewport containing the volumeUID - * - * @returns {VolumeViewport} viewports - */ - public getViewportsContainingVolumeUID(uid: string): Array { - const viewports = this._getViewportsAsArray() as Array - return viewports.filter((vp) => { - const volActors = vp.getDefaultActor() - return volActors.volumeActor && volActors.uid === uid - }) - } - - /** - * @method getScenesContainingVolume Returns the scenes containing the volumeUID - * - * @returns {Scene} scenes - */ - public getScenesContainingVolume(uid: string): Array { - const scenes = this.getScenes() - return scenes.filter((scene) => { - const volumeActors = scene.getVolumeActors() - const firstActor = volumeActors[0] - return firstActor.volumeActor && firstActor.uid === uid - }) - } - - /** - * @method getViewports Returns an array of all `Viewport`s on the `RenderingEngine` instance. - * - * @returns {Viewport} The scene object. - */ - public getViewports(): Array { - this._throwIfDestroyed() - - return this._getViewportsAsArray() - } - - /** - * Filters all the available viewports and return the stack viewports - * @returns stack viewports registered on the rendering Engine - */ - public getStackViewports(): Array { - this._throwIfDestroyed() - - const viewports = this.getViewports() - - const isStackViewport = ( - viewport: StackViewport | VolumeViewport - ): viewport is StackViewport => { - return viewport instanceof StackViewport - } - - return viewports.filter(isStackViewport) - } - private _setViewportsToBeRenderedNextFrame(viewportUIDs: string[]) { // Add the viewports to the set of flagged viewports viewportUIDs.forEach((viewportUID) => { @@ -701,16 +1004,6 @@ class RenderingEngine implements IRenderingEngine { this._render() } - /** - * @method render Renders all viewports on the next animation frame. - */ - public render(): void { - const viewports = this.getViewports() - const viewportUIDs = viewports.map((vp) => vp.uid) - - this._setViewportsToBeRenderedNextFrame(viewportUIDs) - } - /** * @method _render Sets up animation frame if necessary */ @@ -733,45 +1026,17 @@ class RenderingEngine implements IRenderingEngine { private _renderFlaggedViewports = () => { this._throwIfDestroyed() - const { offscreenMultiRenderWindow } = this - const renderWindow = offscreenMultiRenderWindow.getRenderWindow() - - const renderers = offscreenMultiRenderWindow.getRenderers() - - for (let i = 0; i < renderers.length; i++) { - const { renderer, uid } = renderers[i] - - // Requesting viewports that need rendering to be rendered only - if (this._needsRender.has(uid)) { - renderer.setDraw(true) - } else { - renderer.setDraw(false) - } - } - - renderWindow.render() - - // After redraw we set all renderers to not render until necessary - for (let i = 0; i < renderers.length; i++) { - renderers[i].renderer.setDraw(false) + if (!this.useCPURendering) { + this.performVtkDrawCall() } - const openGLRenderWindow = - offscreenMultiRenderWindow.getOpenGLRenderWindow() - const context = openGLRenderWindow.get3DContext() - - const offScreenCanvas = context.canvas - const viewports = this._getViewportsAsArray() - const eventDataArray = [] + for (let i = 0; i < viewports.length; i++) { const viewport = viewports[i] if (this._needsRender.has(viewport.uid)) { - const eventData = this._renderViewportToCanvas( - viewport, - offScreenCanvas - ) + const eventData = this.renderViewportUsingCustomOrVtkPipeline(viewport) eventDataArray.push(eventData) // This viewport has been rendered, we can remove it from the set @@ -794,45 +1059,73 @@ class RenderingEngine implements IRenderingEngine { } /** - * @method renderScene Renders only a specific `Scene` on the next animation frame. - * - * @param {string} sceneUID The UID of the scene to render. + * Performs the single `vtk.js` draw call which is used to render the offscreen + * canvas for vtk.js. This is a bulk rendering step for all Volume and Stack + * viewports when GPU rendering is available. */ - public renderScene(sceneUID: string): void { - const scene = this.getScene(sceneUID) - const viewportUIDs = scene.getViewportUIDs() + private performVtkDrawCall() { + // Render all viewports under vtk.js' control. + const { offscreenMultiRenderWindow } = this + const renderWindow = offscreenMultiRenderWindow.getRenderWindow() - this._setViewportsToBeRenderedNextFrame(viewportUIDs) - } + const renderers = offscreenMultiRenderWindow.getRenderers() - public renderFrameOfReference = (FrameOfReferenceUID: string): void => { - const viewports = this._getViewportsAsArray() - const viewportUidsWithSameFrameOfReferenceUID = viewports.map((vp) => { - if (vp.getFrameOfReferenceUID() === FrameOfReferenceUID) { - return vp.uid + if (!renderers.length) { + return + } + + for (let i = 0; i < renderers.length; i++) { + const { renderer, uid } = renderers[i] + + // Requesting viewports that need rendering to be rendered only + if (this._needsRender.has(uid)) { + renderer.setDraw(true) + } else { + renderer.setDraw(false) } - }) + } - return this.renderViewports(viewportUidsWithSameFrameOfReferenceUID) - } + renderWindow.render() - /** - * @method renderScenes Renders the provided Scene UIDs. - * - * @returns{void} - */ - public renderScenes(sceneUIDs: Array): void { - const scenes = sceneUIDs.map((sUid) => this.getScene(sUid)) - this._renderScenes(scenes) + // After redraw we set all renderers to not render until necessary + for (let i = 0; i < renderers.length; i++) { + renderers[i].renderer.setDraw(false) + } } /** - * @method renderViewports Renders the provided Viewport UIDs. + * @method renderViewportUsingCustomOrVtkPipeline Renders the given viewport + * using its preffered method. * - * @returns{void} + * @param {(StackViewport | VolumeViewport)} viewport The viewport to render */ - public renderViewports(viewportUIDs: Array): void { - this._setViewportsToBeRenderedNextFrame(viewportUIDs) + private renderViewportUsingCustomOrVtkPipeline( + viewport: StackViewport | VolumeViewport + ): unknown { + let eventData + + if (viewportTypeUsesCustomRenderingPipeline(viewport.type) === true) { + eventData = viewport.customRenderViewportToCanvas() + } else { + if (this.useCPURendering) { + throw new Error( + 'GPU not available, and using a viewport with no custom render pipeline.' + ) + } + + const { offscreenMultiRenderWindow } = this + const openGLRenderWindow = + offscreenMultiRenderWindow.getOpenGLRenderWindow() + const context = openGLRenderWindow.get3DContext() + const offScreenCanvas = context.canvas + + eventData = this._renderViewportFromVtkCanvasToOnscreenCanvas( + viewport, + offScreenCanvas + ) + } + + return eventData } /** @@ -856,20 +1149,11 @@ class RenderingEngine implements IRenderingEngine { } /** - * @method renderViewport Renders only a specific `Viewport` on the next animation frame. - * - * @param {string} viewportUID The UID of the viewport. - */ - public renderViewport(viewportUID: string): void { - this._setViewportsToBeRenderedNextFrame([viewportUID]) - } - - /** - * @method _renderViewportToCanvas Renders a particular `Viewport`'s on screen canvas. + * @method _renderViewportFromVtkCanvasToOnscreenCanvas Renders a particular `Viewport`'s on screen canvas. * @param {Viewport} viewport The `Viewport` to render. * @param {object} offScreenCanvas The offscreen canvas to render from. */ - private _renderViewportToCanvas( + private _renderViewportFromVtkCanvasToOnscreenCanvas( viewport: StackViewport | VolumeViewport, offScreenCanvas ): { @@ -962,27 +1246,6 @@ class RenderingEngine implements IRenderingEngine { this._scenes = new Map() } - /** - * @method destroy the rendering engine - */ - public destroy(): void { - if (this.hasBeenDestroyed) { - return - } - - this._reset() - - // Free up WebGL resources - this.offscreenMultiRenderWindow.delete() - - renderingEngineCache.delete(this.uid) - - // Make sure all references go stale and are garbage collected. - delete this.offscreenMultiRenderWindow - - this.hasBeenDestroyed = true - } - /** * @method _throwIfDestroyed Throws an error if trying to interact with the `RenderingEngine` * instance after its `destroy` method has been called. diff --git a/packages/cornerstone-render/src/RenderingEngine/StackViewport.ts b/packages/cornerstone-render/src/RenderingEngine/StackViewport.ts index 628005d092..9425b8e7f7 100644 --- a/packages/cornerstone-render/src/RenderingEngine/StackViewport.ts +++ b/packages/cornerstone-render/src/RenderingEngine/StackViewport.ts @@ -4,13 +4,18 @@ import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume' import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper' import _cloneDeep from 'lodash.clonedeep' import vtkCamera from 'vtk.js/Sources/Rendering/Core/Camera' -import { vec3 } from 'gl-matrix' +import { vec2, vec3 } from 'gl-matrix' import metaData from '../metaData' import Viewport from './Viewport' import eventTarget from '../eventTarget' import EVENTS from '../enums/events' -import { triggerEvent, isEqual, invertRgbTransferFunction } from '../utilities' +import { + triggerEvent, + isEqual, + invertRgbTransferFunction, + windowLevel as windowLevelUtil, +} from '../utilities' import { Point2, Point3, @@ -20,15 +25,31 @@ import { IImage, ScalingParameters, IImageData, + CPUIImageData, PetScaling, Scaling, StackProperties, + FlipDirection, + ActorEntry, + CPUFallbackEnabledElement, + CPUFallbackColormapData, } from '../types' +import drawImageSync from './helpers/cpuFallback/drawImageSync' +import { getColormap } from './helpers/cpuFallback/colors/index' import { loadAndCacheImage } from '../imageLoader' import requestPoolManager from '../requestPool/requestPoolManager' import ERROR_CODES from '../enums/errorCodes' import INTERPOLATION_TYPE from '../constants/interpolationType' +import canvasToPixel from './helpers/cpuFallback/rendering/canvasToPixel' +import pixelToCanvas from './helpers/cpuFallback/rendering/pixelToCanvas' +import getDefaultViewport from './helpers/cpuFallback/rendering/getDefaultViewport' +import calculateTransform from './helpers/cpuFallback/rendering/calculateTransform' +import resize from './helpers/cpuFallback/rendering/resize' + +import resetCamera from './helpers/cpuFallback/rendering/resetCamera' +import { Transform } from './helpers/cpuFallback/rendering/transform' +import { getShouldUseCPURendering } from '../init' const EPSILON = 1 // Slice Thickness @@ -40,8 +61,11 @@ interface ImageDataMetaData { dimensions: Point3 spacing: Point3 numVoxels: number + imagePlaneModule: unknown + imagePixelModule: unknown } +// TODO This needs to be exposed as its published to consumers. type CalibrationEvent = { rowScale: number columnScale: number @@ -65,12 +89,18 @@ class StackViewport extends Viewport { // Helpers private _imageData: vtkImageData private cameraPosOnRender: Point3 - private invalidated = false // if true -> new actor is forced to be created for the stack + private stackInvalidated = false // if true -> new actor is forced to be created for the stack private panCache: Point3 - private shouldInvert = false // since invert is getting applied on the actor we should track it + private shouldFlip = false + private voiApplied = false private rotationCache = 0 private _publishCalibratedEvent = false private _calibrationEvent: CalibrationEvent + private _cpuFallbackEnabledElement?: CPUFallbackEnabledElement + // CPU fallback + private useCPURendering: boolean + private cpuImagePixelData: number[] + private cpuRenderingInvalidated: boolean // TODO: These should not be here and will be nuked public modality: string // this is needed for tools @@ -80,22 +110,34 @@ class StackViewport extends Viewport { super(props) this.scaling = {} this.modality = null - const renderer = this.getRenderer() - const camera = vtkCamera.newInstance() - renderer.setActiveCamera(camera) + this.useCPURendering = getShouldUseCPURendering() + + if (this.useCPURendering) { + this._cpuFallbackEnabledElement = { + canvas: this.canvas, + renderingTools: {}, + transform: new Transform(), + viewport: {}, + } + } else { + const renderer = this.getRenderer() + const camera = vtkCamera.newInstance() + renderer.setActiveCamera(camera) - const sliceNormal = [0, 0, -1] - const viewUp = [0, -1, 0] + const sliceNormal = [0, 0, -1] + const viewUp = [0, -1, 0] + + camera.setDirectionOfProjection( + -sliceNormal[0], + -sliceNormal[1], + -sliceNormal[2] + ) + camera.setViewUp(...viewUp) + camera.setParallelProjection(true) + // @ts-ignore: vtkjs incorrect typing + camera.setFreezeFocalPoint(true) + } - camera.setDirectionOfProjection( - -sliceNormal[0], - -sliceNormal[1], - -sliceNormal[2] - ) - camera.setViewUp(...viewUp) - camera.setParallelProjection(true) - // @ts-ignore: vtkjs incorrect typing - camera.setFreezeFocalPoint(true) this.imageIds = [] this.currentImageIdIndex = 0 this.panCache = [0, 0, 0] @@ -103,6 +145,16 @@ class StackViewport extends Viewport { this.resetCamera() } + /** + * Custom resize method + */ + public resize = (): void => { + // GPU viewport resize is handled inside the RenderingEngine + if (this.useCPURendering) { + this._resizeCPU() + } + } + /** * Returns the image and its properties that is being shown inside the * stack viewport. It returns, the image dimensions, image direction, @@ -110,7 +162,21 @@ class StackViewport extends Viewport { * * @returns IImageData: {dimensions, direction, scalarData, vtkImageData, metadata, scaling} */ - public getImageData(): IImageData | undefined { + public getImageData(): IImageData | CPUIImageData { + if (this.useCPURendering) { + return this.getImageDataCPU() + } else { + return this.getImageDataGPU() + } + } + + private _resizeCPU = (): void => { + if (this._cpuFallbackEnabledElement.viewport) { + resize(this._cpuFallbackEnabledElement) + } + } + + private getImageDataGPU(): IImageData | undefined { const actor = this.getDefaultActor() if (!actor) { @@ -125,12 +191,43 @@ class StackViewport extends Viewport { origin: vtkImageData.getOrigin(), direction: vtkImageData.getDirection(), scalarData: vtkImageData.getPointData().getScalars().getData(), - vtkImageData: volumeActor.getMapper().getInputData(), + imageData: volumeActor.getMapper().getInputData(), metadata: { Modality: this.modality }, scaling: this.scaling, } } + private getImageDataCPU(): CPUIImageData | undefined { + const { metadata } = this._cpuFallbackEnabledElement + + return { + dimensions: metadata.dimensions as Point3, + spacing: metadata.spacing as Point3, + origin: metadata.origin as Point3, + direction: metadata.direction as Float32Array, + metadata: { Modality: this.modality }, + scaling: this.scaling, + imageData: { + worldToIndex: (point: Point3) => { + const canvasPoint = this.worldToCanvasCPU(point) + const pixelCoord = canvasToPixel( + this._cpuFallbackEnabledElement, + canvasPoint + ) + return [pixelCoord[0], pixelCoord[1], 0] + }, + indexToWorld: (point: Point3) => { + const canvasPoint = pixelToCanvas(this._cpuFallbackEnabledElement, [ + point[0], + point[1], + ]) + return this.canvasToWorldCPU(canvasPoint) + }, + }, + scalarData: this.cpuImagePixelData, + } + } + /** * Returns the frame of reference UID, if the image doesn't have imagePlaneModule * metadata, it returns undefined, otherwise, frameOfReferenceUID is returned. @@ -237,7 +334,11 @@ class StackViewport extends Viewport { // }) let imagePlaneModule = metaData.get('imagePlaneModule', imageId) - imagePlaneModule = this.calibrateIfNecessary(imageId, imagePlaneModule) + + // Todo: for now, it gives error for getImageData + if (!this.useCPURendering) { + imagePlaneModule = this.calibrateIfNecessary(imageId, imagePlaneModule) + } return { imagePlaneModule, @@ -277,11 +378,11 @@ class StackViewport extends Viewport { calibratedPixelSpacing // Check if there is already an actor - const imageData = this.getImageData() + const imageDataMetadata = this.getImageData() // If no actor (first load) and calibration matches the dicom header if ( - !imageData && + !imageDataMetadata && imagePlaneModule.rowPixelSpacing === calibratedRowSpacing && imagePlaneModule.columnPixelSpacing === calibratedColumnSpacing ) { @@ -291,7 +392,7 @@ class StackViewport extends Viewport { // If no actor (first load) and calibration doesn't match headers // -> needs calibration if ( - !imageData && + !imageDataMetadata && (imagePlaneModule.rowPixelSpacing !== calibratedRowSpacing || imagePlaneModule.columnPixelSpacing !== calibratedColumnSpacing) ) { @@ -310,8 +411,8 @@ class StackViewport extends Viewport { } // If there is already an actor, check if calibration is needed for the current actor - const { vtkImageData } = imageData - const [columnPixelSpacing, rowPixelSpacing] = vtkImageData.getSpacing() + const { imageData } = imageDataMetadata + const [columnPixelSpacing, rowPixelSpacing] = imageData.getSpacing() imagePlaneModule.rowPixelSpacing = calibratedRowSpacing imagePlaneModule.columnPixelSpacing = calibratedColumnSpacing @@ -336,47 +437,6 @@ class StackViewport extends Viewport { return imagePlaneModule } - /** - * Applies the properties to the volume actor. - * @param actor VolumeActor - */ - private applyProperties(volumeActor) { - const tfunc = volumeActor.getProperty().getRGBTransferFunction(0) - - // apply voiRange if defined - if (typeof this.voiRange !== 'undefined') { - const { lower, upper } = this.voiRange - tfunc.setRange(lower, upper) - } else { - const imageData = volumeActor.getMapper().getInputData() - const range = imageData.getPointData().getScalars().getRange() - tfunc.setRange(range[0], range[1]) - this.voiRange = { lower: range[0], upper: range[1] } - } - - // apply invert if defined - if (this.shouldInvert) { - invertRgbTransferFunction(tfunc) - this.shouldInvert = false - } - - // change interpolation if defined - if (typeof this.interpolationType !== 'undefined') { - const volumeProperty = volumeActor.getProperty() - volumeProperty.setInterpolationType(this.interpolationType) - } - - // apply rotation - if (this.rotationCache !== this.rotation) { - // Moving back to zero rotation, for new scrolled slice rotation is 0 after camera reset - this.getVtkActiveCamera().roll(-this.rotationCache) - - // rotating camera to the new value - this.getVtkActiveCamera().roll(this.rotation) - this.rotationCache = this.rotation - } - } - /** * Sets the properties for the viewport on the default actor. Properties include * setting the VOI, inverting the colors and setting the interpolation type @@ -389,39 +449,48 @@ class StackViewport extends Viewport { invert, interpolationType, rotation, + flipHorizontal, + flipVertical, }: StackProperties = {}): void { - if (typeof voiRange !== 'undefined') { - this.voiRange = voiRange + // if voi is not applied for the first time, run the setVOI function + // which will apply the default voi + if (typeof voiRange !== 'undefined' || !this.voiApplied) { + this.setVOI(voiRange) } if (typeof invert !== 'undefined') { - this.shouldInvert = invert !== this.invert - this.invert = invert + this.setInvertColor(invert) } if (typeof interpolationType !== 'undefined') { - this.interpolationType = interpolationType + this.setInterpolationType(interpolationType) } if (typeof rotation !== 'undefined') { - this.rotation = rotation + if (this.rotationCache !== rotation) { + this.setRotation(this.rotationCache, rotation) + } } - const actor = this.getDefaultActor() - if (actor?.volumeActor) { - this.applyProperties(actor.volumeActor) + if ( + typeof flipHorizontal !== 'undefined' || + typeof flipVertical !== 'undefined' + ) { + this.setFlipDirection({ flipHorizontal, flipVertical }) } } /** * Retrieve the viewport properties */ - public getProperties(): StackProperties { + public getProperties = (): StackProperties => { return { voiRange: this.voiRange, - rotation: this.rotation, + rotation: this.rotationCache, interpolationType: this.interpolationType, invert: this.invert, + flipHorizontal: this.flipHorizontal, + flipVertical: this.flipVertical, } } @@ -429,18 +498,326 @@ class StackViewport extends Viewport { * Reset the viewport properties */ public resetProperties(): void { - this.voiRange = undefined - this.rotation = 0 - this.interpolationType = INTERPOLATION_TYPE.LINEAR + this.cpuRenderingInvalidated = true + + this.fillWithBackgroundColor() + + if (this.useCPURendering) { + this._cpuFallbackEnabledElement.renderingTools = {} + } + + this._resetProperties() + + this.render() + } + + public getCamera(): ICamera { + if (this.useCPURendering) { + return this.getCameraCPU() + } else { + return super.getCamera() + } + } + + public setCamera(cameraInterface: ICamera): void { + if (this.useCPURendering) { + this.setCameraCPU(cameraInterface) + } else { + super.setCamera(cameraInterface) + } + } + + private _resetProperties() { + // to force the default voi to be applied on the next render + this.voiApplied = false + + this.setProperties({ + voiRange: undefined, + rotation: 0, + interpolationType: INTERPOLATION_TYPE.LINEAR, + invert: false, + flipHorizontal: false, + flipVertical: false, + }) + } + + private _setPropertiesFromCache(): void { + this.setProperties({ + voiRange: this.voiRange, + rotation: this.rotation, + interpolationType: this.interpolationType, + invert: this.invert, + flipHorizontal: this.flipHorizontal, + flipVertical: this.flipVertical, + }) + } + + private getCameraCPU(): Partial { + const { metadata, viewport } = this._cpuFallbackEnabledElement + + const { direction } = metadata + + // focalPoint and position of CPU camera is just a placeholder since + // tools need focalPoint to be defined + const viewPlaneNormal = direction.slice(6, 9).map((x) => -x) + const viewUp = direction.slice(3, 6).map((x) => -x) + return { + parallelProjection: true, + focalPoint: [0, 0, 0], + position: [0, 0, 0], + parallelScale: viewport.scale, + viewPlaneNormal: [ + viewPlaneNormal[0], + viewPlaneNormal[1], + viewPlaneNormal[2], + ], + viewUp: [viewUp[0], viewUp[1], viewUp[2]], + } + } + + private setCameraCPU(cameraInterface: ICamera): void { + const { viewport } = this._cpuFallbackEnabledElement + const previousCamera = this.getCameraCPU() + + const { focalPoint, viewUp, parallelScale } = cameraInterface + + if (focalPoint) { + const focalPointCanvas = this.worldToCanvasCPU(cameraInterface.focalPoint) + const previousFocalPointCanvas = this.worldToCanvasCPU( + previousCamera.focalPoint + ) + + const deltaCanvas = vec2.create() + + vec2.subtract( + deltaCanvas, + vec2.fromValues( + previousFocalPointCanvas[0], + previousFocalPointCanvas[1] + ), + vec2.fromValues(focalPointCanvas[0], focalPointCanvas[1]) + ) + + viewport.translation.x += deltaCanvas[0] / previousCamera.parallelScale + viewport.translation.y += deltaCanvas[1] / previousCamera.parallelScale + } + + // If manipulating scale + if (parallelScale && previousCamera.parallelScale !== parallelScale) { + // Note: as parallel scale is defined differently to the GPU version, + // We instead need to find the difference and move the camera in + // the other direction in this adapter. + + const diff = previousCamera.parallelScale - parallelScale + + viewport.scale += diff // parallelScale; //viewport.scale < 0.1 ? 0.1 : viewport.scale; + } - // Ensure that the invert setting is applied properly - this.shouldInvert = this.invert === true - this.invert = false + const updatedCamera = { + ...previousCamera, + focalPoint, + viewUp, + parallelScale, + } + + const eventDetail = { + previousCamera, + camera: updatedCamera, + canvas: this.canvas, + viewportUID: this.uid, + renderingEngineUID: this.renderingEngineUID, + } + + triggerEvent(this.canvas, EVENTS.CAMERA_MODIFIED, eventDetail) + } + + private setFlipDirection(flipDirection: FlipDirection): void { + if (this.useCPURendering) { + this.setFlipCPU(flipDirection) + } else { + super.flip(flipDirection) + } + this.shouldFlip = false + } + + private setFlipCPU({ flipHorizontal, flipVertical }: FlipDirection): void { + const { viewport } = this._cpuFallbackEnabledElement + + if (typeof flipHorizontal !== 'undefined') { + viewport.hflip = flipHorizontal + this.flipHorizontal = viewport.hflip + } + + if (typeof flipVertical !== 'undefined') { + viewport.vflip = flipVertical + this.flipVertical = viewport.vflip + } + } + + private setVOI(voiRange: VOIRange): void { + if (this.useCPURendering) { + this.setVOICPU(voiRange) + return + } + + this.setVOIGPU(voiRange) + } + + private setRotation(rotationCache: number, rotation: number): void { + if (this.useCPURendering) { + this.setRotationCPU(rotationCache, rotation) + return + } + + this.setRotationGPU(rotationCache, rotation) + } + private setInterpolationType(interpolationType: number): void { + if (this.useCPURendering) { + this.setInterpolationTypeCPU(interpolationType) + return + } + + this.setInterpolationTypeGPU(interpolationType) + } + private setInvertColor(invert: boolean): void { + if (this.useCPURendering) { + this.setInvertColorCPU(invert) + return + } + + this.setInvertColorGPU(invert) + } + + public setRotationCPU(rotationCache: number, rotation: number): void { + const { viewport } = this._cpuFallbackEnabledElement + + viewport.rotation = rotation + this.rotationCache = rotation + this.rotation = rotation + } + + public setRotationGPU(rotationCache: number, rotation: number): void { + // Moving back to zero rotation, for new scrolled slice rotation is 0 after camera reset + this.getVtkActiveCamera().roll(rotationCache) + + // rotating camera to the new value + this.getVtkActiveCamera().roll(-rotation) + this.rotationCache = rotation + this.rotation = rotation + } + + private setInterpolationTypeGPU(interpolationType: number): void { + const actor = this.getDefaultActor() + + if (!actor) { + return + } + + const { volumeActor } = actor + const volumeProperty = volumeActor.getProperty() + volumeProperty.setInterpolationType(interpolationType) + this.interpolationType = interpolationType + } + + private setInterpolationTypeCPU(interpolationType: number): void { + const { viewport } = this._cpuFallbackEnabledElement + if (interpolationType === INTERPOLATION_TYPE.LINEAR) { + viewport.pixelReplication = false + } else { + viewport.pixelReplication = true + } + this.interpolationType = interpolationType + } + + private setInvertColorCPU(invert: boolean): void { + const { viewport } = this._cpuFallbackEnabledElement + + if (!viewport) { + return + } + + viewport.invert = invert + this.invert = invert + } + + private setInvertColorGPU(invert: boolean): void { const actor = this.getDefaultActor() - if (actor?.volumeActor) { - this.applyProperties(actor.volumeActor) + + if (!actor) { + return + } + + const { volumeActor } = actor + const tfunc = volumeActor.getProperty().getRGBTransferFunction(0) + + if ((!this.invert && invert) || (this.invert && !invert)) { + invertRgbTransferFunction(tfunc) + } + this.invert = invert + } + + private setVOICPU(voiRange: VOIRange): void { + const { viewport, image } = this._cpuFallbackEnabledElement + + if (!viewport || !image) { + return + } + + if (typeof voiRange === 'undefined') { + const { windowWidth: ww, windowCenter: wc } = image + + const wwToUse = Array.isArray(ww) ? ww[0] : ww + const wcToUse = Array.isArray(wc) ? wc[0] : wc + viewport.voi = { + windowWidth: wwToUse, + windowCenter: wcToUse, + } + + const { lower, upper } = windowLevelUtil.toLowHighRange(wwToUse, wcToUse) + this.voiRange = { lower, upper } + } else { + const { lower, upper } = voiRange + const { windowCenter, windowWidth } = windowLevelUtil.toWindowLevel( + lower, + upper + ) + + if (!viewport.voi) { + viewport.voi = { + windowWidth: 0, + windowCenter: 0, + } + } + + viewport.voi.windowWidth = windowWidth + viewport.voi.windowCenter = windowCenter + this.voiRange = voiRange + } + + this.voiApplied = true + } + + private setVOIGPU(voiRange: VOIRange): void { + const actor = this.getDefaultActor() + if (!actor) { + return + } + + const { volumeActor } = actor + const tfunc = volumeActor.getProperty().getRGBTransferFunction(0) + + if (typeof voiRange === 'undefined') { + const imageData = volumeActor.getMapper().getInputData() + const range = imageData.getPointData().getScalars().getRange() + tfunc.setRange(range[0], range[1]) + this.voiRange = { lower: range[0], upper: range[1] } + } else { + const { lower, upper } = voiRange + tfunc.setRange(lower, upper) + this.voiRange = voiRange } + this.voiApplied = true } /** @@ -561,6 +938,8 @@ class StackViewport extends Viewport { dimensions: [xVoxels, yVoxels, zVoxels], spacing: [xSpacing, ySpacing, zSpacing], numVoxels: xVoxels * yVoxels * zVoxels, + imagePlaneModule, + imagePixelModule, } } @@ -648,29 +1027,28 @@ class StackViewport extends Viewport { * @param currentImageIdIndex number representing the index of the initial image to be displayed * @param callbacks list of function that runs on the volume actor */ - public setStack(imageIds: Array, currentImageIdIndex = 0): void { + public async setStack( + imageIds: Array, + currentImageIdIndex = 0 + ): Promise { this.imageIds = imageIds this.currentImageIdIndex = currentImageIdIndex - this.invalidated = true + this.stackInvalidated = true + this.rotationCache = 0 + this.flipVertical = false + this.flipHorizontal = false - const { canvas, options } = this - const ctx = canvas.getContext('2d') + this._resetProperties() - // Default to black if no background color is set - let fillStyle - if (options && options.background) { - const rgb = options.background.map((f) => Math.floor(255 * f)) - fillStyle = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})` - } else { - fillStyle = 'black' - } + this.fillWithBackgroundColor() - // We draw over the previous stack with the background color while we - // wait for the next stack to load - ctx.fillStyle = fillStyle - ctx.fillRect(0, 0, canvas.width, canvas.height) + if (this.useCPURendering) { + this._cpuFallbackEnabledElement.renderingTools = {} + delete this._cpuFallbackEnabledElement.viewport.colormap + } - this._setImageIdIndex(currentImageIdIndex) + const imageId = await this._setImageIdIndex(currentImageIdIndex) + return imageId } /** @@ -701,8 +1079,8 @@ class StackViewport extends Viewport { if ( xSpacing !== image.rowPixelSpacing || ySpacing !== image.columnPixelSpacing || - xVoxels !== image.rows || - yVoxels !== image.columns || + xVoxels !== image.columns || + yVoxels !== image.rows || !isEqual(imagePlaneModule.rowCosines, rowCosines) || !isEqual(imagePlaneModule.columnCosines, columnCosines) ) { @@ -768,96 +1146,248 @@ class StackViewport extends Viewport { * @param imageId string representing the imageId * @param imageIdIndex index of the imageId in the imageId list */ - private _loadImage(imageId: string, imageIdIndex: number) { - // 1. Load the image using the Image Loader - function successCallback(image, imageIdIndex, imageId) { - const eventData = { - image, - imageId, - viewportUID: this.uid, - renderingEngineUID: this.renderingEngineUID, - } + private async _loadImage( + imageId: string, + imageIdIndex: number + ): Promise { + if (this.useCPURendering) { + await this._loadImageCPU(imageId, imageIdIndex) + } else { + await this._loadImageGPU(imageId, imageIdIndex) + } + + return imageId + } + + private async _loadImageCPU( + imageId: string, + imageIdIndex: number + ): Promise { + return new Promise((resolve, reject) => { + // 1. Load the image using the Image Loader + function successCallback(image, imageIdIndex, imageId) { + const eventData = { + image, + imageId, + viewportUID: this.uid, + renderingEngineUID: this.renderingEngineUID, + } + + triggerEvent(this.canvas, EVENTS.STACK_NEW_IMAGE, eventData) - triggerEvent(this.canvas, EVENTS.STACK_NEW_IMAGE, eventData) + const metadata = this._getImageDataMetadata(image) - this._updateActorToDisplayImageId(image) + const viewport = getDefaultViewport( + this.canvas, + image, + this.modality, + this._cpuFallbackEnabledElement.viewport.colormap + ) + + this._cpuFallbackEnabledElement.image = image + this._cpuFallbackEnabledElement.metadata = { + ...metadata, + } + this.cpuImagePixelData = image.getPixelData() + + const viewportSettingToUse = Object.assign( + {}, + viewport, + this._cpuFallbackEnabledElement.viewport + ) + + // Important: this.stackInvalidated is different than cpuRenderingInvalidated. The + // former is being used to maintain the previous state of the viewport + // in the same stack, the latter is used to trigger drawImageSync + this._cpuFallbackEnabledElement.viewport = this.stackInvalidated + ? viewport + : viewportSettingToUse + + // used the previous state of the viewport, then stackInvalidated is set to false + this.stackInvalidated = false + + // new viewport is set to the current viewport, then cpuRenderingInvalidated is set to true + this.cpuRenderingInvalidated = true + + this._cpuFallbackEnabledElement.transform = calculateTransform( + this._cpuFallbackEnabledElement + ) + + // Todo: trigger an event to allow applications to hook into END of loading state + // Currently we use loadHandlerManagers for this + + // Perform this check after the image has finished loading + // in case the user has already scrolled away to another image. + // In that case, do not render this image. + if (this.currentImageIdIndex !== imageIdIndex) { + return + } - // Todo: trigger an event to allow applications to hook into END of loading state - // Currently we use loadHandlerManagers for this + // Trigger the image to be drawn on the next animation frame + this.render() - // Perform this check after the image has finished loading - // in case the user has already scrolled away to another image. - // In that case, do not render this image. - if (this.currentImageIdIndex !== imageIdIndex) { - return + // Update the viewport's currentImageIdIndex to reflect the newly + // rendered image + this.currentImageIdIndex = imageIdIndex + resolve(imageId) } - // Trigger the image to be drawn on the next animation frame - this.render() + function errorCallback(error, imageIdIndex, imageId) { + const eventData = { + error, + imageIdIndex, + imageId, + } - // Update the viewport's currentImageIdIndex to reflect the newly - // rendered image - this.currentImageIdIndex = imageIdIndex - } + triggerEvent(eventTarget, ERROR_CODES.IMAGE_LOAD_ERROR, eventData) + reject(error) + } - function errorCallback(error, imageIdIndex, imageId) { - const eventData = { - error, - imageIdIndex, - imageId, + function sendRequest(imageId, imageIdIndex, options) { + return loadAndCacheImage(imageId, options).then( + (image) => { + successCallback.call(this, image, imageIdIndex, imageId) + }, + (error) => { + errorCallback.call(this, error, imageIdIndex, imageId) + } + ) } - triggerEvent(eventTarget, ERROR_CODES.IMAGE_LOAD_ERROR, eventData) - } + const modalityLutModule = metaData.get('modalityLutModule', imageId) || {} + const suvFactor = metaData.get('scalingModule', imageId) || {} + + const generalSeriesModule = + metaData.get('generalSeriesModule', imageId) || {} + + const scalingParameters: ScalingParameters = { + rescaleSlope: modalityLutModule.rescaleSlope, + rescaleIntercept: modalityLutModule.rescaleIntercept, + modality: generalSeriesModule.modality, + suvbw: suvFactor.suvbw, + } - function sendRequest(imageId, imageIdIndex, options) { - return loadAndCacheImage(imageId, options).then( - (image) => { - successCallback.call(this, image, imageIdIndex, imageId) + // Todo: Note that eventually all viewport data is converted into Float32Array, + // we use it here for the purpose of scaling for now. + const type = 'Float32Array' + + const priority = -5 + const requestType = 'interaction' + const additionalDetails = { imageId } + const options = { + targetBuffer: { + type, + offset: null, + length: null, }, - (error) => { - errorCallback.call(this, error, imageIdIndex, imageId) - } + preScale: { + scalingParameters, + }, + } + + requestPoolManager.addRequest( + sendRequest.bind(this, imageId, imageIdIndex, options), + requestType, + additionalDetails, + priority ) - } + }) + } - const modalityLutModule = metaData.get('modalityLutModule', imageId) || {} - const suvFactor = metaData.get('scalingModule', imageId) || {} + private async _loadImageGPU(imageId: string, imageIdIndex: number) { + return new Promise((resolve, reject) => { + // 1. Load the image using the Image Loader + function successCallback(image, imageIdIndex, imageId) { + const eventData = { + image, + imageId, + viewportUID: this.uid, + renderingEngineUID: this.renderingEngineUID, + } - const generalSeriesModule = - metaData.get('generalSeriesModule', imageId) || {} + triggerEvent(this.canvas, EVENTS.STACK_NEW_IMAGE, eventData) - const scalingParameters: ScalingParameters = { - rescaleSlope: modalityLutModule.rescaleSlope, - rescaleIntercept: modalityLutModule.rescaleIntercept, - modality: generalSeriesModule.modality, - suvbw: suvFactor.suvbw, - } + this._updateActorToDisplayImageId(image) - // Todo: Note that eventually all viewport data is converted into Float32Array, - // we use it here for the purpose of scaling for now. - const type = 'Float32Array' + // Todo: trigger an event to allow applications to hook into END of loading state + // Currently we use loadHandlerManagers for this - const priority = -5 - const requestType = 'interaction' - const additionalDetails = { imageId } - const options = { - targetBuffer: { - type, - offset: null, - length: null, - }, - preScale: { - scalingParameters, - }, - } + // Perform this check after the image has finished loading + // in case the user has already scrolled away to another image. + // In that case, do not render this image. + if (this.currentImageIdIndex !== imageIdIndex) { + return + } - requestPoolManager.addRequest( - sendRequest.bind(this, imageId, imageIdIndex, options), - requestType, - additionalDetails, - priority - ) + // Trigger the image to be drawn on the next animation frame + this.render() + + // Update the viewport's currentImageIdIndex to reflect the newly + // rendered image + this.currentImageIdIndex = imageIdIndex + resolve(imageId) + } + + function errorCallback(error, imageIdIndex, imageId) { + const eventData = { + error, + imageIdIndex, + imageId, + } + + triggerEvent(eventTarget, ERROR_CODES.IMAGE_LOAD_ERROR, eventData) + reject(error) + } + + function sendRequest(imageId, imageIdIndex, options) { + return loadAndCacheImage(imageId, options).then( + (image) => { + successCallback.call(this, image, imageIdIndex, imageId) + }, + (error) => { + errorCallback.call(this, error, imageIdIndex, imageId) + } + ) + } + + const modalityLutModule = metaData.get('modalityLutModule', imageId) || {} + const suvFactor = metaData.get('scalingModule', imageId) || {} + + const generalSeriesModule = + metaData.get('generalSeriesModule', imageId) || {} + + const scalingParameters: ScalingParameters = { + rescaleSlope: modalityLutModule.rescaleSlope, + rescaleIntercept: modalityLutModule.rescaleIntercept, + modality: generalSeriesModule.modality, + suvbw: suvFactor.suvbw, + } + + // Todo: Note that eventually all viewport data is converted into Float32Array, + // we use it here for the purpose of scaling for now. + const type = 'Float32Array' + + const priority = -5 + const requestType = 'interaction' + const additionalDetails = { imageId } + const options = { + targetBuffer: { + type, + offset: null, + length: null, + }, + preScale: { + scalingParameters, + }, + } + + requestPoolManager.addRequest( + sendRequest.bind(this, imageId, imageIdIndex, options), + requestType, + additionalDetails, + priority + ) + }) } /** @@ -888,8 +1418,7 @@ class StackViewport extends Viewport { // Cache camera props so we can trigger one camera changed event after // The full transition. const previousCameraProps = _cloneDeep(this.getCamera()) - - if (sameImageData && !this.invalidated) { + if (sameImageData && !this.stackInvalidated) { // 3a. If we can reuse it, replace the scalar data under the hood this._updateVTKImageDataFromCornerstoneImage(image) @@ -929,8 +1458,7 @@ class StackViewport extends Viewport { // Restore rotation for the new slice of the image this.rotationCache = 0 - const stackActor = this.getDefaultActor().volumeActor - this.applyProperties(stackActor) + this._setPropertiesFromCache() return } @@ -965,15 +1493,13 @@ class StackViewport extends Viewport { // activeCamera.setThicknessFromFocalPoint(0.1) activeCamera.setFreezeFocalPoint(true) - // Restore rotation for the new actor - this.rotationCache = 0 - this.shouldInvert = this.invert - this.applyProperties(stackActor) + // set voi for the first time + this.setProperties() // Saving position of camera on render, to cache the panning const { position } = this.getCamera() this.cameraPosOnRender = position - this.invalidated = false + this.stackInvalidated = false if (this._publishCalibratedEvent) { this.triggerCalibrationEvent() @@ -984,7 +1510,7 @@ class StackViewport extends Viewport { * Loads the image based on the provided imageIdIndex * @param imageIdIndex number represents imageId index */ - private _setImageIdIndex(imageIdIndex: number): void { + private async _setImageIdIndex(imageIdIndex: number): Promise { if (imageIdIndex >= this.imageIds.length) { throw new Error( `ImageIdIndex provided ${imageIdIndex} is invalid, the stack only has ${this.imageIds.length} elements` @@ -1000,15 +1526,33 @@ class StackViewport extends Viewport { // Todo: trigger an event to allow applications to hook into START of loading state // Currently we use loadHandlerManagers for this - this._loadImage(imageId, imageIdIndex) + await this._loadImage(imageId, imageIdIndex) + return imageId } /** * Centers Pan and resets the zoom for stack viewport. */ - public resetCamera(): void { - // Since for StackViewport, placing the focal point in the center of the volume - // equals resetting the slice to center + public resetCamera(resetPanZoomForViewPlane = true): void { + if (this.useCPURendering) { + this.resetCameraCPU(resetPanZoomForViewPlane) + } else { + this.resetCameraGPU() + } + } + + private resetCameraCPU(resetPanZoomForViewPlane: boolean) { + const { image } = this._cpuFallbackEnabledElement + + if (!image) { + return + } + + resetCamera(this._cpuFallbackEnabledElement, resetPanZoomForViewPlane) + } + + private resetCameraGPU() { + // Todo: this doesn't work for resetPanZoomForViewPlane = true const resetPanZoomForViewPlane = false this.resetViewportCamera(resetPanZoomForViewPlane) } @@ -1039,7 +1583,7 @@ class StackViewport extends Viewport { */ public calibrateSpacing(imageId: string): void { const imageIdIndex = this.getImageIds().indexOf(imageId) - this.invalidated = true + this.stackInvalidated = true this._loadImage(imageId, imageIdIndex) } @@ -1107,8 +1651,7 @@ class StackViewport extends Viewport { private triggerCalibrationEvent() { // Update the indexToWorld and WorldToIndex for viewport - const { vtkImageData } = this.getImageData() - + const { imageData } = this.getImageData() // Finally emit event for the full camera change cause during load image. const eventDetail = { canvas: this.canvas, @@ -1116,10 +1659,10 @@ class StackViewport extends Viewport { sceneUID: this.sceneUID, renderingEngineUID: this.renderingEngineUID, imageId: this.getCurrentImageId(), - imageData: vtkImageData, + imageData, ...this._calibrationEvent, - indexToWorld: vtkImageData.getIndexToWorld(), - worldToIndex: vtkImageData.getWorldToIndex(), + indexToWorld: imageData.getIndexToWorld(), + worldToIndex: imageData.getWorldToIndex(), } // Let the tools know the image spacing has been calibrated @@ -1138,6 +1681,75 @@ class StackViewport extends Viewport { * @public */ public canvasToWorld = (canvasPos: Point2): Point3 => { + if (this.useCPURendering) { + return this.canvasToWorldCPU(canvasPos) + } + + return this.canvasToWorldGPU(canvasPos) + } + + /** + * @canvasToWorld Returns the canvas coordinates of the given `worldPos` + * projected onto the `Viewport`'s `canvas`. + * + * @param worldPos The position in world coordinates. + * @returns The corresponding canvas coordinates. + * @public + */ + public worldToCanvas = (worldPos: Point3): Point2 => { + if (this.useCPURendering) { + return this.worldToCanvasCPU(worldPos) + } + + return this.worldToCanvasGPU(worldPos) + } + + private canvasToWorldCPU = (canvasPos: Point2): Point3 => { + if (!this._cpuFallbackEnabledElement.image) { + return + } + // compute the pixel coordinate in the image + const [px, py] = canvasToPixel(this._cpuFallbackEnabledElement, canvasPos) + + // convert pixel coordinate to world coordinate + const { origin, spacing, direction } = this.getImageData() + + const worldPos = vec3.fromValues(0, 0, 0) + + // Calculate size of spacing vector in normal direction + const iVector = direction.slice(0, 3) + const jVector = direction.slice(3, 6) + + // Calculate the world coordinate of the pixel + vec3.scaleAndAdd(worldPos, origin, iVector, px * spacing[0]) + vec3.scaleAndAdd(worldPos, worldPos, jVector, py * spacing[1]) + + return worldPos as Point3 + } + + private worldToCanvasCPU = (worldPos: Point3): Point2 => { + // world to pixel + const { spacing, direction, origin } = this.getImageData() + + const iVector = direction.slice(0, 3) + const jVector = direction.slice(3, 6) + + const diff = vec3.subtract(vec3.create(), worldPos, origin) + + const worldPoint = [ + vec3.dot(diff, iVector) / spacing[0], + vec3.dot(diff, jVector) / spacing[1], + ] + + // pixel to canvas + const canvasPoint = pixelToCanvas( + this._cpuFallbackEnabledElement, + worldPoint + ) + return canvasPoint + } + + private canvasToWorldGPU = (canvasPos: Point2): Point3 => { const renderer = this.getRenderer() const offscreenMultiRenderWindow = this.getRenderingEngine().offscreenMultiRenderWindow @@ -1161,15 +1773,7 @@ class StackViewport extends Viewport { return worldCoord } - /** - * @canvasToWorld Returns the canvas coordinates of the given `worldPos` - * projected onto the `Viewport`'s `canvas`. - * - * @param worldPos The position in world coordinates. - * @returns The corresponding canvas coordinates. - * @public - */ - public worldToCanvas = (worldPos: Point3): Point2 => { + private worldToCanvasGPU = (worldPos: Point3) => { const renderer = this.getRenderer() const offscreenMultiRenderWindow = this.getRenderingEngine().offscreenMultiRenderWindow @@ -1216,6 +1820,179 @@ class StackViewport extends Viewport { public getCurrentImageId = (): string => { return this.imageIds[this.currentImageIdIndex] } + + /** + * @method getRenderer Returns the `vtkRenderer` responsible for rendering the `Viewport`. + * + * @returns {object} The `vtkRenderer` for the `Viewport`. + */ + public getRenderer() { + if (this.useCPURendering) { + throw this.getCPUFallbackError('getRenderer') + } + + return super.getRenderer() + } + + public getDefaultActor(): ActorEntry { + if (this.useCPURendering) { + throw this.getCPUFallbackError('getDefaultActor') + } + + return super.getDefaultActor() + } + + public getActors(): Array { + if (this.useCPURendering) { + throw this.getCPUFallbackError('getActors') + } + + return super.getActors() + } + + public getActor(actorUID: string): ActorEntry { + if (this.useCPURendering) { + throw this.getCPUFallbackError('getActor') + } + + return super.getActor(actorUID) + } + + public setActors(actors: Array): void { + if (this.useCPURendering) { + throw this.getCPUFallbackError('setActors') + } + + return super.setActors(actors) + } + + public addActors(actors: Array): void { + if (this.useCPURendering) { + throw this.getCPUFallbackError('addActors') + } + + return super.addActors(actors) + } + + public addActor(actorEntry: ActorEntry): void { + if (this.useCPURendering) { + throw this.getCPUFallbackError('addActor') + } + + return super.addActor(actorEntry) + } + + public removeAllActors(): void { + if (this.useCPURendering) { + throw this.getCPUFallbackError('removeAllActors') + } + + return super.removeAllActors() + } + + private getCPUFallbackError(method: string): Error { + return new Error(`method ${method} cannot be used during CPU Fallback mode`) + } + + static get useCustomRenderingPipeline() { + return getShouldUseCPURendering() + } + + private fillWithBackgroundColor() { + const { canvas, options } = this + const ctx = canvas.getContext('2d') + + // Default to black if no background color is set + let fillStyle + if (options && options.background) { + const rgb = options.background.map((f) => Math.floor(255 * f)) + fillStyle = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})` + } else { + fillStyle = 'black' + } + + // We draw over the previous stack with the background color while we + // wait for the next stack to load + ctx.fillStyle = fillStyle + ctx.fillRect(0, 0, canvas.width, canvas.height) + } + + customRenderViewportToCanvas = () => { + if (!this.useCPURendering) { + throw new Error( + 'Custom cpu rendering pipeline should only be hit in CPU rendering mode' + ) + } + + if (this._cpuFallbackEnabledElement.image) { + drawImageSync( + this._cpuFallbackEnabledElement, + this.cpuRenderingInvalidated + ) + // reset flags + this.cpuRenderingInvalidated = false + } else { + this.fillWithBackgroundColor() + } + + return { + canvas: this.canvas, + viewportUID: this.uid, + sceneUID: this.sceneUID, + renderingEngineUID: this.renderingEngineUID, + } + } + + public setColormap(colormap: CPUFallbackColormapData) { + if (this.useCPURendering) { + this.setColormapCPU(colormap) + } else { + this.setColormapGPU(colormap) + } + } + + public unsetColormap() { + if (this.useCPURendering) { + this.unsetColormapCPU() + } else { + this.unsetColormapGPU() + } + } + + private unsetColormapCPU() { + delete this._cpuFallbackEnabledElement.viewport.colormap + this._cpuFallbackEnabledElement.renderingTools = {} + + this.cpuRenderingInvalidated = true + + this.fillWithBackgroundColor() + + this.render() + } + + private setColormapCPU(colormapData: CPUFallbackColormapData) { + const colormap = getColormap(colormapData.name, colormapData) + + this._cpuFallbackEnabledElement.viewport.colormap = colormap + this._cpuFallbackEnabledElement.renderingTools = {} + + this.fillWithBackgroundColor() + this.cpuRenderingInvalidated = true + + this.render() + } + + private setColormapGPU(colormap: CPUFallbackColormapData) { + // TODO -> vtk has full colormaps which are piecewise and frankly better? + // Do we really want a pre defined 256 color map just for the sake of harmonization? + throw new Error('setColorMapGPU not implemented.') + } + + private unsetColormapGPU() { + // TODO -> vtk has full colormaps which are piecewise and frankly better? + // Do we really want a pre defined 256 color map just for the sake of harmonization? + throw new Error('unsetColormapGPU not implemented.') + } } export default StackViewport diff --git a/packages/cornerstone-render/src/RenderingEngine/Viewport.ts b/packages/cornerstone-render/src/RenderingEngine/Viewport.ts index 024e73aaba..f2b31b98cc 100644 --- a/packages/cornerstone-render/src/RenderingEngine/Viewport.ts +++ b/packages/cornerstone-render/src/RenderingEngine/Viewport.ts @@ -6,13 +6,12 @@ import _cloneDeep from 'lodash.clonedeep' import Events from '../enums/events' import VIEWPORT_TYPE from '../constants/viewportType' -import FlipDirection from '../enums/flipDirection' import { ICamera, ViewportInput, ActorEntry } from '../types' import renderingEngineCache from './renderingEngineCache' import RenderingEngine from './RenderingEngine' -import { triggerEvent, isEqual, planar } from '../utilities' +import { triggerEvent, planar } from '../utilities' import vtkMath from 'vtk.js/Sources/Common/Core/Math' -import { ViewportInputOptions, Point2, Point3 } from '../types' +import { ViewportInputOptions, Point2, Point3, FlipDirection } from '../types' import { vtkSlabCamera } from './vtkClasses' /** @@ -25,6 +24,10 @@ class Viewport { readonly renderingEngineUID: string readonly type: string readonly canvas: HTMLCanvasElement + + protected flipHorizontal = false + protected flipVertical = false + sx: number sy: number sWidth: number @@ -62,76 +65,12 @@ class Viewport { getFrameOfReferenceUID: () => string canvasToWorld: (canvasPos: Point2) => Point3 worldToCanvas: (worldPos: Point3) => Point2 + customRenderViewportToCanvas: () => unknown + resize: () => void + getProperties: () => void - public getIntensityFromWorld(point: Point3): number { - const volumeActor = this.getDefaultActor().volumeActor - const imageData = volumeActor.getMapper().getInputData() - - return imageData.getScalarValueFromWorld(point) - } - - public getDefaultActor(): ActorEntry { - return this.getActors()[0] - } - - public getActors(): Array { - return Array.from(this._actors.values()) - } - - public getActor(actorUID: string): ActorEntry { - return this._actors.get(actorUID) - } - - public setActors(actors: Array): void { - this.removeAllActors() - this.addActors(actors) - } - - public addActors(actors: Array): void { - actors.forEach((actor) => this.addActor(actor)) - } - - public addActor(actorEntry: ActorEntry): void { - const { uid: actorUID, volumeActor } = actorEntry - if (!actorUID || !volumeActor) { - throw new Error('Actors should have uid and vtk volumeActor properties') - } - - const actor = this.getActor(actorUID) - if (actor) { - console.warn(`Actor ${actorUID} already exists for this viewport`) - return - } - - const renderer = this.getRenderer() - renderer.addActor(volumeActor) - this._actors.set(actorUID, Object.assign({}, actorEntry)) - } - - /* - Todo: remove actor and remove actors does not work for some reason - public removeActor(actorUID: string): void { - const actor = this.getActor(actorUID) - if (!actor) { - console.warn(`Actor ${actorUID} does not exist for this viewport`) - return - } - const renderer = this.getRenderer() - renderer.removeViewProp(actor) // removeActor not implemented in vtk? - this._actors.delete(actorUID) - } - - public removeActors(actorUIDs: Array): void { - actorUIDs.forEach((actorUID) => { - this.removeActor(actorUID) - }) - } - */ - - public removeAllActors(): void { - this.getRenderer().removeAllViewProps() - this._actors = new Map() - return + static get useCustomRenderingPipeline() { + return false } /** @@ -180,16 +119,6 @@ class Viewport { } } - /** - * @method getBounds gets the visible bounds of the viewport - * - * @param {any} bounds of the viewport - */ - public getBounds() { - const renderer = this.getRenderer() - return renderer.computeVisiblePropBounds() - } - /** * @method reset Resets the options the `Viewport`'s `defaultOptions`.` * @@ -207,66 +136,141 @@ class Viewport { } protected applyFlipTx = (worldPos: Point3): Point3 => { - // One vol actor is enough to get the flip direction. If not flipped - // the transformation is identity const actor = this.getDefaultActor() if (!actor) { - // Until viewports set up their actors return worldPos } const volumeActor = actor.volumeActor as vtkVolume const mat = volumeActor.getMatrix() - const p1 = worldPos[0] - const p2 = worldPos[1] - const p3 = worldPos[2] - const p4 = 1 - - // Apply flip tx - const newPos = [0, 0, 0, 1] - newPos[0] = p1 * mat[0] + p2 * mat[4] + p3 * mat[8] + p4 * mat[12] - newPos[1] = p1 * mat[1] + p2 * mat[5] + p3 * mat[9] + p4 * mat[13] - newPos[2] = p1 * mat[2] + p2 * mat[6] + p3 * mat[10] + p4 * mat[14] - newPos[3] = p1 * mat[3] + p2 * mat[7] + p3 * mat[11] + p4 * mat[15] + const newPos = vec3.create() + const matT = mat4.create() + mat4.transpose(matT, mat) + vec3.transformMat4(newPos, worldPos, matT) return [newPos[0], newPos[1], newPos[2]] } /** - * Flip the viewport on horizontal or vertical axis + * Flip the viewport on horizontal or vertical axis, this method + * works with vtk-js backed rendering pipeline. * - * @param direction 0 for horizontal, 1 for vertical + * @param flipHorizontal: boolean + * @param flipVertical: boolean */ - public flip = (direction: FlipDirection): void => { + protected flip({ flipHorizontal, flipVertical }: FlipDirection): void { const scene = this.getRenderingEngine().getScene(this.sceneUID) - const scale = [1, 1] - scale[direction] *= -1 + const imageData = this.getDefaultImageData() + + if (!imageData) { + return + } + + // In Cornerstone gpu rendering piepline, the images are positioned + // in the space according to their origin, and direction (even StackViewport + // with one slice only). In order to flip the images, we need to flip them + // around their center axis (either horizontal or vertical). Since the images + // are positioned in the space according to their origin and direction, for a + // proper scaling (flipping), they should be transformed to the origin and + // then flipped. The following code does this transformation. + + const origin = imageData.getOrigin() + const direction = imageData.getDirection() + const spacing = imageData.getSpacing() + const size = imageData.getDimensions() + + const iVector = direction.slice(0, 3) + const jVector = direction.slice(3, 6) + const kVector = direction.slice(6, 9) + + // finding the center of the image + const center = vec3.create() + vec3.scaleAndAdd(center, origin, iVector, (size[0] / 2.0) * spacing[0]) + vec3.scaleAndAdd(center, center, jVector, (size[1] / 2.0) * spacing[1]) + vec3.scaleAndAdd(center, center, kVector, (size[2] / 2.0) * spacing[2]) + + let flipHTx, flipVTx + + const transformToOriginTx = vtkMatrixBuilder + .buildFromRadian() + .identity() + .translate(center[0], center[1], center[2]) + .rotateFromDirections(jVector, [0, 1, 0]) + .rotateFromDirections(iVector, [1, 0, 0]) + + const transformBackFromOriginTx = vtkMatrixBuilder + .buildFromRadian() + .identity() + .rotateFromDirections([1, 0, 0], iVector) + .rotateFromDirections([0, 1, 0], jVector) + .translate(-center[0], -center[1], -center[2]) + + if ( + typeof flipHorizontal !== 'undefined' && + ((flipHorizontal && !this.flipHorizontal) || + (!flipHorizontal && this.flipHorizontal)) + ) { + this.flipHorizontal = flipHorizontal + flipHTx = vtkMatrixBuilder + .buildFromRadian() + .multiply(transformToOriginTx.getMatrix()) + .scale(-1, 1, 1) + .multiply(transformBackFromOriginTx.getMatrix()) + } + + if ( + typeof flipVertical !== 'undefined' && + ((flipVertical && !this.flipVertical) || + (!flipVertical && this.flipVertical)) + ) { + this.flipVertical = flipVertical + flipVTx = vtkMatrixBuilder + .buildFromRadian() + .multiply(transformToOriginTx.getMatrix()) + .scale(1, -1, 1) + .multiply(transformBackFromOriginTx.getMatrix()) + } + + if (!flipVTx && !flipHTx) { + return + } const actors = this.getActors() + actors.forEach((actor) => { const volumeActor = actor.volumeActor as vtkVolume - const tx = vtkMatrixBuilder - .buildFromRadian() - .identity() - .scale(scale[0], scale[1], 1) + const mat = volumeActor.getUserMatrix() + + if (flipHTx) { + mat4.multiply(mat, mat, flipHTx.getMatrix()) + } + + if (flipVTx) { + mat4.multiply(mat, mat, flipVTx.getMatrix()) + } - const mat = mat4.create() - mat4.multiply(mat, volumeActor.getUserMatrix(), tx.getMatrix()) volumeActor.setUserMatrix(mat) this.getRenderingEngine().render() if (scene) { - // If volume viewport const viewports = scene.getViewports() viewports.forEach((vp) => { const { focalPoint, position } = vp.getCamera() - tx.apply(focalPoint) - tx.apply(position) + if (flipVTx) { + flipVTx.apply(focalPoint) + flipVTx.apply(position) + } + + if (flipHTx) { + flipHTx.apply(focalPoint) + flipHTx.apply(position) + } + vp.setCamera({ focalPoint, position, @@ -286,6 +290,50 @@ class Viewport { } } + public getDefaultActor(): ActorEntry { + return this.getActors()[0] + } + + public getActors(): Array { + return Array.from(this._actors.values()) + } + + public getActor(actorUID: string): ActorEntry { + return this._actors.get(actorUID) + } + + public setActors(actors: Array): void { + this.removeAllActors() + this.addActors(actors) + } + + public addActors(actors: Array): void { + actors.forEach((actor) => this.addActor(actor)) + } + + public addActor(actorEntry: ActorEntry): void { + const { uid: actorUID, volumeActor } = actorEntry + if (!actorUID || !volumeActor) { + throw new Error('Actors should have uid and vtk volumeActor properties') + } + + const actor = this.getActor(actorUID) + if (actor) { + console.warn(`Actor ${actorUID} already exists for this viewport`) + return + } + + const renderer = this.getRenderer() + renderer.addActor(volumeActor) + this._actors.set(actorUID, Object.assign({}, actorEntry)) + } + + public removeAllActors(): void { + this.getRenderer().removeAllViewProps() + this._actors = new Map() + return + } + protected resetCameraNoEvent() { this._suppressCameraModifiedEvents = true this.resetViewportCamera() @@ -538,7 +586,7 @@ class Viewport { * * @returns {object} the vtkCamera. */ - public getVtkActiveCamera(): vtkCamera | vtkSlabCamera { + protected getVtkActiveCamera(): vtkCamera | vtkSlabCamera { const renderer = this.getRenderer() return renderer.getActiveCamera() diff --git a/packages/cornerstone-render/src/RenderingEngine/VolumeViewport.ts b/packages/cornerstone-render/src/RenderingEngine/VolumeViewport.ts index 9ef5932556..f50258d6e0 100644 --- a/packages/cornerstone-render/src/RenderingEngine/VolumeViewport.ts +++ b/packages/cornerstone-render/src/RenderingEngine/VolumeViewport.ts @@ -4,16 +4,27 @@ import Viewport from './Viewport' import { ViewportInput, Point2, Point3, IImageData } from '../types' import vtkSlabCamera from './vtkClasses/vtkSlabCamera' -import { ActorEntry } from '../types' +import { ActorEntry, FlipDirection } from '../types' +import { getShouldUseCPURendering } from '../init' /** * An object representing a single viewport, which is a camera * looking into a scene, and an associated target output `canvas`. */ class VolumeViewport extends Viewport { + useCPURendering = false + constructor(props: ViewportInput) { super(props) + this.useCPURendering = getShouldUseCPURendering() + + if (this.useCPURendering) { + throw new Error( + 'VolumeViewports cannot be used whilst CPU Fallback Rendering is enabled.' + ) + } + const renderer = this.getRenderer() const camera = vtkSlabCamera.newInstance() @@ -43,6 +54,51 @@ class VolumeViewport extends Viewport { this.resetCamera() } + static get useCustomRenderingPipeline() { + return false + } + + public getIntensityFromWorld(point: Point3): number { + const volumeActor = this.getDefaultActor().volumeActor + const imageData = volumeActor.getMapper().getInputData() + + return imageData.getScalarValueFromWorld(point) + } + + /** + * @method getBounds gets the visible bounds of the viewport + * + * @param {any} bounds of the viewport + */ + public getBounds() { + const renderer = this.getRenderer() + return renderer.computeVisiblePropBounds() + } + + public flip(flipDirection: FlipDirection): void { + super.flip(flipDirection) + } + + /* + Todo: remove actor and remove actors does not work for some reason + public removeActor(actorUID: string): void { + const actor = this.getActor(actorUID) + if (!actor) { + console.warn(`Actor ${actorUID} does not exist for this viewport`) + return + } + const renderer = this.getRenderer() + renderer.removeViewProp(actor) // removeActor not implemented in vtk? + this._actors.delete(actorUID) + } + + public removeActors(actorUIDs: Array): void { + actorUIDs.forEach((actorUID) => { + this.removeActor(actorUID) + }) + } + */ + /** * Reset the camera for the volume viewport * @param resetPanZoomForViewPlane=false only reset Pan and Zoom, if true, @@ -111,12 +167,20 @@ class VolumeViewport extends Viewport { origin: vtkImageData.getOrigin(), direction: vtkImageData.getDirection(), scalarData: vtkImageData.getPointData().getScalars().getData(), - vtkImageData: volumeActor.getMapper().getInputData(), + imageData: volumeActor.getMapper().getInputData(), metadata: undefined, scaling: undefined, } } + // Todo: expand this to include all properties for volume viewport + public getProperties = (): FlipDirection => { + return { + flipHorizontal: this.flipHorizontal, + flipVertical: this.flipVertical, + } + } + /** * @method _setVolumeActors Attaches the volume actors to the viewport. * diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/colormap.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/colormap.ts new file mode 100644 index 0000000000..54d40d7118 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/colormap.ts @@ -0,0 +1,343 @@ +import LookupTable from './lookupTable'; +import colormapsData from './colormaps'; +import { + CPUFallbackColormap, + CPUFallbackColormapData, + Point4, +} from '../../../../types'; + +const COLOR_TRANSPARENT: Point4 = [0, 0, 0, 0]; + +/** + * Generate linearly spaced vectors + * http://cens.ioc.ee/local/man/matlab/techdoc/ref/linspace.html + * @param {Number} a A number representing the first vector + * @param {Number} b A number representing the second vector + * @param {Number} n The number of linear spaced vectors to generate + * @returns {Array} An array of points representing linear spaced vectors. + * @memberof Colors + */ +function linspace(a: number, b: number, n: number): number[] { + n = n === null ? 100 : n; + + const increment = (b - a) / (n - 1); + const vector = []; + + while (n-- > 0) { + vector.push(a); + a += increment; + } + + // Make sure the last item will always be "b" because most of the + // Time we'll get numbers like 1.0000000000000002 instead of 1. + vector[vector.length - 1] = b; + + return vector; +} + +/** + * Returns the "rank/index" of the element in a sorted array if found or the highest index if not. Uses (binary search) + * @param {Array} array A sorted array to search in + * @param {any} elem the element in the array to search for + * @returns {number} The rank/index of the element in the given array + * @memberof Colors + */ +function getRank(array, elem) { + let left = 0; + let right = array.length - 1; + + while (left <= right) { + const mid = left + Math.floor((right - left) / 2); + const midElem = array[mid]; + + if (midElem === elem) { + return mid; + } else if (elem < midElem) { + right = mid - 1; + } else { + left = mid + 1; + } + } + + return left; +} + +/** + * Find the indices into a sorted array a such that, if the corresponding elements + * In v were inserted before the indices, the order of a would be preserved. + * http://lagrange.univ-lyon1.fr/docs/numpy/1.11.0/reference/generated/numpy.searchsorted.html + * @param {Array} inputArray The array where the values will be inserted + * @param {Array} values An array of the values to be inserted into the inputArray + * @returns {Array} The indices where elements should be inserted to maintain order. + * @memberof Colors + */ +function searchSorted(inputArray, values) { + let i; + const indexes = []; + const len = values.length; + + inputArray.sort(function (a, b) { + return a - b; + }); + + for (i = 0; i < len; i++) { + indexes[i] = getRank(inputArray, values[i]); + } + + return indexes; +} + +/** + * Creates an *N* -element 1-d lookup table + * @param {Number} N The number of elements in the result lookup table + * @param {Array} data represented by a list of x,y0,y1 mapping correspondences. Each element in this + * List represents how a value between 0 and 1 (inclusive) represented by x is mapped to + * A corresponding value between 0 and 1 (inclusive). The two values of y are to allow for + * Discontinuous mapping functions (say as might be found in a sawtooth) where y0 represents + * The value of y for values of x <= to that given, and y1 is the value to be used for x > + * Than that given). The list must start with x=0, end with x=1, and all values of x must be + * In increasing order. Values between the given mapping points are determined by simple linear + * Interpolation. + * @param {any} gamma value denotes a "gamma curve" value which adjusts the brightness + * at the bottom and top of the map. + * @returns {any[]} an array "result" where result[x*(N-1)] gives the closest value for + * Values of x between 0 and 1. + * @memberof Colors + */ +function makeMappingArray(N, data, gamma) { + let i; + const x = []; + const y0 = []; + const y1 = []; + const lut = []; + + gamma = gamma === null ? 1 : gamma; + + for (i = 0; i < data.length; i++) { + const element = data[i]; + + x.push((N - 1) * element[0]); + y0.push(element[1]); + y1.push(element[1]); + } + + const xLinSpace = linspace(0, 1, N); + + for (i = 0; i < N; i++) { + xLinSpace[i] = (N - 1) * Math.pow(xLinSpace[i], gamma); + } + + const xLinSpaceIndexes = searchSorted(x, xLinSpace); + + for (i = 1; i < N - 1; i++) { + const index = xLinSpaceIndexes[i]; + const colorPercent = + (xLinSpace[i] - x[index - 1]) / (x[index] - x[index - 1]); + const colorDelta = y0[index] - y1[index - 1]; + + lut[i] = colorPercent * colorDelta + y1[index - 1]; + } + + lut[0] = y1[0]; + lut[N - 1] = y0[data.length - 1]; + + return lut; +} + +/** + * Creates a Colormap based on lookup tables using linear segments. + * @param {{red:Array, green:Array, blue:Array}} segmentedData An object with a red, green and blue entries. + * Each entry should be a list of x, y0, y1 tuples, forming rows in a table. + * @param {Number} N The number of elements in the result Colormap + * @param {any} gamma value denotes a "gamma curve" value which adjusts the brightness + * at the bottom and top of the Colormap. + * @returns {Array} The created Colormap object + * @description The lookup table is generated using linear interpolation for each + * Primary color, with the 0-1 domain divided into any number of + * Segments. + * https://github.com/stefanv/matplotlib/blob/3f1a23755e86fef97d51e30e106195f34425c9e3/lib/matplotlib/colors.py#L663 + * @memberof Colors + */ +function createLinearSegmentedColormap(segmentedData, N, gamma) { + let i; + const lut = []; + + N = N === null ? 256 : N; + gamma = gamma === null ? 1 : gamma; + + const redLut = makeMappingArray(N, segmentedData.red, gamma); + const greenLut = makeMappingArray(N, segmentedData.green, gamma); + const blueLut = makeMappingArray(N, segmentedData.blue, gamma); + + for (i = 0; i < N; i++) { + const red = Math.round(redLut[i] * 255); + const green = Math.round(greenLut[i] * 255); + const blue = Math.round(blueLut[i] * 255); + const rgba = [red, green, blue, 255]; + + lut.push(rgba); + } + + return lut; +} + +/** + * Return all available colormaps (id and name) + * @returns {Array<{id,key}>} An array of colormaps with an object containing the "id" and display "name" + * @memberof Colors + */ +export function getColormapsList() { + const colormaps = []; + const keys = Object.keys(colormapsData); + + keys.forEach(function (key) { + if (colormapsData.hasOwnProperty(key)) { + const colormap = colormapsData[key]; + + colormaps.push({ + id: key, + name: colormap.name, + }); + } + }); + + colormaps.sort(function (a, b) { + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + + if (aName === bName) { + return 0; + } + + return aName < bName ? -1 : 1; + }); + + return colormaps; +} + +/** + * Return a colorMap object with the provided id and colormapData + * if the Id matches existent colorMap objects (check colormapsData) the colormapData is ignored. + * if the colormapData is not empty, the colorMap will be added to the colormapsData list. Otherwise, an empty colorMap object is returned. + * @param {string} id The ID of the colormap + * @param {Object} colormapData - An object that can contain a name, numColors, gama, segmentedData and/or colors + * @returns {*} The Colormap Object + * @memberof Colors + */ +export function getColormap( + id: string, + colormapData?: CPUFallbackColormapData +): CPUFallbackColormap { + let colormap = colormapsData[id]; + + if (!colormap) { + colormap = colormapsData[id] = colormapData || { + name: '', + colors: [], + }; + } + + if (!colormap.colors && colormap.segmentedData) { + colormap.colors = createLinearSegmentedColormap( + colormap.segmentedData, + colormap.numColors, + colormap.gamma + ); + } + + const cpuFallbackColormap: CPUFallbackColormap = { + getId() { + return id; + }, + + getColorSchemeName() { + return colormap.name; + }, + + setColorSchemeName(name) { + colormap.name = name; + }, + + getNumberOfColors() { + return colormap.colors.length; + }, + + setNumberOfColors(numColors) { + while (colormap.colors.length < numColors) { + colormap.colors.push(COLOR_TRANSPARENT); + } + + colormap.colors.length = numColors; + }, + + getColor(index) { + if (this.isValidIndex(index)) { + return colormap.colors[index]; + } + + return COLOR_TRANSPARENT; + }, + + getColorRepeating(index) { + const numColors = colormap.colors.length; + + index = numColors ? index % numColors : 0; + + return this.getColor(index); + }, + + setColor(index, rgba) { + if (this.isValidIndex(index)) { + colormap.colors[index] = rgba; + } + }, + + addColor(rgba) { + colormap.colors.push(rgba); + }, + + insertColor(index, rgba) { + if (this.isValidIndex(index)) { + colormap.colors.splice(index, 1, rgba); + } + }, + + removeColor(index) { + if (this.isValidIndex(index)) { + colormap.colors.splice(index, 1); + } + }, + + clearColors() { + colormap.colors = []; + }, + + buildLookupTable(lut) { + if (!lut) { + return; + } + + const numColors = colormap.colors.length; + + lut.setNumberOfTableValues(numColors); + + for (let i = 0; i < numColors; i++) { + lut.setTableValue(i, colormap.colors[i]); + } + }, + + createLookupTable() { + const lut = new LookupTable(); + + this.buildLookupTable(lut); + + return lut; + }, + + isValidIndex(index) { + return index >= 0 && index < colormap.colors.length; + }, + }; + + return cpuFallbackColormap; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/colormaps.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/colormaps.ts new file mode 100644 index 0000000000..cd7d931928 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/colormaps.ts @@ -0,0 +1,1537 @@ +import { CPUFallbackColormapsData } from '../../../../types'; + +// Colormaps +// +// Hot Iron, PET, Hot Metal Blue and PET 20 Step are color palettes +// Defined by the DICOM standard +// http://dicom.nema.org/dicom/2013/output/chtml/part06/chapter_B.html +// +// All Linear Segmented Colormaps were copied from matplotlib +// https://github.com/stefanv/matplotlib/blob/master/lib/matplotlib/_cm.py + +const colormapsData: CPUFallbackColormapsData = { + hotIron: { + name: 'Hot Iron', + numOfColors: 256, + colors: [ + [0, 0, 0, 255], + [2, 0, 0, 255], + [4, 0, 0, 255], + [6, 0, 0, 255], + [8, 0, 0, 255], + [10, 0, 0, 255], + [12, 0, 0, 255], + [14, 0, 0, 255], + [16, 0, 0, 255], + [18, 0, 0, 255], + [20, 0, 0, 255], + [22, 0, 0, 255], + [24, 0, 0, 255], + [26, 0, 0, 255], + [28, 0, 0, 255], + [30, 0, 0, 255], + [32, 0, 0, 255], + [34, 0, 0, 255], + [36, 0, 0, 255], + [38, 0, 0, 255], + [40, 0, 0, 255], + [42, 0, 0, 255], + [44, 0, 0, 255], + [46, 0, 0, 255], + [48, 0, 0, 255], + [50, 0, 0, 255], + [52, 0, 0, 255], + [54, 0, 0, 255], + [56, 0, 0, 255], + [58, 0, 0, 255], + [60, 0, 0, 255], + [62, 0, 0, 255], + [64, 0, 0, 255], + [66, 0, 0, 255], + [68, 0, 0, 255], + [70, 0, 0, 255], + [72, 0, 0, 255], + [74, 0, 0, 255], + [76, 0, 0, 255], + [78, 0, 0, 255], + [80, 0, 0, 255], + [82, 0, 0, 255], + [84, 0, 0, 255], + [86, 0, 0, 255], + [88, 0, 0, 255], + [90, 0, 0, 255], + [92, 0, 0, 255], + [94, 0, 0, 255], + [96, 0, 0, 255], + [98, 0, 0, 255], + [100, 0, 0, 255], + [102, 0, 0, 255], + [104, 0, 0, 255], + [106, 0, 0, 255], + [108, 0, 0, 255], + [110, 0, 0, 255], + [112, 0, 0, 255], + [114, 0, 0, 255], + [116, 0, 0, 255], + [118, 0, 0, 255], + [120, 0, 0, 255], + [122, 0, 0, 255], + [124, 0, 0, 255], + [126, 0, 0, 255], + [128, 0, 0, 255], + [130, 0, 0, 255], + [132, 0, 0, 255], + [134, 0, 0, 255], + [136, 0, 0, 255], + [138, 0, 0, 255], + [140, 0, 0, 255], + [142, 0, 0, 255], + [144, 0, 0, 255], + [146, 0, 0, 255], + [148, 0, 0, 255], + [150, 0, 0, 255], + [152, 0, 0, 255], + [154, 0, 0, 255], + [156, 0, 0, 255], + [158, 0, 0, 255], + [160, 0, 0, 255], + [162, 0, 0, 255], + [164, 0, 0, 255], + [166, 0, 0, 255], + [168, 0, 0, 255], + [170, 0, 0, 255], + [172, 0, 0, 255], + [174, 0, 0, 255], + [176, 0, 0, 255], + [178, 0, 0, 255], + [180, 0, 0, 255], + [182, 0, 0, 255], + [184, 0, 0, 255], + [186, 0, 0, 255], + [188, 0, 0, 255], + [190, 0, 0, 255], + [192, 0, 0, 255], + [194, 0, 0, 255], + [196, 0, 0, 255], + [198, 0, 0, 255], + [200, 0, 0, 255], + [202, 0, 0, 255], + [204, 0, 0, 255], + [206, 0, 0, 255], + [208, 0, 0, 255], + [210, 0, 0, 255], + [212, 0, 0, 255], + [214, 0, 0, 255], + [216, 0, 0, 255], + [218, 0, 0, 255], + [220, 0, 0, 255], + [222, 0, 0, 255], + [224, 0, 0, 255], + [226, 0, 0, 255], + [228, 0, 0, 255], + [230, 0, 0, 255], + [232, 0, 0, 255], + [234, 0, 0, 255], + [236, 0, 0, 255], + [238, 0, 0, 255], + [240, 0, 0, 255], + [242, 0, 0, 255], + [244, 0, 0, 255], + [246, 0, 0, 255], + [248, 0, 0, 255], + [250, 0, 0, 255], + [252, 0, 0, 255], + [254, 0, 0, 255], + [255, 0, 0, 255], + [255, 2, 0, 255], + [255, 4, 0, 255], + [255, 6, 0, 255], + [255, 8, 0, 255], + [255, 10, 0, 255], + [255, 12, 0, 255], + [255, 14, 0, 255], + [255, 16, 0, 255], + [255, 18, 0, 255], + [255, 20, 0, 255], + [255, 22, 0, 255], + [255, 24, 0, 255], + [255, 26, 0, 255], + [255, 28, 0, 255], + [255, 30, 0, 255], + [255, 32, 0, 255], + [255, 34, 0, 255], + [255, 36, 0, 255], + [255, 38, 0, 255], + [255, 40, 0, 255], + [255, 42, 0, 255], + [255, 44, 0, 255], + [255, 46, 0, 255], + [255, 48, 0, 255], + [255, 50, 0, 255], + [255, 52, 0, 255], + [255, 54, 0, 255], + [255, 56, 0, 255], + [255, 58, 0, 255], + [255, 60, 0, 255], + [255, 62, 0, 255], + [255, 64, 0, 255], + [255, 66, 0, 255], + [255, 68, 0, 255], + [255, 70, 0, 255], + [255, 72, 0, 255], + [255, 74, 0, 255], + [255, 76, 0, 255], + [255, 78, 0, 255], + [255, 80, 0, 255], + [255, 82, 0, 255], + [255, 84, 0, 255], + [255, 86, 0, 255], + [255, 88, 0, 255], + [255, 90, 0, 255], + [255, 92, 0, 255], + [255, 94, 0, 255], + [255, 96, 0, 255], + [255, 98, 0, 255], + [255, 100, 0, 255], + [255, 102, 0, 255], + [255, 104, 0, 255], + [255, 106, 0, 255], + [255, 108, 0, 255], + [255, 110, 0, 255], + [255, 112, 0, 255], + [255, 114, 0, 255], + [255, 116, 0, 255], + [255, 118, 0, 255], + [255, 120, 0, 255], + [255, 122, 0, 255], + [255, 124, 0, 255], + [255, 126, 0, 255], + [255, 128, 4, 255], + [255, 130, 8, 255], + [255, 132, 12, 255], + [255, 134, 16, 255], + [255, 136, 20, 255], + [255, 138, 24, 255], + [255, 140, 28, 255], + [255, 142, 32, 255], + [255, 144, 36, 255], + [255, 146, 40, 255], + [255, 148, 44, 255], + [255, 150, 48, 255], + [255, 152, 52, 255], + [255, 154, 56, 255], + [255, 156, 60, 255], + [255, 158, 64, 255], + [255, 160, 68, 255], + [255, 162, 72, 255], + [255, 164, 76, 255], + [255, 166, 80, 255], + [255, 168, 84, 255], + [255, 170, 88, 255], + [255, 172, 92, 255], + [255, 174, 96, 255], + [255, 176, 100, 255], + [255, 178, 104, 255], + [255, 180, 108, 255], + [255, 182, 112, 255], + [255, 184, 116, 255], + [255, 186, 120, 255], + [255, 188, 124, 255], + [255, 190, 128, 255], + [255, 192, 132, 255], + [255, 194, 136, 255], + [255, 196, 140, 255], + [255, 198, 144, 255], + [255, 200, 148, 255], + [255, 202, 152, 255], + [255, 204, 156, 255], + [255, 206, 160, 255], + [255, 208, 164, 255], + [255, 210, 168, 255], + [255, 212, 172, 255], + [255, 214, 176, 255], + [255, 216, 180, 255], + [255, 218, 184, 255], + [255, 220, 188, 255], + [255, 222, 192, 255], + [255, 224, 196, 255], + [255, 226, 200, 255], + [255, 228, 204, 255], + [255, 230, 208, 255], + [255, 232, 212, 255], + [255, 234, 216, 255], + [255, 236, 220, 255], + [255, 238, 224, 255], + [255, 240, 228, 255], + [255, 242, 232, 255], + [255, 244, 236, 255], + [255, 246, 240, 255], + [255, 248, 244, 255], + [255, 250, 248, 255], + [255, 252, 252, 255], + [255, 255, 255, 255], + ], + }, + pet: { + name: 'PET', + numColors: 256, + colors: [ + [0, 0, 0, 255], + [0, 2, 1, 255], + [0, 4, 3, 255], + [0, 6, 5, 255], + [0, 8, 7, 255], + [0, 10, 9, 255], + [0, 12, 11, 255], + [0, 14, 13, 255], + [0, 16, 15, 255], + [0, 18, 17, 255], + [0, 20, 19, 255], + [0, 22, 21, 255], + [0, 24, 23, 255], + [0, 26, 25, 255], + [0, 28, 27, 255], + [0, 30, 29, 255], + [0, 32, 31, 255], + [0, 34, 33, 255], + [0, 36, 35, 255], + [0, 38, 37, 255], + [0, 40, 39, 255], + [0, 42, 41, 255], + [0, 44, 43, 255], + [0, 46, 45, 255], + [0, 48, 47, 255], + [0, 50, 49, 255], + [0, 52, 51, 255], + [0, 54, 53, 255], + [0, 56, 55, 255], + [0, 58, 57, 255], + [0, 60, 59, 255], + [0, 62, 61, 255], + [0, 65, 63, 255], + [0, 67, 65, 255], + [0, 69, 67, 255], + [0, 71, 69, 255], + [0, 73, 71, 255], + [0, 75, 73, 255], + [0, 77, 75, 255], + [0, 79, 77, 255], + [0, 81, 79, 255], + [0, 83, 81, 255], + [0, 85, 83, 255], + [0, 87, 85, 255], + [0, 89, 87, 255], + [0, 91, 89, 255], + [0, 93, 91, 255], + [0, 95, 93, 255], + [0, 97, 95, 255], + [0, 99, 97, 255], + [0, 101, 99, 255], + [0, 103, 101, 255], + [0, 105, 103, 255], + [0, 107, 105, 255], + [0, 109, 107, 255], + [0, 111, 109, 255], + [0, 113, 111, 255], + [0, 115, 113, 255], + [0, 117, 115, 255], + [0, 119, 117, 255], + [0, 121, 119, 255], + [0, 123, 121, 255], + [0, 125, 123, 255], + [0, 128, 125, 255], + [1, 126, 127, 255], + [3, 124, 129, 255], + [5, 122, 131, 255], + [7, 120, 133, 255], + [9, 118, 135, 255], + [11, 116, 137, 255], + [13, 114, 139, 255], + [15, 112, 141, 255], + [17, 110, 143, 255], + [19, 108, 145, 255], + [21, 106, 147, 255], + [23, 104, 149, 255], + [25, 102, 151, 255], + [27, 100, 153, 255], + [29, 98, 155, 255], + [31, 96, 157, 255], + [33, 94, 159, 255], + [35, 92, 161, 255], + [37, 90, 163, 255], + [39, 88, 165, 255], + [41, 86, 167, 255], + [43, 84, 169, 255], + [45, 82, 171, 255], + [47, 80, 173, 255], + [49, 78, 175, 255], + [51, 76, 177, 255], + [53, 74, 179, 255], + [55, 72, 181, 255], + [57, 70, 183, 255], + [59, 68, 185, 255], + [61, 66, 187, 255], + [63, 64, 189, 255], + [65, 63, 191, 255], + [67, 61, 193, 255], + [69, 59, 195, 255], + [71, 57, 197, 255], + [73, 55, 199, 255], + [75, 53, 201, 255], + [77, 51, 203, 255], + [79, 49, 205, 255], + [81, 47, 207, 255], + [83, 45, 209, 255], + [85, 43, 211, 255], + [86, 41, 213, 255], + [88, 39, 215, 255], + [90, 37, 217, 255], + [92, 35, 219, 255], + [94, 33, 221, 255], + [96, 31, 223, 255], + [98, 29, 225, 255], + [100, 27, 227, 255], + [102, 25, 229, 255], + [104, 23, 231, 255], + [106, 21, 233, 255], + [108, 19, 235, 255], + [110, 17, 237, 255], + [112, 15, 239, 255], + [114, 13, 241, 255], + [116, 11, 243, 255], + [118, 9, 245, 255], + [120, 7, 247, 255], + [122, 5, 249, 255], + [124, 3, 251, 255], + [126, 1, 253, 255], + [128, 0, 255, 255], + [130, 2, 252, 255], + [132, 4, 248, 255], + [134, 6, 244, 255], + [136, 8, 240, 255], + [138, 10, 236, 255], + [140, 12, 232, 255], + [142, 14, 228, 255], + [144, 16, 224, 255], + [146, 18, 220, 255], + [148, 20, 216, 255], + [150, 22, 212, 255], + [152, 24, 208, 255], + [154, 26, 204, 255], + [156, 28, 200, 255], + [158, 30, 196, 255], + [160, 32, 192, 255], + [162, 34, 188, 255], + [164, 36, 184, 255], + [166, 38, 180, 255], + [168, 40, 176, 255], + [170, 42, 172, 255], + [171, 44, 168, 255], + [173, 46, 164, 255], + [175, 48, 160, 255], + [177, 50, 156, 255], + [179, 52, 152, 255], + [181, 54, 148, 255], + [183, 56, 144, 255], + [185, 58, 140, 255], + [187, 60, 136, 255], + [189, 62, 132, 255], + [191, 64, 128, 255], + [193, 66, 124, 255], + [195, 68, 120, 255], + [197, 70, 116, 255], + [199, 72, 112, 255], + [201, 74, 108, 255], + [203, 76, 104, 255], + [205, 78, 100, 255], + [207, 80, 96, 255], + [209, 82, 92, 255], + [211, 84, 88, 255], + [213, 86, 84, 255], + [215, 88, 80, 255], + [217, 90, 76, 255], + [219, 92, 72, 255], + [221, 94, 68, 255], + [223, 96, 64, 255], + [225, 98, 60, 255], + [227, 100, 56, 255], + [229, 102, 52, 255], + [231, 104, 48, 255], + [233, 106, 44, 255], + [235, 108, 40, 255], + [237, 110, 36, 255], + [239, 112, 32, 255], + [241, 114, 28, 255], + [243, 116, 24, 255], + [245, 118, 20, 255], + [247, 120, 16, 255], + [249, 122, 12, 255], + [251, 124, 8, 255], + [253, 126, 4, 255], + [255, 128, 0, 255], + [255, 130, 4, 255], + [255, 132, 8, 255], + [255, 134, 12, 255], + [255, 136, 16, 255], + [255, 138, 20, 255], + [255, 140, 24, 255], + [255, 142, 28, 255], + [255, 144, 32, 255], + [255, 146, 36, 255], + [255, 148, 40, 255], + [255, 150, 44, 255], + [255, 152, 48, 255], + [255, 154, 52, 255], + [255, 156, 56, 255], + [255, 158, 60, 255], + [255, 160, 64, 255], + [255, 162, 68, 255], + [255, 164, 72, 255], + [255, 166, 76, 255], + [255, 168, 80, 255], + [255, 170, 85, 255], + [255, 172, 89, 255], + [255, 174, 93, 255], + [255, 176, 97, 255], + [255, 178, 101, 255], + [255, 180, 105, 255], + [255, 182, 109, 255], + [255, 184, 113, 255], + [255, 186, 117, 255], + [255, 188, 121, 255], + [255, 190, 125, 255], + [255, 192, 129, 255], + [255, 194, 133, 255], + [255, 196, 137, 255], + [255, 198, 141, 255], + [255, 200, 145, 255], + [255, 202, 149, 255], + [255, 204, 153, 255], + [255, 206, 157, 255], + [255, 208, 161, 255], + [255, 210, 165, 255], + [255, 212, 170, 255], + [255, 214, 174, 255], + [255, 216, 178, 255], + [255, 218, 182, 255], + [255, 220, 186, 255], + [255, 222, 190, 255], + [255, 224, 194, 255], + [255, 226, 198, 255], + [255, 228, 202, 255], + [255, 230, 206, 255], + [255, 232, 210, 255], + [255, 234, 214, 255], + [255, 236, 218, 255], + [255, 238, 222, 255], + [255, 240, 226, 255], + [255, 242, 230, 255], + [255, 244, 234, 255], + [255, 246, 238, 255], + [255, 248, 242, 255], + [255, 250, 246, 255], + [255, 252, 250, 255], + [255, 255, 255, 255], + ], + }, + hotMetalBlue: { + name: 'Hot Metal Blue', + numColors: 256, + colors: [ + [0, 0, 0, 255], + [0, 0, 2, 255], + [0, 0, 4, 255], + [0, 0, 6, 255], + [0, 0, 8, 255], + [0, 0, 10, 255], + [0, 0, 12, 255], + [0, 0, 14, 255], + [0, 0, 16, 255], + [0, 0, 17, 255], + [0, 0, 19, 255], + [0, 0, 21, 255], + [0, 0, 23, 255], + [0, 0, 25, 255], + [0, 0, 27, 255], + [0, 0, 29, 255], + [0, 0, 31, 255], + [0, 0, 33, 255], + [0, 0, 35, 255], + [0, 0, 37, 255], + [0, 0, 39, 255], + [0, 0, 41, 255], + [0, 0, 43, 255], + [0, 0, 45, 255], + [0, 0, 47, 255], + [0, 0, 49, 255], + [0, 0, 51, 255], + [0, 0, 53, 255], + [0, 0, 55, 255], + [0, 0, 57, 255], + [0, 0, 59, 255], + [0, 0, 61, 255], + [0, 0, 63, 255], + [0, 0, 65, 255], + [0, 0, 67, 255], + [0, 0, 69, 255], + [0, 0, 71, 255], + [0, 0, 73, 255], + [0, 0, 75, 255], + [0, 0, 77, 255], + [0, 0, 79, 255], + [0, 0, 81, 255], + [0, 0, 83, 255], + [0, 0, 84, 255], + [0, 0, 86, 255], + [0, 0, 88, 255], + [0, 0, 90, 255], + [0, 0, 92, 255], + [0, 0, 94, 255], + [0, 0, 96, 255], + [0, 0, 98, 255], + [0, 0, 100, 255], + [0, 0, 102, 255], + [0, 0, 104, 255], + [0, 0, 106, 255], + [0, 0, 108, 255], + [0, 0, 110, 255], + [0, 0, 112, 255], + [0, 0, 114, 255], + [0, 0, 116, 255], + [0, 0, 117, 255], + [0, 0, 119, 255], + [0, 0, 121, 255], + [0, 0, 123, 255], + [0, 0, 125, 255], + [0, 0, 127, 255], + [0, 0, 129, 255], + [0, 0, 131, 255], + [0, 0, 133, 255], + [0, 0, 135, 255], + [0, 0, 137, 255], + [0, 0, 139, 255], + [0, 0, 141, 255], + [0, 0, 143, 255], + [0, 0, 145, 255], + [0, 0, 147, 255], + [0, 0, 149, 255], + [0, 0, 151, 255], + [0, 0, 153, 255], + [0, 0, 155, 255], + [0, 0, 157, 255], + [0, 0, 159, 255], + [0, 0, 161, 255], + [0, 0, 163, 255], + [0, 0, 165, 255], + [0, 0, 167, 255], + [3, 0, 169, 255], + [6, 0, 171, 255], + [9, 0, 173, 255], + [12, 0, 175, 255], + [15, 0, 177, 255], + [18, 0, 179, 255], + [21, 0, 181, 255], + [24, 0, 183, 255], + [26, 0, 184, 255], + [29, 0, 186, 255], + [32, 0, 188, 255], + [35, 0, 190, 255], + [38, 0, 192, 255], + [41, 0, 194, 255], + [44, 0, 196, 255], + [47, 0, 198, 255], + [50, 0, 200, 255], + [52, 0, 197, 255], + [55, 0, 194, 255], + [57, 0, 191, 255], + [59, 0, 188, 255], + [62, 0, 185, 255], + [64, 0, 182, 255], + [66, 0, 179, 255], + [69, 0, 176, 255], + [71, 0, 174, 255], + [74, 0, 171, 255], + [76, 0, 168, 255], + [78, 0, 165, 255], + [81, 0, 162, 255], + [83, 0, 159, 255], + [85, 0, 156, 255], + [88, 0, 153, 255], + [90, 0, 150, 255], + [93, 2, 144, 255], + [96, 4, 138, 255], + [99, 6, 132, 255], + [102, 8, 126, 255], + [105, 9, 121, 255], + [108, 11, 115, 255], + [111, 13, 109, 255], + [114, 15, 103, 255], + [116, 17, 97, 255], + [119, 19, 91, 255], + [122, 21, 85, 255], + [125, 23, 79, 255], + [128, 24, 74, 255], + [131, 26, 68, 255], + [134, 28, 62, 255], + [137, 30, 56, 255], + [140, 32, 50, 255], + [143, 34, 47, 255], + [146, 36, 44, 255], + [149, 38, 41, 255], + [152, 40, 38, 255], + [155, 41, 35, 255], + [158, 43, 32, 255], + [161, 45, 29, 255], + [164, 47, 26, 255], + [166, 49, 24, 255], + [169, 51, 21, 255], + [172, 53, 18, 255], + [175, 55, 15, 255], + [178, 56, 12, 255], + [181, 58, 9, 255], + [184, 60, 6, 255], + [187, 62, 3, 255], + [190, 64, 0, 255], + [194, 66, 0, 255], + [198, 68, 0, 255], + [201, 70, 0, 255], + [205, 72, 0, 255], + [209, 73, 0, 255], + [213, 75, 0, 255], + [217, 77, 0, 255], + [221, 79, 0, 255], + [224, 81, 0, 255], + [228, 83, 0, 255], + [232, 85, 0, 255], + [236, 87, 0, 255], + [240, 88, 0, 255], + [244, 90, 0, 255], + [247, 92, 0, 255], + [251, 94, 0, 255], + [255, 96, 0, 255], + [255, 98, 3, 255], + [255, 100, 6, 255], + [255, 102, 9, 255], + [255, 104, 12, 255], + [255, 105, 15, 255], + [255, 107, 18, 255], + [255, 109, 21, 255], + [255, 111, 24, 255], + [255, 113, 26, 255], + [255, 115, 29, 255], + [255, 117, 32, 255], + [255, 119, 35, 255], + [255, 120, 38, 255], + [255, 122, 41, 255], + [255, 124, 44, 255], + [255, 126, 47, 255], + [255, 128, 50, 255], + [255, 130, 53, 255], + [255, 132, 56, 255], + [255, 134, 59, 255], + [255, 136, 62, 255], + [255, 137, 65, 255], + [255, 139, 68, 255], + [255, 141, 71, 255], + [255, 143, 74, 255], + [255, 145, 76, 255], + [255, 147, 79, 255], + [255, 149, 82, 255], + [255, 151, 85, 255], + [255, 152, 88, 255], + [255, 154, 91, 255], + [255, 156, 94, 255], + [255, 158, 97, 255], + [255, 160, 100, 255], + [255, 162, 103, 255], + [255, 164, 106, 255], + [255, 166, 109, 255], + [255, 168, 112, 255], + [255, 169, 115, 255], + [255, 171, 118, 255], + [255, 173, 121, 255], + [255, 175, 124, 255], + [255, 177, 126, 255], + [255, 179, 129, 255], + [255, 181, 132, 255], + [255, 183, 135, 255], + [255, 184, 138, 255], + [255, 186, 141, 255], + [255, 188, 144, 255], + [255, 190, 147, 255], + [255, 192, 150, 255], + [255, 194, 153, 255], + [255, 196, 156, 255], + [255, 198, 159, 255], + [255, 200, 162, 255], + [255, 201, 165, 255], + [255, 203, 168, 255], + [255, 205, 171, 255], + [255, 207, 174, 255], + [255, 209, 176, 255], + [255, 211, 179, 255], + [255, 213, 182, 255], + [255, 215, 185, 255], + [255, 216, 188, 255], + [255, 218, 191, 255], + [255, 220, 194, 255], + [255, 222, 197, 255], + [255, 224, 200, 255], + [255, 226, 203, 255], + [255, 228, 206, 255], + [255, 229, 210, 255], + [255, 231, 213, 255], + [255, 233, 216, 255], + [255, 235, 219, 255], + [255, 237, 223, 255], + [255, 239, 226, 255], + [255, 240, 229, 255], + [255, 242, 232, 255], + [255, 244, 236, 255], + [255, 246, 239, 255], + [255, 248, 242, 255], + [255, 250, 245, 255], + [255, 251, 249, 255], + [255, 253, 252, 255], + [255, 255, 255, 255], + ], + }, + pet20Step: { + name: 'PET 20 Step', + numColors: 256, + colors: [ + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [0, 0, 0, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [96, 0, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 80, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [48, 48, 112, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [80, 80, 128, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [96, 96, 176, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [112, 112, 192, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [128, 128, 224, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 96, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [48, 144, 48, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [80, 192, 80, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [64, 224, 64, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [224, 224, 80, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 208, 96, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 176, 64, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [208, 144, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [192, 96, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [176, 48, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 0, 0, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + [255, 255, 255, 255], + ], + }, + gray: { + name: 'Gray', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0, 0], + [1, 1, 1], + ], + green: [ + [0, 0, 0], + [1, 1, 1], + ], + blue: [ + [0, 0, 0], + [1, 1, 1], + ], + }, + }, + jet: { + name: 'Jet', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0, 0], + [0.35, 0, 0], + [0.66, 1, 1], + [0.89, 1, 1], + [1, 0.5, 0.5], + ], + green: [ + [0, 0, 0], + [0.125, 0, 0], + [0.375, 1, 1], + [0.64, 1, 1], + [0.91, 0, 0], + [1, 0, 0], + ], + blue: [ + [0, 0.5, 0.5], + [0.11, 1, 1], + [0.34, 1, 1], + [0.65, 0, 0], + [1, 0, 0], + ], + }, + }, + hsv: { + name: 'HSV', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 1, 1], + [0.15873, 1, 1], + [0.174603, 0.96875, 0.96875], + [0.333333, 0.03125, 0.03125], + [0.349206, 0, 0], + [0.666667, 0, 0], + [0.68254, 0.03125, 0.03125], + [0.84127, 0.96875, 0.96875], + [0.857143, 1, 1], + [1, 1, 1], + ], + green: [ + [0, 0, 0], + [0.15873, 0.9375, 0.9375], + [0.174603, 1, 1], + [0.507937, 1, 1], + [0.666667, 0.0625, 0.0625], + [0.68254, 0, 0], + [1, 0, 0], + ], + blue: [ + [0, 0, 0], + [0.333333, 0, 0], + [0.349206, 0.0625, 0.0625], + [0.507937, 1, 1], + [0.84127, 1, 1], + [0.857143, 0.9375, 0.9375], + [1, 0.09375, 0.09375], + ], + }, + }, + hot: { + name: 'Hot', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0.0416, 0.0416], + [0.365079, 1, 1], + [1, 1, 1], + ], + green: [ + [0, 0, 0], + [0.365079, 0, 0], + [0.746032, 1, 1], + [1, 1, 1], + ], + blue: [ + [0, 0, 0], + [0.746032, 0, 0], + [1, 1, 1], + ], + }, + }, + cool: { + name: 'Cool', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0, 0], + [1, 1, 1], + ], + green: [ + [0, 1, 1], + [1, 0, 0], + ], + blue: [ + [0, 1, 1], + [1, 1, 1], + ], + }, + }, + spring: { + name: 'Spring', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 1, 1], + [1, 1, 1], + ], + green: [ + [0, 0, 0], + [1, 1, 1], + ], + blue: [ + [0, 1, 1], + [1, 0, 0], + ], + }, + }, + summer: { + name: 'Summer', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0, 0], + [1, 1, 1], + ], + green: [ + [0, 0.5, 0.5], + [1, 1, 1], + ], + blue: [ + [0, 0.4, 0.4], + [1, 0.4, 0.4], + ], + }, + }, + autumn: { + name: 'Autumn', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 1, 1], + [1, 1, 1], + ], + green: [ + [0, 0, 0], + [1, 1, 1], + ], + blue: [ + [0, 0, 0], + [1, 0, 0], + ], + }, + }, + winter: { + name: 'Winter', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0, 0], + [1, 0, 0], + ], + green: [ + [0, 0, 0], + [1, 1, 1], + ], + blue: [ + [0, 1, 1], + [1, 0.5, 0.5], + ], + }, + }, + bone: { + name: 'Bone', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0, 0], + [0.746032, 0.652778, 0.652778], + [1, 1, 1], + ], + green: [ + [0, 0, 0], + [0.365079, 0.319444, 0.319444], + [0.746032, 0.777778, 0.777778], + [1, 1, 1], + ], + blue: [ + [0, 0, 0], + [0.365079, 0.444444, 0.444444], + [1, 1, 1], + ], + }, + }, + copper: { + name: 'Copper', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0, 0], + [0.809524, 1, 1], + [1, 1, 1], + ], + green: [ + [0, 0, 0], + [1, 0.7812, 0.7812], + ], + blue: [ + [0, 0, 0], + [1, 0.4975, 0.4975], + ], + }, + }, + spectral: { + name: 'Spectral', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0, 0], + [0.05, 0.4667, 0.4667], + [0.1, 0.5333, 0.5333], + [0.15, 0, 0], + [0.2, 0, 0], + [0.25, 0, 0], + [0.3, 0, 0], + [0.35, 0, 0], + [0.4, 0, 0], + [0.45, 0, 0], + [0.5, 0, 0], + [0.55, 0, 0], + [0.6, 0, 0], + [0.65, 0.7333, 0.7333], + [0.7, 0.9333, 0.9333], + [0.75, 1, 1], + [0.8, 1, 1], + [0.85, 1, 1], + [0.9, 0.8667, 0.8667], + [0.95, 0.8, 0.8], + [1, 0.8, 0.8], + ], + green: [ + [0, 0, 0], + [0.05, 0, 0], + [0.1, 0, 0], + [0.15, 0, 0], + [0.2, 0, 0], + [0.25, 0.4667, 0.4667], + [0.3, 0.6, 0.6], + [0.35, 0.6667, 0.6667], + [0.4, 0.6667, 0.6667], + [0.45, 0.6, 0.6], + [0.5, 0.7333, 0.7333], + [0.55, 0.8667, 0.8667], + [0.6, 1, 1], + [0.65, 1, 1], + [0.7, 0.9333, 0.9333], + [0.75, 0.8, 0.8], + [0.8, 0.6, 0.6], + [0.85, 0, 0], + [0.9, 0, 0], + [0.95, 0, 0], + [1, 0.8, 0.8], + ], + blue: [ + [0, 0, 0], + [0.05, 0.5333, 0.5333], + [0.1, 0.6, 0.6], + [0.15, 0.6667, 0.6667], + [0.2, 0.8667, 0.8667], + [0.25, 0.8667, 0.8667], + [0.3, 0.8667, 0.8667], + [0.35, 0.6667, 0.6667], + [0.4, 0.5333, 0.5333], + [0.45, 0, 0], + [0.5, 0, 0], + [0.55, 0, 0], + [0.6, 0, 0], + [0.65, 0, 0], + [0.7, 0, 0], + [0.75, 0, 0], + [0.8, 0, 0], + [0.85, 0, 0], + [0.9, 0, 0], + [0.95, 0, 0], + [1, 0.8, 0.8], + ], + }, + }, + coolwarm: { + name: 'CoolWarm', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0.2298057, 0.2298057], + [0.03125, 0.26623388, 0.26623388], + [0.0625, 0.30386891, 0.30386891], + [0.09375, 0.342804478, 0.342804478], + [0.125, 0.38301334, 0.38301334], + [0.15625, 0.424369608, 0.424369608], + [0.1875, 0.46666708, 0.46666708], + [0.21875, 0.509635204, 0.509635204], + [0.25, 0.552953156, 0.552953156], + [0.28125, 0.596262162, 0.596262162], + [0.3125, 0.639176211, 0.639176211], + [0.34375, 0.681291281, 0.681291281], + [0.375, 0.722193294, 0.722193294], + [0.40625, 0.761464949, 0.761464949], + [0.4375, 0.798691636, 0.798691636], + [0.46875, 0.833466556, 0.833466556], + [0.5, 0.865395197, 0.865395197], + [0.53125, 0.897787179, 0.897787179], + [0.5625, 0.924127593, 0.924127593], + [0.59375, 0.944468518, 0.944468518], + [0.625, 0.958852946, 0.958852946], + [0.65625, 0.96732803, 0.96732803], + [0.6875, 0.969954137, 0.969954137], + [0.71875, 0.966811177, 0.966811177], + [0.75, 0.958003065, 0.958003065], + [0.78125, 0.943660866, 0.943660866], + [0.8125, 0.923944917, 0.923944917], + [0.84375, 0.89904617, 0.89904617], + [0.875, 0.869186849, 0.869186849], + [0.90625, 0.834620542, 0.834620542], + [0.9375, 0.795631745, 0.795631745], + [0.96875, 0.752534934, 0.752534934], + [1, 0.705673158, 0.705673158], + ], + green: [ + [0, 0.298717966, 0.298717966], + [0.03125, 0.353094838, 0.353094838], + [0.0625, 0.406535296, 0.406535296], + [0.09375, 0.458757618, 0.458757618], + [0.125, 0.50941904, 0.50941904], + [0.15625, 0.558148092, 0.558148092], + [0.1875, 0.604562568, 0.604562568], + [0.21875, 0.648280772, 0.648280772], + [0.25, 0.688929332, 0.688929332], + [0.28125, 0.726149107, 0.726149107], + [0.3125, 0.759599947, 0.759599947], + [0.34375, 0.788964712, 0.788964712], + [0.375, 0.813952739, 0.813952739], + [0.40625, 0.834302879, 0.834302879], + [0.4375, 0.849786142, 0.849786142], + [0.46875, 0.860207984, 0.860207984], + [0.5, 0.86541021, 0.86541021], + [0.53125, 0.848937047, 0.848937047], + [0.5625, 0.827384882, 0.827384882], + [0.59375, 0.800927443, 0.800927443], + [0.625, 0.769767752, 0.769767752], + [0.65625, 0.734132809, 0.734132809], + [0.6875, 0.694266682, 0.694266682], + [0.71875, 0.650421156, 0.650421156], + [0.75, 0.602842431, 0.602842431], + [0.78125, 0.551750968, 0.551750968], + [0.8125, 0.49730856, 0.49730856], + [0.84375, 0.439559467, 0.439559467], + [0.875, 0.378313092, 0.378313092], + [0.90625, 0.312874446, 0.312874446], + [0.9375, 0.24128379, 0.24128379], + [0.96875, 0.157246067, 0.157246067], + [1, 0.01555616, 0.01555616], + ], + blue: [ + [0, 0.753683153, 0.753683153], + [0.03125, 0.801466763, 0.801466763], + [0.0625, 0.84495867, 0.84495867], + [0.09375, 0.883725899, 0.883725899], + [0.125, 0.917387822, 0.917387822], + [0.15625, 0.945619588, 0.945619588], + [0.1875, 0.968154911, 0.968154911], + [0.21875, 0.98478814, 0.98478814], + [0.25, 0.995375608, 0.995375608], + [0.28125, 0.999836203, 0.999836203], + [0.3125, 0.998151185, 0.998151185], + [0.34375, 0.990363227, 0.990363227], + [0.375, 0.976574709, 0.976574709], + [0.40625, 0.956945269, 0.956945269], + [0.4375, 0.931688648, 0.931688648], + [0.46875, 0.901068838, 0.901068838], + [0.5, 0.865395561, 0.865395561], + [0.53125, 0.820880546, 0.820880546], + [0.5625, 0.774508472, 0.774508472], + [0.59375, 0.726736146, 0.726736146], + [0.625, 0.678007945, 0.678007945], + [0.65625, 0.628751763, 0.628751763], + [0.6875, 0.579375448, 0.579375448], + [0.71875, 0.530263762, 0.530263762], + [0.75, 0.481775914, 0.481775914], + [0.78125, 0.434243684, 0.434243684], + [0.8125, 0.387970225, 0.387970225], + [0.84375, 0.343229596, 0.343229596], + [0.875, 0.300267182, 0.300267182], + [0.90625, 0.259301199, 0.259301199], + [0.9375, 0.220525627, 0.220525627], + [0.96875, 0.184115123, 0.184115123], + [1, 0.150232812, 0.150232812], + ], + }, + }, + blues: { + name: 'Blues', + numColors: 256, + gamma: 1, + segmentedData: { + red: [ + [0, 0.9686274528503418, 0.9686274528503418], + [0.125, 0.87058824300765991, 0.87058824300765991], + [0.25, 0.7764706015586853, 0.7764706015586853], + [0.375, 0.61960786581039429, 0.61960786581039429], + [0.5, 0.41960784792900085, 0.41960784792900085], + [0.625, 0.25882354378700256, 0.25882354378700256], + [0.75, 0.12941177189350128, 0.12941177189350128], + [0.875, 0.031372550874948502, 0.031372550874948502], + [1, 0.031372550874948502, 0.031372550874948502], + ], + green: [ + [0, 0.9843137264251709, 0.9843137264251709], + [0.125, 0.92156863212585449, 0.92156863212585449], + [0.25, 0.85882353782653809, 0.85882353782653809], + [0.375, 0.7921568751335144, 0.7921568751335144], + [0.5, 0.68235296010971069, 0.68235296010971069], + [0.625, 0.57254904508590698, 0.57254904508590698], + [0.75, 0.44313725829124451, 0.44313725829124451], + [0.875, 0.31764706969261169, 0.31764706969261169], + [1, 0.18823529779911041, 0.18823529779911041], + ], + blue: [ + [0, 1, 1], + [0.125, 0.9686274528503418, 0.9686274528503418], + [0.25, 0.93725490570068359, 0.93725490570068359], + [0.375, 0.88235294818878174, 0.88235294818878174], + [0.5, 0.83921569585800171, 0.83921569585800171], + [0.625, 0.7764706015586853, 0.7764706015586853], + [0.75, 0.70980393886566162, 0.70980393886566162], + [0.875, 0.61176472902297974, 0.61176472902297974], + [1, 0.41960784792900085, 0.41960784792900085], + ], + }, + }, +}; + +export default colormapsData; diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/index.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/index.ts new file mode 100644 index 0000000000..0a0dc6b4e5 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/index.ts @@ -0,0 +1,12 @@ +import { getColormap, getColormapsList } from './colormap'; +import LookupTable from './lookupTable'; +import colormaps from './colormaps'; + +export default { + getColormap, + getColormapsList, + LookupTable, + colormaps, +}; + +export { getColormap, getColormapsList, LookupTable, colormaps }; diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/lookupTable.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/lookupTable.ts new file mode 100644 index 0000000000..51a1387b96 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/colors/lookupTable.ts @@ -0,0 +1,469 @@ +import { Point2, Point4, CPUFallbackLookupTable } from '../../../../types'; + +// This code was created based on vtkLookupTable +// http://www.vtk.org/doc/release/5.0/html/a01697.html +// https://github.com/Kitware/VTK/blob/master/Common/Core/vtkLookupTable.cxx +const BELOW_RANGE_COLOR_INDEX = 0; +const ABOVE_RANGE_COLOR_INDEX = 1; +const NAN_COLOR_INDEX = 2; + +/** + * Converts an HSV (Hue, Saturation, Value) color to RGB (Red, Green, Blue) color value + * @param {Number} hue A number representing the hue color value + * @param {any} sat A number representing the saturation color value + * @param {any} val A number representing the value color value + * @returns {Numberp[]} An RGB color array + */ +function HSVToRGB(hue, sat, val) { + if (hue > 1) { + throw new Error('HSVToRGB expects hue < 1'); + } + + const rgb = []; + + if (sat === 0) { + rgb[0] = val; + rgb[1] = val; + rgb[2] = val; + + return rgb; + } + + const hueCase = Math.floor(hue * 6); + const frac = 6 * hue - hueCase; + const lx = val * (1 - sat); + const ly = val * (1 - sat * frac); + const lz = val * (1 - sat * (1 - frac)); + + switch (hueCase) { + /* 0 p.Range[1]) { + dIndex = p.MaxIndex + ABOVE_RANGE_COLOR_INDEX + 1.5; + } else { + dIndex = (v + p.Shift) * p.Scale; + } + + return Math.floor(dIndex); +} + +/** + * Maps scalar values into colors via a lookup table + * LookupTable is an object that is used by mapper objects to map scalar values into rgba (red-green-blue-alpha transparency) color specification, + * or rgba into scalar values. The color table can be created by direct insertion of color values, or by specifying hue, saturation, value, and alpha range and generating a table + */ +class LookupTable implements CPUFallbackLookupTable { + NumberOfColors: number; + Ramp: string; + TableRange: Point2; + HueRange: Point2; + SaturationRange: Point2; + ValueRange: Point2; + AlphaRange: Point2; + NaNColor: Point4; + BelowRangeColor: Point4; + UseBelowRangeColor: boolean; + AboveRangeColor: Point4; + UseAboveRangeColor: boolean; + InputRange: Point2; + Table: Point4[]; + + /** + * Creates a default linear LookupTable object with 256 colors. + */ + constructor() { + this.NumberOfColors = 256; + this.Ramp = 'linear'; + this.TableRange = [0, 255]; + this.HueRange = [0, 0.66667]; + this.SaturationRange = [1, 1]; + this.ValueRange = [1, 1]; + this.AlphaRange = [1, 1]; + this.NaNColor = [128, 0, 0, 255]; + this.BelowRangeColor = [0, 0, 0, 255]; + this.UseBelowRangeColor = true; + this.AboveRangeColor = [255, 255, 255, 255]; + this.UseAboveRangeColor = true; + this.InputRange = [0, 255]; + this.Table = []; + } + + /** + * Specify the number of values (i.e., colors) in the lookup table. + * @param {Number} number The number of colors in he LookupTable + * @returns {void} + * @memberof Colors + */ + public setNumberOfTableValues(number) { + this.NumberOfColors = number; + } + + /** + * Set the shape of the table ramp to either 'linear', 'scurve' or 'sqrt' + * @param {String} ramp A string value representing the shape of the table. Allowed values are 'linear', 'scurve' or 'sqrt' + * @returns {void} + * @memberof Colors + */ + public setRamp(ramp) { + this.Ramp = ramp; + } + + /** + * Sets the minimum/maximum scalar values for scalar mapping. + * Scalar values less than minimum range value are clamped to minimum range value. + * Scalar values greater than maximum range value are clamped to maximum range value. + * @param {Number} start A double representing the minimum scaler value of the LookupTable + * @param {any} end A double representing the maximum scaler value of the LookupTable + * @returns {void} + * @memberof Colors + */ + public setTableRange(start, end) { + this.TableRange[0] = start; + this.TableRange[1] = end; + } + + /** + * Set the range in hue (using automatic generation). Hue ranges between [0,1]. + * @param {Number} start A double representing the minimum hue value in a range. Min. is 0 + * @param {Number} end A double representing the maximum hue value in a range. Max. is 1 + * @returns {void} + * @memberof Colors + */ + public setHueRange(start, end) { + this.HueRange[0] = start; + this.HueRange[1] = end; + } + + /** + * Set the range in saturation (using automatic generation). Saturation ranges between [0,1]. + * @param {Number} start A double representing the minimum Saturation value in a range. Min. is 0 + * @param {Number} end A double representing the maximum Saturation value in a range. Max. is 1 + * @returns {void} + * @memberof Colors + */ + public setSaturationRange(start, end) { + this.SaturationRange[0] = start; + this.SaturationRange[1] = end; + } + + /** + * Set the range in value (using automatic generation). Value ranges between [0,1]. + * @param {Numeber } start A double representing the minimum value in a range. Min. is 0 + * @param {Numeber} end A double representing the maximum value in a range. Max. is 1 + * @returns {void} + * @memberof Colors + */ + public setValueRange(start, end) { + // Set the range in value (using automatic generation). Value ranges between [0,1]. + this.ValueRange[0] = start; + this.ValueRange[1] = end; + } + + /** + * (Not Used) Sets the range of scalars which will be mapped. + * @param {Number} start the minimum scalar value in the range + * @param {Number} end the maximum scalar value in the range + * @returns {void} + * @memberof Colors + */ + public setRange(start, end) { + this.InputRange[0] = start; + this.InputRange[1] = end; + } + + /** + * Set the range in alpha (using automatic generation). Alpha ranges from [0,1]. + * @param {Number} start A double representing the minimum alpha value + * @param {Number} end A double representing the maximum alpha value + * @returns {void} + * @memberof Colors + */ + public setAlphaRange(start, end) { + // Set the range in alpha (using automatic generation). Alpha ranges from [0,1]. + this.AlphaRange[0] = start; + this.AlphaRange[1] = end; + } + + /** + * Map one value through the lookup table and return the color as an + * RGBA array of doubles between 0 and 1. + * @param {Number} scalar A double scalar value which will be mapped to a color in the LookupTable + * @returns {Number[]} An RGBA array of doubles between 0 and 1 + * @memberof Colors + */ + public getColor(scalar) { + return this.mapValue(scalar); + } + + /** + * Generate lookup table from hue, saturation, value, alpha min/max values. Table is built from linear ramp of each value. + * @param {Boolean} force true to force the build of the LookupTable. Otherwie, false. This is useful if a lookup table has been defined manually + * (using SetTableValue) and then an application decides to rebuild the lookup table using the implicit process. + * @returns {void} + * @memberof Colors + */ + public build(force) { + if (this.Table.length > 1 && !force) { + return; + } + + // Clear the table + this.Table = []; + + const maxIndex = this.NumberOfColors - 1; + + let hinc, sinc, vinc, ainc; + + if (maxIndex) { + hinc = (this.HueRange[1] - this.HueRange[0]) / maxIndex; + sinc = (this.SaturationRange[1] - this.SaturationRange[0]) / maxIndex; + vinc = (this.ValueRange[1] - this.ValueRange[0]) / maxIndex; + ainc = (this.AlphaRange[1] - this.AlphaRange[0]) / maxIndex; + } else { + hinc = sinc = vinc = ainc = 0.0; + } + + for (let i = 0; i <= maxIndex; i++) { + const hue = this.HueRange[0] + i * hinc; + const sat = this.SaturationRange[0] + i * sinc; + const val = this.ValueRange[0] + i * vinc; + const alpha = this.AlphaRange[0] + i * ainc; + + const rgb = HSVToRGB(hue, sat, val); + const c_rgba: Point4 = [0, 0, 0, 0]; + + switch (this.Ramp) { + case 'scurve': + c_rgba[0] = Math.floor( + 127.5 * (1.0 + Math.cos((1.0 - rgb[0]) * Math.PI)) + ); + c_rgba[1] = Math.floor( + 127.5 * (1.0 + Math.cos((1.0 - rgb[1]) * Math.PI)) + ); + c_rgba[2] = Math.floor( + 127.5 * (1.0 + Math.cos((1.0 - rgb[2]) * Math.PI)) + ); + c_rgba[3] = Math.floor(alpha * 255); + break; + case 'linear': + c_rgba[0] = Math.floor(rgb[0] * 255 + 0.5); + c_rgba[1] = Math.floor(rgb[1] * 255 + 0.5); + c_rgba[2] = Math.floor(rgb[2] * 255 + 0.5); + c_rgba[3] = Math.floor(alpha * 255 + 0.5); + break; + case 'sqrt': + c_rgba[0] = Math.floor(Math.sqrt(rgb[0]) * 255 + 0.5); + c_rgba[1] = Math.floor(Math.sqrt(rgb[1]) * 255 + 0.5); + c_rgba[2] = Math.floor(Math.sqrt(rgb[2]) * 255 + 0.5); + c_rgba[3] = Math.floor(Math.sqrt(alpha) * 255 + 0.5); + break; + default: + throw new Error(`Invalid Ramp value (${this.Ramp})`); + } + + this.Table.push(c_rgba); + } + + this.buildSpecialColors(); + } + + /** + * Ensures the out-of-range colors (Below range and Above range) are set correctly. + * @returns {void} + * @memberof Colors + */ + private buildSpecialColors() { + const numberOfColors = this.NumberOfColors; + const belowRangeColorIndex = numberOfColors + BELOW_RANGE_COLOR_INDEX; + const aboveRangeColorIndex = numberOfColors + ABOVE_RANGE_COLOR_INDEX; + const nanColorIndex = numberOfColors + NAN_COLOR_INDEX; + + // Below range color + if (this.UseBelowRangeColor || numberOfColors === 0) { + this.Table[belowRangeColorIndex] = this.BelowRangeColor; + } else { + // Duplicate the first color in the table. + this.Table[belowRangeColorIndex] = this.Table[0]; + } + + // Above range color + if (this.UseAboveRangeColor || numberOfColors === 0) { + this.Table[aboveRangeColorIndex] = this.AboveRangeColor; + } else { + // Duplicate the last color in the table. + this.Table[aboveRangeColorIndex] = this.Table[numberOfColors - 1]; + } + + // Always use NanColor + this.Table[nanColorIndex] = this.NaNColor; + } + + /** + * Similar to GetColor - Map one value through the lookup table and return the color as an + * RGBA array of doubles between 0 and 1. + * @param {Numeber} v A double scalar value which will be mapped to a color in the LookupTable + * @returns {Number[]} An RGBA array of doubles between 0 and 1 + * @memberof Colors + */ + private mapValue(v) { + const index = this.getIndex(v); + + if (index < 0) { + return this.NaNColor; + } else if (index === 0) { + if (this.UseBelowRangeColor && v < this.TableRange[0]) { + return this.BelowRangeColor; + } + } else if (index === this.NumberOfColors - 1) { + if (this.UseAboveRangeColor && v > this.TableRange[1]) { + return this.AboveRangeColor; + } + } + + return this.Table[index]; + } + + /** + * Return the table index associated with a particular value. + * @param {Number} v A double value which table index will be returned. + * @returns {Number} The index in the LookupTable + * @memberof Colors + */ + private getIndex(v) { + const p = { + Range: [], + MaxIndex: this.NumberOfColors - 1, + Shift: -this.TableRange[0], + Scale: 1, + }; + + if (this.TableRange[1] <= this.TableRange[0]) { + p.Scale = Number.MAX_VALUE; + } else { + p.Scale = p.MaxIndex / (this.TableRange[1] - this.TableRange[0]); + } + + p.Range[0] = this.TableRange[0]; + p.Range[1] = this.TableRange[1]; + + // First, check whether we have a number... + if (isNaN(v)) { + // For backwards compatibility + return -1; + } + + // Map to an index: + let index = linearIndexLookupMain(v, p); + + // For backwards compatibility, if the index indicates an + // Out-of-range value, truncate to index range for in-range colors. + if (index === this.NumberOfColors + BELOW_RANGE_COLOR_INDEX) { + index = 0; + } else if (index === this.NumberOfColors + ABOVE_RANGE_COLOR_INDEX) { + index = this.NumberOfColors - 1; + } + + return index; + } + + /** + * Directly load color into lookup table. Use [0,1] double values for color component specification. + * Make sure that you've either used the Build() method or used SetNumberOfTableValues() prior to using this method. + * @param {Number} index The index in the LookupTable of where to insert the color value + * @param {Number[]} rgba An array of [0,1] double values for an RGBA color component + * @returns {void} + * @memberof Colors + */ + public setTableValue(index, rgba) { + // Check if it index, red, green, blue and alpha were passed as parameter + if (arguments.length === 5) { + rgba = Array.prototype.slice.call(arguments, 1); + } + + // Check the index to make sure it is valid + if (index < 0) { + throw new Error( + `Can't set the table value for negative index (${index})` + ); + } + + if (index >= this.NumberOfColors) { + new Error( + `Index ${index} is greater than the number of colors ${this.NumberOfColors}` + ); + } + + this.Table[index] = rgba; + + if (index === 0 || index === this.NumberOfColors - 1) { + // This is needed due to the way the special colors are stored in + // The internal table. If Above/BelowRangeColors are not used and + // The min/max colors are changed in the table with this member + // Function, then the colors used for values outside the range may + // Be incorrect. Calling this here ensures the out-of-range colors + // Are set correctly. + this.buildSpecialColors(); + } + } +} + +export default LookupTable; diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/drawImageSync.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/drawImageSync.ts new file mode 100644 index 0000000000..98ffb7c01d --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/drawImageSync.ts @@ -0,0 +1,61 @@ +import now from './rendering/now'; +import { renderColorImage } from './rendering/renderColorImage'; +import { renderGrayscaleImage } from './rendering/renderGrayscaleImage'; +import { renderPseudoColorImage } from './rendering/renderPseudoColorImage'; +import { CPUFallbackEnabledElement } from '../../../types'; + +/** + * Draw an image to a given enabled element synchronously + * + * @param {EnabledElement} enabledElement An enabled element to draw into + * @param {Boolean} invalidated - true if pixel data has been invalidated and cached rendering should not be used + * @returns {void} + * @memberof Internal + */ +export default function ( + enabledElement: CPUFallbackEnabledElement, + invalidated: boolean +): void { + const image = enabledElement.image; + const canvas = enabledElement.canvas; + + // Check if enabledElement can be redrawn + if (!enabledElement.canvas || !enabledElement.image) { + return; + } + + // Start measuring the time needed to draw the image. + const start = now(); + + image.stats = { + lastGetPixelDataTime: -1.0, + lastStoredPixelDataToCanvasImageDataTime: -1.0, + lastPutImageDataTime: -1.0, + lastRenderTime: -1.0, + lastLutGenerateTime: -1.0, + }; + + if (image) { + let render = image.render; + + if (!render) { + if (enabledElement.viewport.colormap) { + render = renderPseudoColorImage; + } else if (image.color) { + render = renderColorImage; + } else { + render = renderGrayscaleImage; + } + } + + render(enabledElement, invalidated); + } + + // Calculate how long it took to draw the image/layers + const renderTimeInMs = now() - start; + + image.stats.lastRenderTime = renderTimeInMs; + + enabledElement.invalid = false; + enabledElement.needsRedraw = false; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/calculateTransform.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/calculateTransform.ts new file mode 100644 index 0000000000..66b9f6f4f4 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/calculateTransform.ts @@ -0,0 +1,137 @@ +import { Transform } from './transform'; +import { + CPUFallbackEnabledElement, + CPUFallbackTransform, +} from '../../../../types'; + +/** + * Calculate the transform for a Cornerstone enabled element + * + * @param {EnabledElement} enabledElement The Cornerstone Enabled Element + * @param {Number} [scale] The viewport scale + * @return {Transform} The current transform + * @memberof Internal + */ +export default function ( + enabledElement: CPUFallbackEnabledElement, + scale?: number +): CPUFallbackTransform { + const transform = new Transform(); + + if (!enabledElement.viewport.displayedArea) { + return transform; + } + + // Move to center of canvas + transform.translate( + enabledElement.canvas.width / 2, + enabledElement.canvas.height / 2 + ); + + // Apply the rotation before scaling for non square pixels + const angle = enabledElement.viewport.rotation; + + if (angle !== 0) { + transform.rotate((angle * Math.PI) / 180); + } + + // Apply the scale + let widthScale = enabledElement.viewport.scale; + let heightScale = enabledElement.viewport.scale; + + const width = + enabledElement.viewport.displayedArea.brhc.x - + (enabledElement.viewport.displayedArea.tlhc.x - 1); + const height = + enabledElement.viewport.displayedArea.brhc.y - + (enabledElement.viewport.displayedArea.tlhc.y - 1); + + if (enabledElement.viewport.displayedArea.presentationSizeMode === 'NONE') { + if ( + enabledElement.image.rowPixelSpacing < + enabledElement.image.columnPixelSpacing + ) { + widthScale *= + enabledElement.image.columnPixelSpacing / + enabledElement.image.rowPixelSpacing; + } else if ( + enabledElement.image.columnPixelSpacing < + enabledElement.image.rowPixelSpacing + ) { + heightScale *= + enabledElement.image.rowPixelSpacing / + enabledElement.image.columnPixelSpacing; + } + } else { + // These should be good for "TRUE SIZE" and "MAGNIFY" + widthScale = enabledElement.viewport.displayedArea.columnPixelSpacing; + heightScale = enabledElement.viewport.displayedArea.rowPixelSpacing; + + if ( + enabledElement.viewport.displayedArea.presentationSizeMode === + 'SCALE TO FIT' + ) { + // Fit TRUE IMAGE image (width/height) to window + const verticalScale = + enabledElement.canvas.height / (height * heightScale); + const horizontalScale = + enabledElement.canvas.width / (width * widthScale); + + // Apply new scale + widthScale = heightScale = Math.min(horizontalScale, verticalScale); + + if ( + enabledElement.viewport.displayedArea.rowPixelSpacing < + enabledElement.viewport.displayedArea.columnPixelSpacing + ) { + widthScale *= + enabledElement.viewport.displayedArea.columnPixelSpacing / + enabledElement.viewport.displayedArea.rowPixelSpacing; + } else if ( + enabledElement.viewport.displayedArea.columnPixelSpacing < + enabledElement.viewport.displayedArea.rowPixelSpacing + ) { + heightScale *= + enabledElement.viewport.displayedArea.rowPixelSpacing / + enabledElement.viewport.displayedArea.columnPixelSpacing; + } + } + } + + transform.scale(widthScale, heightScale); + + // Unrotate to so we can translate unrotated + if (angle !== 0) { + transform.rotate((-angle * Math.PI) / 180); + } + + // Apply the pan offset + transform.translate( + enabledElement.viewport.translation.x, + enabledElement.viewport.translation.y + ); + + // Rotate again so we can apply general scale + if (angle !== 0) { + transform.rotate((angle * Math.PI) / 180); + } + + if (scale !== undefined) { + // Apply the font scale + transform.scale(scale, scale); + } + + // Apply Flip if required + if (enabledElement.viewport.hflip) { + transform.scale(-1, 1); + } + + if (enabledElement.viewport.vflip) { + transform.scale(1, -1); + } + + // Move back from center of image + transform.translate(-width / 2, -height / 2); + + return transform; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/canvasToPixel.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/canvasToPixel.ts new file mode 100644 index 0000000000..5425ed6c80 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/canvasToPixel.ts @@ -0,0 +1,26 @@ +import getTransform from './getTransform'; + +import { Point2, CPUFallbackEnabledElement } from '../../../../types'; + +/** + * Converts a point in the canvas coordinate system to the pixel coordinate system + * system. This can be used to reset tools' image coordinates after modifications + * have been made in canvas space (e.g. moving a tool by a few cm, independent of + * image resolution). + * + * @param {HTMLElement} element The Cornerstone element within which the input point lies + * @param {[Number, Number]} pt The input point in the canvas coordinate system + * + * @returns {[Number, Number]} The transformed point in the pixel coordinate system + * @memberof PixelCoordinateSystem + */ +export default function ( + enabledElement: CPUFallbackEnabledElement, + pt: Point2 +): Point2 { + const transform = getTransform(enabledElement); + + transform.invert(); + + return transform.transformPoint(pt); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/computeAutoVoi.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/computeAutoVoi.ts new file mode 100644 index 0000000000..f726743296 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/computeAutoVoi.ts @@ -0,0 +1,50 @@ +import { IImage, CPUFallbackViewport } from '../../../../types'; + +/** + * Computes the VOI to display all the pixels if no VOI LUT data (Window Width/Window Center or voiLUT) exists on the viewport object. + * + * @param {Viewport} viewport - Object containing the viewport properties + * @param {Object} image An Image loaded by a Cornerstone Image Loader + * @returns {void} + * @memberof Internal + */ +export default function computeAutoVoi( + viewport: CPUFallbackViewport, + image: IImage +): void { + if (hasVoi(viewport)) { + return; + } + + const maxVoi = image.maxPixelValue * image.slope + image.intercept; + const minVoi = image.minPixelValue * image.slope + image.intercept; + const ww = maxVoi - minVoi; + const wc = (maxVoi + minVoi) / 2; + + if (viewport.voi === undefined) { + viewport.voi = { + windowWidth: ww, + windowCenter: wc, + }; + } else { + viewport.voi.windowWidth = ww; + viewport.voi.windowCenter = wc; + } +} + +/** + * Check if viewport has voi LUT data + * @param {any} viewport The viewport to check for voi LUT data + * @returns {Boolean} true viewport has LUT data (Window Width/Window Center or voiLUT). Otherwise, false. + * @memberof Internal + */ +function hasVoi(viewport: CPUFallbackViewport): boolean { + const hasLut = + viewport.voiLUT && viewport.voiLUT.lut && viewport.voiLUT.lut.length > 0; + + return ( + hasLut || + (viewport.voi.windowWidth !== undefined && + viewport.voi.windowCenter !== undefined) + ); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/createViewport.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/createViewport.ts new file mode 100644 index 0000000000..10d5318e2f --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/createViewport.ts @@ -0,0 +1,64 @@ +import { state } from './setDefaultViewport'; +import { + CPUFallbackViewportDisplayedArea, + CPUFallbackViewport, +} from '../../../../types'; + +// eslint-disable-next-line valid-jsdoc +/** + * Creates the default displayed area. + * C.10.4 Displayed Area Module: This Module describes Attributes required to define a Specified Displayed Area space. + * + * @returns {tlhc: {x,y}, brhc: {x, y},rowPixelSpacing: Number, columnPixelSpacing: Number, presentationSizeMode: Number} displayedArea object + * @memberof Internal + */ +function createDefaultDisplayedArea(): CPUFallbackViewportDisplayedArea { + return { + // Top Left Hand Corner + tlhc: { + x: 1, + y: 1, + }, + // Bottom Right Hand Corner + brhc: { + x: 1, + y: 1, + }, + rowPixelSpacing: 1, + columnPixelSpacing: 1, + presentationSizeMode: 'NONE', + }; +} + +/** + * Creates a new viewport object containing default values + * + * @returns {Viewport} viewport object + * @memberof Internal + */ +export default function createViewport(): CPUFallbackViewport { + const displayedArea = createDefaultDisplayedArea(); + const initialDefaultViewport = { + scale: 1, + translation: { + x: 0, + y: 0, + }, + voi: { + windowWidth: undefined, + windowCenter: undefined, + }, + invert: false, + pixelReplication: false, + rotation: 0, + hflip: false, + vflip: false, + modalityLUT: undefined, + voiLUT: undefined, + colormap: undefined, + labelmap: false, + displayedArea, + }; + + return Object.assign({}, initialDefaultViewport, state.viewport); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/doesImageNeedToBeRendered.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/doesImageNeedToBeRendered.ts new file mode 100644 index 0000000000..2c9f95d0fe --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/doesImageNeedToBeRendered.ts @@ -0,0 +1,37 @@ +import { CPUFallbackEnabledElement, IImage } from '../../../../types'; + +/** + * Determine whether or not an Enabled Element needs to be re-rendered. + * + * If the imageId has changed, or if any of the last rendered viewport + * parameters have changed, this function will return true. + * + * @param {EnabledElement} enabledElement An Enabled Element + * @param {Image} image An Image + * @return {boolean} Whether or not the Enabled Element needs to re-render its image + * @memberof rendering + */ +export default function doesImageNeedToBeRendered( + enabledElement: CPUFallbackEnabledElement, + image: IImage +): boolean { + const lastRenderedImageId = enabledElement.renderingTools.lastRenderedImageId; + const lastRenderedViewport = + enabledElement.renderingTools.lastRenderedViewport; + + return ( + image.imageId !== lastRenderedImageId || + !lastRenderedViewport || + lastRenderedViewport.windowCenter !== + enabledElement.viewport.voi.windowCenter || + lastRenderedViewport.windowWidth !== + enabledElement.viewport.voi.windowWidth || + lastRenderedViewport.invert !== enabledElement.viewport.invert || + lastRenderedViewport.rotation !== enabledElement.viewport.rotation || + lastRenderedViewport.hflip !== enabledElement.viewport.hflip || + lastRenderedViewport.vflip !== enabledElement.viewport.vflip || + lastRenderedViewport.modalityLUT !== enabledElement.viewport.modalityLUT || + lastRenderedViewport.voiLUT !== enabledElement.viewport.voiLUT || + lastRenderedViewport.colormap !== enabledElement.viewport.colormap + ); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/fitToWindow.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/fitToWindow.ts new file mode 100644 index 0000000000..8dfc52ce3f --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/fitToWindow.ts @@ -0,0 +1,23 @@ +import getImageFitScale from './getImageFitScale'; +import { CPUFallbackEnabledElement } from '../../../../types'; + +/** + * Adjusts an image's scale and translation so the image is centered and all pixels + * in the image are viewable. + * + * @param {HTMLElement} element The Cornerstone element to update + * @returns {void} + */ +export default function (enabledElement: CPUFallbackEnabledElement): void { + const { image } = enabledElement; + + // The new scale is the minimum of the horizontal and vertical scale values + enabledElement.viewport.scale = getImageFitScale( + enabledElement.canvas, + image, + enabledElement.viewport.rotation + ).scaleFactor; + + enabledElement.viewport.translation.x = 0; + enabledElement.viewport.translation.y = 0; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/generateColorLut.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/generateColorLut.ts new file mode 100644 index 0000000000..848df0b6a7 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/generateColorLut.ts @@ -0,0 +1,61 @@ +import getVOILUT from './getVOILut'; +import { IImage, CPUFallbackLUT } from '../../../../types'; + +/** + * Creates a LUT used while rendering to convert stored pixel values to + * display pixels + * + * @param {Image} image A Cornerstone Image Object + * @param {Number} windowWidth The Window Width + * @param {Number} windowCenter The Window Center + * @param {Boolean} invert A boolean describing whether or not the image has been inverted + * @param {Array} [voiLUT] A Volume of Interest Lookup Table + * + * @returns {Uint8ClampedArray} A lookup table to apply to the image + * @memberof Internal + */ +export default function ( + image: IImage, + windowWidth: number | number[], + windowCenter: number | number[], + invert: boolean, + voiLUT?: CPUFallbackLUT +) { + const maxPixelValue = image.maxPixelValue; + const minPixelValue = image.minPixelValue; + const offset = Math.min(minPixelValue, 0); + + if (image.cachedLut === undefined) { + const length = maxPixelValue - offset + 1; + + image.cachedLut = {}; + image.cachedLut.lutArray = new Uint8ClampedArray(length); + } + + const lut = image.cachedLut.lutArray; + const vlutfn = getVOILUT( + Array.isArray(windowWidth) ? windowWidth[0] : windowWidth, + Array.isArray(windowCenter) ? windowCenter[0] : windowCenter, + voiLUT + ); + + if (invert === true) { + for ( + let storedValue = minPixelValue; + storedValue <= maxPixelValue; + storedValue++ + ) { + lut[storedValue + -offset] = 255 - vlutfn(storedValue); + } + } else { + for ( + let storedValue = minPixelValue; + storedValue <= maxPixelValue; + storedValue++ + ) { + lut[storedValue + -offset] = vlutfn(storedValue); + } + } + + return lut; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/generateLut.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/generateLut.ts new file mode 100644 index 0000000000..9bfcf700a4 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/generateLut.ts @@ -0,0 +1,62 @@ +import getModalityLUT from './getModalityLUT'; +import getVOILUT from './getVOILut'; +import { IImage, CPUFallbackLUT } from '../../../../types'; + +/** + * Creates a LUT used while rendering to convert stored pixel values to + * display pixels + * + * @param {Image} image A Cornerstone Image Object + * @param {Number} windowWidth The Window Width + * @param {Number} windowCenter The Window Center + * @param {Boolean} invert A boolean describing whether or not the image has been inverted + * @param {Array} [modalityLUT] A modality Lookup Table + * @param {Array} [voiLUT] A Volume of Interest Lookup Table + * + * @returns {Uint8ClampedArray} A lookup table to apply to the image + * @memberof Internal + */ +export default function ( + image: IImage, + windowWidth: number, + windowCenter: number, + invert: boolean, + modalityLUT: CPUFallbackLUT, + voiLUT: CPUFallbackLUT +): Uint8ClampedArray { + const maxPixelValue = image.maxPixelValue; + const minPixelValue = image.minPixelValue; + const offset = Math.min(minPixelValue, 0); + + if (image.cachedLut === undefined) { + const length = maxPixelValue - offset + 1; + + image.cachedLut = {}; + image.cachedLut.lutArray = new Uint8ClampedArray(length); + } + + const lut = image.cachedLut.lutArray; + + const mlutfn = getModalityLUT(image.slope, image.intercept, modalityLUT); + const vlutfn = getVOILUT(windowWidth, windowCenter, voiLUT); + + if (invert === true) { + for ( + let storedValue = minPixelValue; + storedValue <= maxPixelValue; + storedValue++ + ) { + lut[storedValue + -offset] = 255 - vlutfn(mlutfn(storedValue)); + } + } else { + for ( + let storedValue = minPixelValue; + storedValue <= maxPixelValue; + storedValue++ + ) { + lut[storedValue + -offset] = vlutfn(mlutfn(storedValue)); + } + } + + return lut; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getDefaultViewport.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getDefaultViewport.ts new file mode 100644 index 0000000000..29bcbc5328 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getDefaultViewport.ts @@ -0,0 +1,86 @@ +import createViewport from './createViewport'; +import getImageFitScale from './getImageFitScale'; +import { + IImage, + CPUFallbackColormap, + CPUFallbackViewport, +} from '../../../../types'; + +/** + * Creates a new viewport object containing default values for the image and canvas + * + * @param {HTMLElement} canvas A Canvas DOM element + * @param {Image} image A Cornerstone Image Object + * @returns {Viewport} viewport object + * @memberof Internal + */ +export default function ( + canvas: HTMLCanvasElement, + image: IImage, + modality: string, + colormap?: CPUFallbackColormap +): CPUFallbackViewport { + if (canvas === undefined) { + throw new Error( + 'getDefaultViewport: parameter canvas must not be undefined' + ); + } + + if (image === undefined) { + return createViewport(); + } + + // Fit image to window + const scale = getImageFitScale(canvas, image, 0).scaleFactor; + + let voi; + + if (image.windowWidth && image.windowCenter) { + voi = { + windowWidth: Array.isArray(image.windowWidth) + ? image.windowWidth[0] + : image.windowWidth, + windowCenter: Array.isArray(image.windowCenter) + ? image.windowCenter[0] + : image.windowCenter, + }; + } else if (modality === 'PT') { + voi = { + windowWidth: 5, + windowCenter: 2.5, + }; + } + + return { + scale, + translation: { + x: 0, + y: 0, + }, + voi, + invert: image.invert, + pixelReplication: false, + rotation: 0, + hflip: false, + vflip: false, + modalityLUT: image.modalityLUT, + modality, + voiLUT: image.voiLUT, + colormap: colormap !== undefined ? colormap : image.colormap, + displayedArea: { + tlhc: { + x: 1, + y: 1, + }, + brhc: { + x: image.columns, + y: image.rows, + }, + rowPixelSpacing: + image.rowPixelSpacing === undefined ? 1 : image.rowPixelSpacing, + columnPixelSpacing: + image.columnPixelSpacing === undefined ? 1 : image.columnPixelSpacing, + presentationSizeMode: 'NONE', + }, + }; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getImageFitScale.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getImageFitScale.ts new file mode 100644 index 0000000000..743d7913ef --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getImageFitScale.ts @@ -0,0 +1,53 @@ +import { validateParameterUndefinedOrNull } from './validator'; +import getImageSize from './getImageSize'; +import { IImage } from '../../../../types'; + +/** + * Calculates the horizontal, vertical and minimum scale factor for an image + @param {{width, height}} canvas The window size where the image is displayed. This can be any HTML element or structure with a width, height fields (e.g. canvas). + * @param {any} image The cornerstone image object + * @param {Number} rotation Optional. The rotation angle of the image. + * @return {{horizontalScale, verticalScale, scaleFactor}} The calculated horizontal, vertical and minimum scale factor + * @memberof Internal + */ +export default function ( + canvas: HTMLCanvasElement, + image: IImage, + rotation: number | null = null +): { + verticalScale: number; + horizontalScale: number; + scaleFactor: number; +} { + validateParameterUndefinedOrNull( + canvas, + 'getImageScale: parameter canvas must not be undefined' + ); + validateParameterUndefinedOrNull( + image, + 'getImageScale: parameter image must not be undefined' + ); + + const imageSize = getImageSize(image, rotation); + const rowPixelSpacing = image.rowPixelSpacing || 1; + const columnPixelSpacing = image.columnPixelSpacing || 1; + let verticalRatio = 1; + let horizontalRatio = 1; + + if (rowPixelSpacing < columnPixelSpacing) { + horizontalRatio = columnPixelSpacing / rowPixelSpacing; + } else { + // even if they are equal we want to calculate this ratio (the ration might be 0.5) + verticalRatio = rowPixelSpacing / columnPixelSpacing; + } + + const verticalScale = canvas.height / imageSize.height / verticalRatio; + const horizontalScale = canvas.width / imageSize.width / horizontalRatio; + + // Fit image to window + return { + verticalScale, + horizontalScale, + scaleFactor: Math.min(horizontalScale, verticalScale), + }; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getImageSize.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getImageSize.ts new file mode 100644 index 0000000000..c7d9e6b314 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getImageSize.ts @@ -0,0 +1,55 @@ +import { validateParameterUndefinedOrNull } from './validator'; +import { IImage } from '../../../../types'; + +/** + * Check if the angle is rotated + * @param {Number} rotation the rotation angle + * @returns {Boolean} true if the angle is rotated; Otherwise, false. + * @memberof Internal + */ +function isRotated(rotation?: number | null): boolean { + return !( + rotation === null || + rotation === undefined || + rotation === 0 || + rotation === 180 + ); +} + +/** + * Retrieves the current image dimensions given an enabled element + * + * @param {any} image The Cornerstone image. + * @param {Number} rotation Optional. The rotation angle of the image. + * @return {{width:Number, height:Number}} The Image dimensions + * @memberof Internal + */ +export default function ( + image: IImage, + rotation = null +): { height: number; width: number } { + validateParameterUndefinedOrNull( + image, + 'getImageSize: parameter image must not be undefined' + ); + validateParameterUndefinedOrNull( + image.width, + 'getImageSize: parameter image must have width' + ); + validateParameterUndefinedOrNull( + image.height, + 'getImageSize: parameter image must have height' + ); + + if (isRotated(rotation)) { + return { + height: image.width, + width: image.height, + }; + } + + return { + width: image.width, + height: image.height, + }; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getLut.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getLut.ts new file mode 100644 index 0000000000..9ece45bf8a --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getLut.ts @@ -0,0 +1,53 @@ +import computeAutoVoi from './computeAutoVoi'; +import lutMatches from './lutMatches'; +import generateLut from './generateLut'; +import { IImage, CPUFallbackViewport } from '../../../../types'; + +/** + * Retrieve or generate a LUT Array for an Image and Viewport + * + * @param {Image} image An Image Object + * @param {Viewport} viewport An Viewport Object + * @param {Boolean} invalidated Whether or not the LUT data has been invalidated + * (e.g. by a change to the windowWidth, windowCenter, or invert viewport parameters). + * @return {Uint8ClampedArray} LUT Array + * @memberof rendering + */ +export default function ( + image: IImage, + viewport: CPUFallbackViewport, + invalidated: boolean +): Uint8ClampedArray { + // If we have a cached lut and it has the right values, return it immediately + if ( + image.cachedLut !== undefined && + image.cachedLut.windowCenter === viewport.voi.windowCenter && + image.cachedLut.windowWidth === viewport.voi.windowWidth && + lutMatches(image.cachedLut.modalityLUT, viewport.modalityLUT) && + lutMatches(image.cachedLut.voiLUT, viewport.voiLUT) && + image.cachedLut.invert === viewport.invert && + invalidated !== true + ) { + return image.cachedLut.lutArray; + } + + computeAutoVoi(viewport, image); + + // Lut is invalid or not present, regenerate it and cache it + generateLut( + image, + viewport.voi.windowWidth, + viewport.voi.windowCenter, + viewport.invert, + viewport.modalityLUT, + viewport.voiLUT + ); + + image.cachedLut.windowWidth = viewport.voi.windowWidth; + image.cachedLut.windowCenter = viewport.voi.windowCenter; + image.cachedLut.invert = viewport.invert; + image.cachedLut.voiLUT = viewport.voiLUT; + image.cachedLut.modalityLUT = viewport.modalityLUT; + + return image.cachedLut.lutArray; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getModalityLUT.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getModalityLUT.ts new file mode 100644 index 0000000000..9addbbb6bb --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getModalityLUT.ts @@ -0,0 +1,55 @@ +/** + * Generates a linear modality transformation function + * + * See DICOM PS3.3 C.11.1 Modality LUT Module + * + * http://dicom.nema.org/medical/Dicom/current/output/chtml/part03/sect_C.11.html + * + * @param {Number} slope m in the equation specified by Rescale Intercept (0028,1052). + * @param {Number} intercept The value b in relationship between stored values (SV) and the output units specified in Rescale Type (0028,1054). + + Output units = m*SV + b. + * @return {function(*): *} A linear modality LUT function. Given a stored pixel it returns the modality pixel value + * @memberof Internal + */ +function generateLinearModalityLUT(slope, intercept) { + return (storedPixelValue) => storedPixelValue * slope + intercept; +} + +function generateNonLinearModalityLUT(modalityLUT) { + const minValue = modalityLUT.lut[0]; + const maxValue = modalityLUT.lut[modalityLUT.lut.length - 1]; + const maxValueMapped = modalityLUT.firstValueMapped + modalityLUT.lut.length; + + return (storedPixelValue) => { + if (storedPixelValue < modalityLUT.firstValueMapped) { + return minValue; + } else if (storedPixelValue >= maxValueMapped) { + return maxValue; + } + + return modalityLUT.lut[storedPixelValue]; + }; +} + +/** + * Get the appropriate Modality LUT for the current situation. + * + * @param {Number} [slope] m in the equation specified by Rescale Intercept (0028,1052). + * @param {Number} [intercept] The value b in relationship between stored values (SV) and the output units specified in Rescale Type (0028,1054). + * @param {Function} [modalityLUT] A modality LUT function. Given a stored pixel it returns the modality pixel value. + * + * @return {function(*): *} A modality LUT function. Given a stored pixel it returns the modality pixel value. + * @memberof Internal + */ +export default function ( + slope: number, + intercept: number, + modalityLUT: unknown +) { + if (modalityLUT) { + return generateNonLinearModalityLUT(modalityLUT); + } + + return generateLinearModalityLUT(slope, intercept); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getTransform.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getTransform.ts new file mode 100644 index 0000000000..fe2eb3c1c0 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getTransform.ts @@ -0,0 +1,17 @@ +import calculateTransform from './calculateTransform'; +import { + CPUFallbackEnabledElement, + CPUFallbackTransform, +} from '../../../../types'; + +export default function ( + enabledElement: CPUFallbackEnabledElement +): CPUFallbackTransform { + // Todo: for some reason using the cached transfer after the first call + // does not give correct transform. + // if (enabledElement.transform) { + // return enabledElement.transform; + // } + + return calculateTransform(enabledElement); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getVOILut.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getVOILut.ts new file mode 100644 index 0000000000..295b4711c1 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/getVOILut.ts @@ -0,0 +1,74 @@ +/* eslint no-bitwise: 0 */ + +/** + * Volume of Interest Lookup Table Function + * + * @typedef {Function} VOILUTFunction + * + * @param {Number} modalityLutValue + * @returns {Number} transformed value + * @memberof Objects + */ + +/** + * @module: VOILUT + */ + +/** + * + * @param {Number} windowWidth Window Width + * @param {Number} windowCenter Window Center + * @returns {VOILUTFunction} VOI LUT mapping function + * @memberof VOILUT + */ +function generateLinearVOILUT(windowWidth: number, windowCenter: number) { + return function (modalityLutValue) { + return ((modalityLutValue - windowCenter) / windowWidth + 0.5) * 255.0; + }; +} + +/** + * Generate a non-linear volume of interest lookup table + * + * @param {LUT} voiLUT Volume of Interest Lookup Table Object + * + * @returns {VOILUTFunction} VOI LUT mapping function + * @memberof VOILUT + */ +function generateNonLinearVOILUT(voiLUT) { + // We don't trust the voiLUT.numBitsPerEntry, mainly thanks to Agfa! + const bitsPerEntry = Math.max(...voiLUT.lut).toString(2).length; + const shift = bitsPerEntry - 8; + const minValue = voiLUT.lut[0] >> shift; + const maxValue = voiLUT.lut[voiLUT.lut.length - 1] >> shift; + const maxValueMapped = voiLUT.firstValueMapped + voiLUT.lut.length - 1; + + return function (modalityLutValue) { + if (modalityLutValue < voiLUT.firstValueMapped) { + return minValue; + } else if (modalityLutValue >= maxValueMapped) { + return maxValue; + } + + return voiLUT.lut[modalityLutValue - voiLUT.firstValueMapped] >> shift; + }; +} + +/** + * Retrieve a VOI LUT mapping function given the current windowing settings + * and the VOI LUT for the image + * + * @param {Number} windowWidth Window Width + * @param {Number} windowCenter Window Center + * @param {LUT} [voiLUT] Volume of Interest Lookup Table Object + * + * @return {VOILUTFunction} VOI LUT mapping function + * @memberof VOILUT + */ +export default function (windowWidth: number, windowCenter: number, voiLUT) { + if (voiLUT) { + return generateNonLinearVOILUT(voiLUT); + } + + return generateLinearVOILUT(windowWidth, windowCenter); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/initializeRenderCanvas.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/initializeRenderCanvas.ts new file mode 100644 index 0000000000..42e8aa19de --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/initializeRenderCanvas.ts @@ -0,0 +1,37 @@ +import { CPUFallbackEnabledElement, IImage } from '../../../../types'; + +/** + * Sets size and clears canvas + * + * @param {Object} enabledElement Cornerstone Enabled Element + * @param {Object} image Image to be rendered + * @returns {void} + * @memberof rendering + */ +export default function ( + enabledElement: CPUFallbackEnabledElement, + image: IImage +): void { + const renderCanvas = enabledElement.renderingTools.renderCanvas; + + // Resize the canvas + renderCanvas.width = image.width; + renderCanvas.height = image.height; + + const canvasContext = renderCanvas.getContext('2d'); + + // NOTE - we need to fill the render canvas with white pixels since we + // control the luminance using the alpha channel to improve rendering performance. + canvasContext.fillStyle = 'white'; + canvasContext.fillRect(0, 0, renderCanvas.width, renderCanvas.height); + + const renderCanvasData = canvasContext.getImageData( + 0, + 0, + image.width, + image.height + ); + + enabledElement.renderingTools.renderCanvasContext = canvasContext; + enabledElement.renderingTools.renderCanvasData = renderCanvasData; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/lutMatches.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/lutMatches.ts new file mode 100644 index 0000000000..887d7ff273 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/lutMatches.ts @@ -0,0 +1,21 @@ +/** + * Check if two lookup tables match + * + * @param {LUT} a A lookup table function + * @param {LUT} b Another lookup table function + * @return {boolean} Whether or not they match + * @memberof rendering + */ +export default function (a: any, b: any) { + // If undefined, they are equal + if (!a && !b) { + return true; + } + // If one is undefined, not equal + if (!a || !b) { + return false; + } + + // Check the unique ids + return a.id === b.id; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/now.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/now.ts new file mode 100644 index 0000000000..a657dbd8c1 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/now.ts @@ -0,0 +1,13 @@ +/** + * Use the performance.now() method if possible, and if not, use Date.now() + * + * @return {number} Time elapsed since the time origin + * @memberof Polyfills + */ +export default function (): number { + if (window.performance) { + return performance.now(); + } + + return Date.now(); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/pixelToCanvas.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/pixelToCanvas.ts new file mode 100644 index 0000000000..fe60ba9312 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/pixelToCanvas.ts @@ -0,0 +1,22 @@ +import getTransform from './getTransform'; +import { CPUFallbackEnabledElement, Point2 } from '../../../../types'; + +/** + * Converts a point in the pixel coordinate system to the canvas coordinate system + * system. This can be used to render using canvas context without having the weird + * side effects that come from scaling and non square pixels + * + * @param {HTMLElement} element An HTML Element enabled for Cornerstone + * @param {{x: Number, y: Number}} pt The transformed point in the pixel coordinate system + * + * @returns {{x: Number, y: Number}} The input point in the canvas coordinate system + * @memberof PixelCoordinateSystem + */ +export default function ( + enabledElement: CPUFallbackEnabledElement, + pt: Point2 +): Point2 { + const transform = getTransform(enabledElement); + + return transform.transformPoint(pt); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderColorImage.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderColorImage.ts new file mode 100644 index 0000000000..a9f6cc5c7e --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderColorImage.ts @@ -0,0 +1,193 @@ +import now from './now'; +import generateColorLut from './generateColorLut'; +import storedColorPixelDataToCanvasImageData from './storedColorPixelDataToCanvasImageData'; +import storedRGBAPixelDataToCanvasImageData from './storedRGBAPixelDataToCanvasImageData'; +import setToPixelCoordinateSystem from './setToPixelCoordinateSystem'; +import doesImageNeedToBeRendered from './doesImageNeedToBeRendered'; +import initializeRenderCanvas from './initializeRenderCanvas'; +import saveLastRendered from './saveLastRendered'; +import { + IImage, + CPUFallbackViewport, + CPUFallbackEnabledElement, +} from '../../../../types'; + +/** + * Generates an appropriate Look Up Table to render the given image with the given window width and level (specified in the viewport) + * Uses an internal cache for performance + * + * @param {Object} image The image to be rendered + * @param {Object} viewport The viewport values used for rendering + * @returns {Uint8ClampedArray} Look Up Table array. + * @memberof rendering + */ +function getLut(image: IImage, viewport: CPUFallbackViewport) { + // If we have a cached lut and it has the right values, return it immediately + if ( + image.cachedLut !== undefined && + image.cachedLut.windowCenter === viewport.voi.windowCenter && + image.cachedLut.windowWidth === viewport.voi.windowWidth && + image.cachedLut.invert === viewport.invert + ) { + return image.cachedLut.lutArray; + } + + // Lut is invalid or not present, regenerate it and cache it + generateColorLut( + image, + viewport.voi.windowWidth, + viewport.voi.windowCenter, + viewport.invert + ); + image.cachedLut.windowWidth = viewport.voi.windowWidth; + image.cachedLut.windowCenter = viewport.voi.windowCenter; + image.cachedLut.invert = viewport.invert; + + return image.cachedLut.lutArray; +} + +/** + * Returns an appropriate canvas to render the Image. If the canvas available in the cache is appropriate + * it is returned, otherwise adjustments are made. It also sets the color transfer functions. + * + * @param {Object} enabledElement The cornerstone enabled element + * @param {Object} image The image to be rendered + * @param {Boolean} invalidated Is pixel data valid + * @returns {HTMLCanvasElement} An appropriate canvas for rendering the image + * @memberof rendering + */ +function getRenderCanvas( + enabledElement: CPUFallbackEnabledElement, + image: IImage, + invalidated: boolean +): HTMLCanvasElement { + const canvasWasColor = + enabledElement.renderingTools.lastRenderedIsColor === true; + + if (!enabledElement.renderingTools.renderCanvas || !canvasWasColor) { + enabledElement.renderingTools.renderCanvas = + document.createElement('canvas'); + } + + const renderCanvas = enabledElement.renderingTools.renderCanvas; + + // The ww/wc is identity and not inverted - get a canvas with the image rendered into it for + // Fast drawing + if ( + enabledElement.viewport.voi.windowWidth === 255 && + enabledElement.viewport.voi.windowCenter === 128 && + enabledElement.viewport.invert === false && + image.getCanvas && + image.getCanvas() + ) { + return image.getCanvas(); + } + + // Apply the lut to the stored pixel data onto the render canvas + if ( + doesImageNeedToBeRendered(enabledElement, image) === false && + invalidated !== true + ) { + return renderCanvas; + } + + // If our render canvas does not match the size of this image reset it + // NOTE: This might be inefficient if we are updating multiple images of different + // Sizes frequently. + if ( + renderCanvas.width !== image.width || + renderCanvas.height !== image.height + ) { + initializeRenderCanvas(enabledElement, image); + } + + // Get the lut to use + let start = now(); + const colorLut = getLut(image, enabledElement.viewport); + + image.stats = image.stats || {}; + image.stats.lastLutGenerateTime = now() - start; + + const renderCanvasData = enabledElement.renderingTools.renderCanvasData; + const renderCanvasContext = enabledElement.renderingTools.renderCanvasContext; + + // The color image voi/invert has been modified - apply the lut to the underlying + // Pixel data and put it into the renderCanvas + if (image.rgba) { + storedRGBAPixelDataToCanvasImageData( + image, + colorLut, + renderCanvasData.data + ); + } else { + storedColorPixelDataToCanvasImageData( + image, + colorLut, + renderCanvasData.data + ); + } + + start = now(); + renderCanvasContext.putImageData(renderCanvasData, 0, 0); + image.stats.lastPutImageDataTime = now() - start; + + return renderCanvas; +} + +/** + * API function to render a color image to an enabled element + * + * @param {EnabledElement} enabledElement The Cornerstone Enabled Element to redraw + * @param {Boolean} invalidated - true if pixel data has been invalidated and cached rendering should not be used + * @returns {void} + * @memberof rendering + */ +export function renderColorImage( + enabledElement: CPUFallbackEnabledElement, + invalidated: boolean +): void { + if (enabledElement === undefined) { + throw new Error( + 'renderColorImage: enabledElement parameter must not be undefined' + ); + } + + const image = enabledElement.image; + + if (image === undefined) { + throw new Error( + 'renderColorImage: image must be loaded before it can be drawn' + ); + } + + // Get the canvas context and reset the transform + const context = enabledElement.canvas.getContext('2d'); + + context.setTransform(1, 0, 0, 1, 0, 0); + + // Clear the canvas + context.fillStyle = 'black'; + context.fillRect( + 0, + 0, + enabledElement.canvas.width, + enabledElement.canvas.height + ); + + // Turn off image smooth/interpolation if pixelReplication is set in the viewport + context.imageSmoothingEnabled = !enabledElement.viewport.pixelReplication; + + // Save the canvas context state and apply the viewport properties + setToPixelCoordinateSystem(enabledElement, context); + + const renderCanvas = getRenderCanvas(enabledElement, image, invalidated); + + const sx = enabledElement.viewport.displayedArea.tlhc.x - 1; + const sy = enabledElement.viewport.displayedArea.tlhc.y - 1; + const width = enabledElement.viewport.displayedArea.brhc.x - sx; + const height = enabledElement.viewport.displayedArea.brhc.y - sy; + + context.drawImage(renderCanvas, sx, sy, width, height, 0, 0, width, height); + + enabledElement.renderingTools = saveLastRendered(enabledElement); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderGrayscaleImage.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderGrayscaleImage.ts new file mode 100644 index 0000000000..90b8b92e88 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderGrayscaleImage.ts @@ -0,0 +1,166 @@ +import storedPixelDataToCanvasImageData from './storedPixelDataToCanvasImageData'; +import storedPixelDataToCanvasImageDataPET from './storedPixelDataToCanvasImageDataPET'; +import storedPixelDataToCanvasImageDataRGBA from './storedPixelDataToCanvasImageDataRGBA'; +import setToPixelCoordinateSystem from './setToPixelCoordinateSystem'; +import now from './now'; +import getLut from './getLut'; +import doesImageNeedToBeRendered from './doesImageNeedToBeRendered'; +import initializeRenderCanvas from './initializeRenderCanvas'; +import saveLastRendered from './saveLastRendered'; +import { IImage, CPUFallbackEnabledElement } from '../../../../types'; + +/** + * Returns an appropriate canvas to render the Image. If the canvas available in the cache is appropriate + * it is returned, otherwise adjustments are made. It also sets the color transfer functions. + * + * @param {Object} enabledElement The cornerstone enabled element + * @param {Object} image The image to be rendered + * @param {Boolean} invalidated Is pixel data valid + * @param {Boolean} [useAlphaChannel = true] Will an alpha channel be used + * @returns {HTMLCanvasElement} An appropriate canvas for rendering the image + * @memberof rendering + */ +function getRenderCanvas( + enabledElement: CPUFallbackEnabledElement, + image: IImage, + invalidated: boolean, + useAlphaChannel = true +): HTMLCanvasElement { + const canvasWasColor = + enabledElement.renderingTools.lastRenderedIsColor === true; + + if (!enabledElement.renderingTools.renderCanvas || canvasWasColor) { + enabledElement.renderingTools.renderCanvas = + document.createElement('canvas'); + initializeRenderCanvas(enabledElement, image); + } + + const renderCanvas = enabledElement.renderingTools.renderCanvas; + + if ( + doesImageNeedToBeRendered(enabledElement, image) === false && + invalidated !== true + ) { + return renderCanvas; + } + + // If our render canvas does not match the size of this image reset it + // NOTE: This might be inefficient if we are updating multiple images of different + // Sizes frequently. + if ( + renderCanvas.width !== image.width || + renderCanvas.height !== image.height + ) { + initializeRenderCanvas(enabledElement, image); + } + + image.stats = image.stats || {}; + + const renderCanvasData = enabledElement.renderingTools.renderCanvasData; + const renderCanvasContext = enabledElement.renderingTools.renderCanvasContext; + + let start = now(); + image.stats.lastLutGenerateTime = now() - start; + + const { viewport } = enabledElement; + + // If modality is 'PT' the resulting scaled image is floatting point, and + // we cannot create a lut for it (cannot have float indices). Therefore, + // we use a mapping function to get the voiLUT from the values by applying + // the windowLevel and windowWidth. + if (viewport.modality === 'PT') { + const { windowWidth, windowCenter } = viewport.voi; + const minimum = windowCenter - windowWidth / 2; + const maximum = windowCenter + windowWidth / 2; + const range = maximum - minimum; + const collectedMultiplierTerms = 255.0 / range; + + let petVOILutFunction; + + if (viewport.invert) { + petVOILutFunction = (value) => + 255 - (value - minimum) * collectedMultiplierTerms; + } else { + // Note, don't need to math.floor, that is dealt with by setting the value in the Uint8Array. + petVOILutFunction = (value) => + (value - minimum) * collectedMultiplierTerms; + } + + storedPixelDataToCanvasImageDataPET( + image, + petVOILutFunction, + renderCanvasData.data + ); + } else { + // Get the lut to use + const lut = getLut(image, viewport, invalidated); + + if (useAlphaChannel) { + storedPixelDataToCanvasImageData(image, lut, renderCanvasData.data); + } else { + storedPixelDataToCanvasImageDataRGBA(image, lut, renderCanvasData.data); + } + } + + start = now(); + renderCanvasContext.putImageData(renderCanvasData, 0, 0); + image.stats.lastPutImageDataTime = now() - start; + + return renderCanvas; +} + +/** + * API function to draw a grayscale image to a given enabledElement + * + * @param {EnabledElement} enabledElement The Cornerstone Enabled Element to redraw + * @param {Boolean} invalidated - true if pixel data has been invalidated and cached rendering should not be used + * @returns {void} + * @memberof rendering + */ +export function renderGrayscaleImage( + enabledElement: CPUFallbackEnabledElement, + invalidated: boolean +): void { + if (enabledElement === undefined) { + throw new Error( + 'drawImage: enabledElement parameter must not be undefined' + ); + } + + const image = enabledElement.image; + + if (image === undefined) { + throw new Error('drawImage: image must be loaded before it can be drawn'); + } + + // Get the canvas context and reset the transform + const context = enabledElement.canvas.getContext('2d'); + + context.setTransform(1, 0, 0, 1, 0, 0); + + // Clear the canvas + context.fillStyle = 'black'; + context.fillRect( + 0, + 0, + enabledElement.canvas.width, + enabledElement.canvas.height + ); + + // Turn off image smooth/interpolation if pixelReplication is set in the viewport + context.imageSmoothingEnabled = !enabledElement.viewport.pixelReplication; + + // Save the canvas context state and apply the viewport properties + setToPixelCoordinateSystem(enabledElement, context); + + const renderCanvas = getRenderCanvas(enabledElement, image, invalidated); + + const sx = enabledElement.viewport.displayedArea.tlhc.x - 1; + const sy = enabledElement.viewport.displayedArea.tlhc.y - 1; + const width = enabledElement.viewport.displayedArea.brhc.x - sx; + const height = enabledElement.viewport.displayedArea.brhc.y - sy; + + context.drawImage(renderCanvas, sx, sy, width, height, 0, 0, width, height); + + enabledElement.renderingTools = saveLastRendered(enabledElement); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderPseudoColorImage.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderPseudoColorImage.ts new file mode 100644 index 0000000000..33ac358db0 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/renderPseudoColorImage.ts @@ -0,0 +1,203 @@ +import setToPixelCoordinateSystem from './setToPixelCoordinateSystem'; +import now from './now'; +import initializeRenderCanvas from './initializeRenderCanvas'; +import getLut from './getLut'; +import saveLastRendered from './saveLastRendered'; +import doesImageNeedToBeRendered from './doesImageNeedToBeRendered'; +import storedPixelDataToCanvasImageDataPseudocolorLUT from './storedPixelDataToCanvasImageDataPseudocolorLUT'; +import storedPixelDataToCanvasImageDataPseudocolorLUTPET from './storedPixelDataToCanvasImageDataPseudocolorLUTPET'; +import colors from '../colors/index'; +import { IImage, CPUFallbackEnabledElement } from '../../../../types'; + +function clamp(value: number, min: number, max: number) { + return Math.max(min, Math.min(max, value)); +} + +/** + * Returns an appropriate canvas to render the Image. If the canvas available in the cache is appropriate + * it is returned, otherwise adjustments are made. It also sets the color transfer functions. + * + * @param {Object} enabledElement The cornerstone enabled element + * @param {Object} image The image to be rendered + * @param {Boolean} invalidated Is pixel data valid + * @returns {HTMLCanvasElement} An appropriate canvas for rendering the image + * @memberof rendering + */ +function getRenderCanvas( + enabledElement: CPUFallbackEnabledElement, + image: IImage, + invalidated: boolean +): HTMLCanvasElement { + if (!enabledElement.renderingTools.renderCanvas) { + enabledElement.renderingTools.renderCanvas = + document.createElement('canvas'); + } + + const renderCanvas = enabledElement.renderingTools.renderCanvas; + + let colormap = + enabledElement.viewport.colormap || enabledElement.options.colormap; + + if (enabledElement.options && enabledElement.options.colormap) { + console.warn( + 'enabledElement.options.colormap is deprecated. Use enabledElement.viewport.colormap instead' + ); + } + if (colormap && typeof colormap === 'string') { + colormap = colors.getColormap(colormap); + } + + if (!colormap) { + throw new Error('renderPseudoColorImage: colormap not found.'); + } + + const colormapId = colormap.getId(); + + if ( + doesImageNeedToBeRendered(enabledElement, image) === false && + invalidated !== true && + enabledElement.renderingTools.colormapId === colormapId + ) { + return renderCanvas; + } + + // If our render canvas does not match the size of this image reset it + // NOTE: This might be inefficient if we are updating multiple images of different + // Sizes frequently. + if ( + renderCanvas.width !== image.width || + renderCanvas.height !== image.height + ) { + initializeRenderCanvas(enabledElement, image); + } + + // Get the lut to use + let start = now(); + + if ( + !enabledElement.renderingTools.colorLut || + invalidated || + enabledElement.renderingTools.colormapId !== colormapId + ) { + colormap.setNumberOfColors(256); + enabledElement.renderingTools.colorLut = colormap.createLookupTable(); + enabledElement.renderingTools.colormapId = colormapId; + } + + const renderCanvasData = enabledElement.renderingTools.renderCanvasData; + const renderCanvasContext = enabledElement.renderingTools.renderCanvasContext; + const { viewport } = enabledElement; + const colorLut = enabledElement.renderingTools.colorLut; + + if (viewport.modality === 'PT') { + const { windowWidth, windowCenter } = viewport.voi; + const minimum = windowCenter - windowWidth / 2; + const maximum = windowCenter + windowWidth / 2; + const range = maximum - minimum; + const collectedMultiplierTerms = 255.0 / range; + + let petVOILutFunction; + + if (viewport.invert) { + petVOILutFunction = (value) => { + return clamp( + Math.floor(255 - (value - minimum) * collectedMultiplierTerms), + 0, + 255 + ); + }; + } else { + petVOILutFunction = (value) => { + return clamp( + Math.floor((value - minimum) * collectedMultiplierTerms), + 0, + 255 + ); + }; + } + + storedPixelDataToCanvasImageDataPseudocolorLUTPET( + image, + petVOILutFunction, + colorLut, + renderCanvasData.data + ); + } else { + const lut = getLut(image, enabledElement.viewport, invalidated); + + image.stats = image.stats || {}; + image.stats.lastLutGenerateTime = now() - start; + + storedPixelDataToCanvasImageDataPseudocolorLUT( + image, + lut, + colorLut, + renderCanvasData.data + ); + } + + start = now(); + renderCanvasContext.putImageData(renderCanvasData, 0, 0); + image.stats.lastPutImageDataTime = now() - start; + + return renderCanvas; +} + +/** + * API function to draw a pseudo-color image to a given enabledElement + * + * @param {EnabledElement} enabledElement The Cornerstone Enabled Element to redraw + * @param {Boolean} invalidated - true if pixel data has been invalidated and cached rendering should not be used + * @returns {void} + * @memberof rendering + */ +export function renderPseudoColorImage( + enabledElement: CPUFallbackEnabledElement, + invalidated: boolean +): void { + if (enabledElement === undefined) { + throw new Error( + 'drawImage: enabledElement parameter must not be undefined' + ); + } + + const image = enabledElement.image; + + if (image === undefined) { + throw new Error('drawImage: image must be loaded before it can be drawn'); + } + + // Get the canvas context and reset the transform + const context = enabledElement.canvas.getContext('2d'); + + context.setTransform(1, 0, 0, 1, 0, 0); + + // Clear the canvas + context.fillStyle = 'black'; + context.fillRect( + 0, + 0, + enabledElement.canvas.width, + enabledElement.canvas.height + ); + + // Turn off image smooth/interpolation if pixelReplication is set in the viewport + context.imageSmoothingEnabled = !enabledElement.viewport.pixelReplication; + + // Save the canvas context state and apply the viewport properties + setToPixelCoordinateSystem(enabledElement, context); + + // If no options are set we will retrieve the renderCanvas through the + // Normal Canvas rendering path + // TODO: Add WebGL support for pseudocolor pipeline + const renderCanvas = getRenderCanvas(enabledElement, image, invalidated); + + const sx = enabledElement.viewport.displayedArea.tlhc.x - 1; + const sy = enabledElement.viewport.displayedArea.tlhc.y - 1; + const width = enabledElement.viewport.displayedArea.brhc.x - sx; + const height = enabledElement.viewport.displayedArea.brhc.y - sy; + + context.drawImage(renderCanvas, sx, sy, width, height, 0, 0, width, height); + + enabledElement.renderingTools = saveLastRendered(enabledElement); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/resetCamera.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/resetCamera.ts new file mode 100644 index 0000000000..1fc2057599 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/resetCamera.ts @@ -0,0 +1,29 @@ +import getImageFitScale from './getImageFitScale'; +import { CPUFallbackEnabledElement } from '../../../../types'; + +/** + * Resets the camera to the default position. which would be the center of the image. + * with no translation, no flipping, no zoom and proper scale. + */ +export default function ( + enabledElement: CPUFallbackEnabledElement, + resetPanZoomForViewPlane: boolean +): void { + const { canvas, image, viewport } = enabledElement; + const scale = getImageFitScale(canvas, image, 0).scaleFactor; + + viewport.vflip = false; + viewport.hflip = false; + + if (resetPanZoomForViewPlane) { + viewport.translation.x = 0; + viewport.translation.y = 0; + + viewport.displayedArea.tlhc.x = 1; + viewport.displayedArea.tlhc.y = 1; + viewport.displayedArea.brhc.x = image.columns; + viewport.displayedArea.brhc.y = image.rows; + + viewport.scale = scale; + } +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/resize.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/resize.ts new file mode 100644 index 0000000000..ce3b8e39cb --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/resize.ts @@ -0,0 +1,109 @@ +import fitToWindow from './fitToWindow'; +import getImageSize from './getImageSize'; +import { CPUFallbackEnabledElement } from '../../../../types'; + +/** + * This module is responsible for enabling an element to display images with cornerstone + * + * @param {HTMLElement} element The DOM element enabled for Cornerstone + * @param {HTMLElement} canvas The Canvas DOM element within the DOM element enabled for Cornerstone + * @returns {void} + */ +function setCanvasSize(enabledElement: CPUFallbackEnabledElement) { + const { canvas } = enabledElement; + const { clientWidth, clientHeight } = canvas; + + // Set the canvas to be same resolution as the client. + if (canvas.width !== clientWidth || canvas.height !== clientHeight) { + canvas.width = clientWidth; + canvas.height = clientHeight; + } +} + +/** + * Checks if the image of a given enabled element fitted the window + * before the resize + * + * @param {EnabledElement} enabledElement The Cornerstone Enabled Element + * @param {number} oldCanvasWidth The width of the canvas before the resize + * @param {number} oldCanvasHeight The height of the canvas before the resize + * @return {Boolean} true if it fitted the windows, false otherwise + */ +function wasFitToWindow( + enabledElement: CPUFallbackEnabledElement, + oldCanvasWidth: number, + oldCanvasHeight: number +): boolean { + const scale = enabledElement.viewport.scale; + const imageSize = getImageSize( + enabledElement.image, + enabledElement.viewport.rotation + ); + const imageWidth = Math.round(imageSize.width * scale); + const imageHeight = Math.round(imageSize.height * scale); + const x = enabledElement.viewport.translation.x; + const y = enabledElement.viewport.translation.y; + + return ( + (imageWidth === oldCanvasWidth && imageHeight <= oldCanvasHeight) || + (imageWidth <= oldCanvasWidth && + imageHeight === oldCanvasHeight && + x === 0 && + y === 0) + ); +} + +/** + * Rescale the image relative to the changed size of the canvas + * + * @param {EnabledElement} enabledElement The Cornerstone Enabled Element + * @param {number} oldCanvasWidth The width of the canvas before the resize + * @param {number} oldCanvasHeight The height of the canvas before the resize + * @return {void} + */ +function relativeRescale( + enabledElement: CPUFallbackEnabledElement, + oldCanvasWidth: number, + oldCanvasHeight: number +): void { + const scale = enabledElement.viewport.scale; + const canvasWidth = enabledElement.canvas.width; + const canvasHeight = enabledElement.canvas.height; + const relWidthChange = canvasWidth / oldCanvasWidth; + const relHeightChange = canvasHeight / oldCanvasHeight; + const relChange = Math.sqrt(relWidthChange * relHeightChange); + + enabledElement.viewport.scale = relChange * scale; +} + +/** + * Resizes an enabled element and optionally fits the image to window + * + * @param {HTMLElement} element The DOM element enabled for Cornerstone + * @param {Boolean} forceFitToWindow true to to force a refit, false to rescale accordingly + * @returns {void} + */ +export default function ( + enabledElement: CPUFallbackEnabledElement, + forceFitToWindow = false +): void { + const oldCanvasWidth = enabledElement.canvas.width; + const oldCanvasHeight = enabledElement.canvas.height; + + setCanvasSize(enabledElement); + + if (enabledElement.image === undefined) { + return; + } + + if ( + forceFitToWindow || + wasFitToWindow(enabledElement, oldCanvasWidth, oldCanvasHeight) + ) { + // Fit the image to the window again if it fitted before the resize + fitToWindow(enabledElement); + } else { + // Adapt the scale of a zoomed or panned image relative to the size change + relativeRescale(enabledElement, oldCanvasWidth, oldCanvasHeight); + } +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/saveLastRendered.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/saveLastRendered.ts new file mode 100644 index 0000000000..a28a569620 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/saveLastRendered.ts @@ -0,0 +1,36 @@ +import { + CPUFallbackEnabledElement, + CPUFallbackRenderingTools, +} from '../../../../types'; + +/** + * Saves the parameters of the last render into renderingTools, used later to decide if data can be reused. + * + * @param {Object} enabledElement Cornerstone EnabledElement + * @returns {Object} enabledElement.renderingTools + * @memberof rendering + */ + +export default function ( + enabledElement: CPUFallbackEnabledElement +): CPUFallbackRenderingTools { + const imageId = enabledElement.image.imageId; + const viewport = enabledElement.viewport; + const isColor = enabledElement.image.color; + + enabledElement.renderingTools.lastRenderedImageId = imageId; + enabledElement.renderingTools.lastRenderedIsColor = isColor; + enabledElement.renderingTools.lastRenderedViewport = { + windowCenter: viewport.voi.windowCenter, + windowWidth: viewport.voi.windowWidth, + invert: viewport.invert, + rotation: viewport.rotation, + hflip: viewport.hflip, + vflip: viewport.vflip, + modalityLUT: viewport.modalityLUT, + voiLUT: viewport.voiLUT, + colormap: viewport.colormap, + }; + + return enabledElement.renderingTools; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/setDefaultViewport.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/setDefaultViewport.ts new file mode 100644 index 0000000000..9d53effce2 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/setDefaultViewport.ts @@ -0,0 +1,17 @@ +import { CPUFallbackViewport } from '../../../../types'; + +const state = { + viewport: {}, +}; + +/** + * Sets new default values for `getDefaultViewport` + * + * @param {Object} viewport - Object that sets new default values for getDefaultViewport + * @returns {undefined} + */ +export default function (viewport: CPUFallbackViewport): void { + state.viewport = viewport || {}; +} + +export { state }; diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/setToPixelCoordinateSystem.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/setToPixelCoordinateSystem.ts new file mode 100644 index 0000000000..2ef084cdc1 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/setToPixelCoordinateSystem.ts @@ -0,0 +1,32 @@ +import calculateTransform from './calculateTransform'; +import { CPUFallbackEnabledElement } from '../../../../types'; + +/** + * Sets the canvas context transformation matrix to the pixel coordinate system. This allows + * geometry to be driven using the canvas context using coordinates in the pixel coordinate system + * @param {EnabledElement} enabledElement The + * @param {CanvasRenderingContext2D} context The CanvasRenderingContext2D for the enabledElement's Canvas + * @param {Number} [scale] Optional scale to apply + * @returns {void} + */ +export default function ( + enabledElement: CPUFallbackEnabledElement, + context: CanvasRenderingContext2D, + scale?: number +): void { + if (enabledElement === undefined) { + throw new Error( + 'setToPixelCoordinateSystem: parameter enabledElement must not be undefined' + ); + } + if (context === undefined) { + throw new Error( + 'setToPixelCoordinateSystem: parameter context must not be undefined' + ); + } + + const transform = calculateTransform(enabledElement, scale); + const m = transform.getMatrix(); + + context.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedColorPixelDataToCanvasImageData.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedColorPixelDataToCanvasImageData.ts new file mode 100644 index 0000000000..2fda5c4fc7 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedColorPixelDataToCanvasImageData.ts @@ -0,0 +1,58 @@ +import now from './now'; +import { IImage } from '../../../../types'; + +/** + * Converts stored color pixel values to display pixel values using a LUT. + * + * Note: Skips alpha value for any input image pixel data. + * + * @param {Image} image A Cornerstone Image Object + * @param {Array} lut Lookup table array + * @param {Uint8ClampedArray} canvasImageDataData canvasImageData.data buffer filled with white pixels + * + * @returns {void} + * @memberof Internal + */ +export default function ( + image: IImage, + lut: Uint8ClampedArray, + canvasImageDataData: Uint8ClampedArray +): void { + let start = now(); + const pixelData = image.getPixelData(); + + image.stats.lastGetPixelDataTime = now() - start; + + const minPixelValue = image.minPixelValue; + let canvasImageDataIndex = 0; + let storedPixelDataIndex = 0; + const numPixels = pixelData.length; + + // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes. + // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement + start = now(); + if (minPixelValue < 0) { + while (storedPixelDataIndex < numPixels) { + canvasImageDataData[canvasImageDataIndex++] = + lut[pixelData[storedPixelDataIndex++] + -minPixelValue]; // Red + canvasImageDataData[canvasImageDataIndex++] = + lut[pixelData[storedPixelDataIndex++] + -minPixelValue]; // Green + canvasImageDataData[canvasImageDataIndex] = + lut[pixelData[storedPixelDataIndex] + -minPixelValue]; // Blue + storedPixelDataIndex += 2; + canvasImageDataIndex += 2; + } + } else { + while (storedPixelDataIndex < numPixels) { + canvasImageDataData[canvasImageDataIndex++] = + lut[pixelData[storedPixelDataIndex++]]; // Red + canvasImageDataData[canvasImageDataIndex++] = + lut[pixelData[storedPixelDataIndex++]]; // Green + canvasImageDataData[canvasImageDataIndex] = + lut[pixelData[storedPixelDataIndex]]; // Blue + storedPixelDataIndex += 2; + canvasImageDataIndex += 2; + } + } + image.stats.lastStoredPixelDataToCanvasImageDataTime = now() - start; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageData.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageData.ts new file mode 100644 index 0000000000..d2fc77cf60 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageData.ts @@ -0,0 +1,76 @@ +import now from './now'; +import { IImage } from '../../../../types'; + +/** + * This function transforms stored pixel values into a canvas image data buffer + * by using a LUT. This is the most performance sensitive code in cornerstone and + * we use a special trick to make this go as fast as possible. Specifically we + * use the alpha channel only to control the luminance rather than the red, green and + * blue channels which makes it over 3x faster. The canvasImageDataData buffer needs + * to be previously filled with white pixels. + * + * NOTE: Attribution would be appreciated if you use this technique! + * + * @param {Image} image A Cornerstone Image Object + * @param {Array} lut Lookup table array + * @param {Uint8ClampedArray} canvasImageDataData canvasImageData.data buffer filled with white pixels + * + * @returns {void} + * @memberof Internal + */ +export default function ( + image: IImage, + lut: Uint8ClampedArray, + canvasImageDataData: Uint8ClampedArray +): void { + let start = now(); + const pixelData = image.getPixelData(); + + image.stats.lastGetPixelDataTime = now() - start; + + const numPixels = pixelData.length; + const minPixelValue = image.minPixelValue; + let canvasImageDataIndex = 3; + let storedPixelDataIndex = 0; + + // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes. + // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement + + // Added two paths (Int16Array, Uint16Array) to avoid polymorphic deoptimization in chrome. + start = now(); + if (pixelData instanceof Int16Array) { + if (minPixelValue < 0) { + while (storedPixelDataIndex < numPixels) { + canvasImageDataData[canvasImageDataIndex] = + lut[pixelData[storedPixelDataIndex++] + -minPixelValue]; // Alpha + canvasImageDataIndex += 4; + } + } else { + while (storedPixelDataIndex < numPixels) { + canvasImageDataData[canvasImageDataIndex] = + lut[pixelData[storedPixelDataIndex++]]; // Alpha + canvasImageDataIndex += 4; + } + } + } else if (pixelData instanceof Uint16Array) { + while (storedPixelDataIndex < numPixels) { + canvasImageDataData[canvasImageDataIndex] = + lut[pixelData[storedPixelDataIndex++]]; // Alpha + canvasImageDataIndex += 4; + } + } else if (minPixelValue < 0) { + while (storedPixelDataIndex < numPixels) { + canvasImageDataData[canvasImageDataIndex] = + lut[pixelData[storedPixelDataIndex++] + -minPixelValue]; // Alpha + canvasImageDataIndex += 4; + } + } else { + while (storedPixelDataIndex < numPixels) { + canvasImageDataData[canvasImageDataIndex] = + lut[pixelData[storedPixelDataIndex++]]; // Alpha + canvasImageDataIndex += 4; + } + } + + image.stats.lastStoredPixelDataToCanvasImageDataTime = now() - start; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataColorLUT.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataColorLUT.ts new file mode 100644 index 0000000000..27e9eeac4c --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataColorLUT.ts @@ -0,0 +1,60 @@ +import colors from '../colors'; +import now from './now'; +import { IImage, CPUFallbackLookupTable } from '../../../../types'; + +/** + * + * @param {Image} image A Cornerstone Image Object + * @param {LookupTable|Array} colorLut Lookup table array + * @param {Uint8ClampedArray} canvasImageDataData canvasImageData.data buffer filled with white pixels + * + * @returns {void} + * @memberof Internal + */ +function storedPixelDataToCanvasImageDataColorLUT( + image: IImage, + colorLut: CPUFallbackLookupTable, + canvasImageDataData: Uint8ClampedArray +): void { + let start = now(); + const pixelData = image.getPixelData(); + + image.stats.lastGetPixelDataTime = now() - start; + + const numPixels = pixelData.length; + const minPixelValue = image.minPixelValue; + let canvasImageDataIndex = 0; + let storedPixelDataIndex = 0; + let rgba; + let clut; + + start = now(); + + if (colorLut instanceof colors.LookupTable) { + clut = colorLut.Table; + } else { + clut = colorLut; + } + + if (minPixelValue < 0) { + while (storedPixelDataIndex < numPixels) { + rgba = clut[pixelData[storedPixelDataIndex++] + -minPixelValue]; + canvasImageDataData[canvasImageDataIndex++] = rgba[0]; + canvasImageDataData[canvasImageDataIndex++] = rgba[1]; + canvasImageDataData[canvasImageDataIndex++] = rgba[2]; + canvasImageDataData[canvasImageDataIndex++] = rgba[3]; + } + } else { + while (storedPixelDataIndex < numPixels) { + rgba = clut[pixelData[storedPixelDataIndex++]]; + canvasImageDataData[canvasImageDataIndex++] = rgba[0]; + canvasImageDataData[canvasImageDataIndex++] = rgba[1]; + canvasImageDataData[canvasImageDataIndex++] = rgba[2]; + canvasImageDataData[canvasImageDataIndex++] = rgba[3]; + } + } + + image.stats.lastStoredPixelDataToCanvasImageDataTime = now() - start; +} + +export default storedPixelDataToCanvasImageDataColorLUT; diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPET.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPET.ts new file mode 100644 index 0000000000..a56f20e3c3 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPET.ts @@ -0,0 +1,50 @@ +import now from './now'; +import { IImage } from '../../../../types'; + +/** + * This function transforms stored pixel values into a canvas image data buffer + * by using a LUT. This is the most performance sensitive code in cornerstone and + * we use a special trick to make this go as fast as possible. Specifically we + * use the alpha channel only to control the luminance rather than the red, green and + * blue channels which makes it over 3x faster. The canvasImageDataData buffer needs + * to be previously filled with white pixels. + * + * NOTE: Attribution would be appreciated if you use this technique! + * + * @param {Image} image A Cornerstone Image Object + * @param {Array} lut Lookup table array + * @param {Uint8ClampedArray} canvasImageDataData canvasImageData.data buffer filled with white pixels + * + * @returns {void} + * @memberof Internal + */ +export default function ( + image: IImage, + lutFunction: (value: number) => number, + canvasImageDataData: Uint8ClampedArray +): void { + let start = now(); + const pixelData = image.getPixelData(); + + image.stats.lastGetPixelDataTime = now() - start; + + const numPixels = pixelData.length; + // const minPixelValue = image.minPixelValue; + let canvasImageDataIndex = 3; + let storedPixelDataIndex = 0; + + // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes. + // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement + + // Added two paths (Int16Array, Uint16Array) to avoid polymorphic deoptimization in chrome. + start = now(); + + while (storedPixelDataIndex < numPixels) { + canvasImageDataData[canvasImageDataIndex] = lutFunction( + pixelData[storedPixelDataIndex++] + ); // Alpha + canvasImageDataIndex += 4; + } + + image.stats.lastStoredPixelDataToCanvasImageDataTime = now() - start; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPseudocolorLUT.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPseudocolorLUT.ts new file mode 100644 index 0000000000..13be1b6c1a --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPseudocolorLUT.ts @@ -0,0 +1,66 @@ +import colors from '../colors/index'; +import now from './now'; +import { IImage, CPUFallbackLookupTable } from '../../../../types'; + +/** + * + * @param {Image} image A Cornerstone Image Object + * @param {Array} grayscaleLut Lookup table array + * @param {LookupTable|Array} colorLut Lookup table array + * @param {Uint8ClampedArray} canvasImageDataData canvasImageData.data buffer filled with white pixels + * + * @returns {void} + * @memberof Internal + */ +function storedPixelDataToCanvasImageDataPseudocolorLUT( + image: IImage, + grayscaleLut: Uint8ClampedArray, + colorLut: CPUFallbackLookupTable, + canvasImageDataData: Uint8ClampedArray +): void { + let start = now(); + const pixelData = image.getPixelData(); + + image.stats.lastGetPixelDataTime = now() - start; + + const numPixels = pixelData.length; + const minPixelValue = image.minPixelValue; + let canvasImageDataIndex = 0; + let storedPixelDataIndex = 0; + let grayscale; + let rgba; + let clut; + + start = now(); + + if (colorLut instanceof colors.LookupTable) { + clut = colorLut.Table; + } else { + clut = colorLut; + } + + if (minPixelValue < 0) { + while (storedPixelDataIndex < numPixels) { + grayscale = + grayscaleLut[pixelData[storedPixelDataIndex++] + -minPixelValue]; + rgba = clut[grayscale]; + canvasImageDataData[canvasImageDataIndex++] = rgba[0]; + canvasImageDataData[canvasImageDataIndex++] = rgba[1]; + canvasImageDataData[canvasImageDataIndex++] = rgba[2]; + canvasImageDataData[canvasImageDataIndex++] = rgba[3]; + } + } else { + while (storedPixelDataIndex < numPixels) { + grayscale = grayscaleLut[pixelData[storedPixelDataIndex++]]; + rgba = clut[grayscale]; + canvasImageDataData[canvasImageDataIndex++] = rgba[0]; + canvasImageDataData[canvasImageDataIndex++] = rgba[1]; + canvasImageDataData[canvasImageDataIndex++] = rgba[2]; + canvasImageDataData[canvasImageDataIndex++] = rgba[3]; + } + } + + image.stats.lastStoredPixelDataToCanvasImageDataTime = now() - start; +} + +export default storedPixelDataToCanvasImageDataPseudocolorLUT; diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPseudocolorLUTPET.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPseudocolorLUTPET.ts new file mode 100644 index 0000000000..8ae79d8b4d --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataPseudocolorLUTPET.ts @@ -0,0 +1,68 @@ +import colors from '../colors/index'; +import now from './now'; +import { IImage, CPUFallbackLookupTable } from '../../../../types'; + +/** + * + * @param {Image} image A Cornerstone Image Object + * @param {Array} grayscaleLut Lookup table array + * @param {LookupTable|Array} colorLut Lookup table array + * @param {Uint8ClampedArray} canvasImageDataData canvasImageData.data buffer filled with white pixels + * + * @returns {void} + * @memberof Internal + */ +function storedPixelDataToCanvasImageDataPseudocolorLUTPET( + image: IImage, + lutFunction: (value: number) => number, + colorLut: CPUFallbackLookupTable, + canvasImageDataData: Uint8ClampedArray +): void { + let start = now(); + const pixelData = image.getPixelData(); + + image.stats.lastGetPixelDataTime = now() - start; + + const numPixels = pixelData.length; + const minPixelValue = image.minPixelValue; + let canvasImageDataIndex = 0; + let storedPixelDataIndex = 0; + let grayscale; + let rgba; + let clut; + + start = now(); + + if (colorLut instanceof colors.LookupTable) { + clut = colorLut.Table; + } else { + clut = colorLut; + } + + if (minPixelValue < 0) { + while (storedPixelDataIndex < numPixels) { + grayscale = lutFunction( + pixelData[storedPixelDataIndex++] + -minPixelValue + ); + + rgba = clut[grayscale]; + canvasImageDataData[canvasImageDataIndex++] = rgba[0]; + canvasImageDataData[canvasImageDataIndex++] = rgba[1]; + canvasImageDataData[canvasImageDataIndex++] = rgba[2]; + canvasImageDataData[canvasImageDataIndex++] = rgba[3]; + } + } else { + while (storedPixelDataIndex < numPixels) { + grayscale = lutFunction(pixelData[storedPixelDataIndex++]); + rgba = clut[grayscale]; + canvasImageDataData[canvasImageDataIndex++] = rgba[0]; + canvasImageDataData[canvasImageDataIndex++] = rgba[1]; + canvasImageDataData[canvasImageDataIndex++] = rgba[2]; + canvasImageDataData[canvasImageDataIndex++] = rgba[3]; + } + } + + image.stats.lastStoredPixelDataToCanvasImageDataTime = now() - start; +} + +export default storedPixelDataToCanvasImageDataPseudocolorLUTPET; diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataRGBA.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataRGBA.ts new file mode 100644 index 0000000000..afbab48d8e --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedPixelDataToCanvasImageDataRGBA.ts @@ -0,0 +1,81 @@ +import now from './now'; +import { IImage } from '../../../../types'; + +/** + * This function transforms stored pixel values into a canvas image data buffer + * by using a LUT. + * + * @param {Image} image A Cornerstone Image Object + * @param {Array} lut Lookup table array + * @param {Uint8ClampedArray} canvasImageDataData canvasImageData.data buffer filled with white pixels + * + * @returns {void} + * @memberof Internal + */ +export default function ( + image: IImage, + lut: Uint8ClampedArray, + canvasImageDataData: Uint8ClampedArray +): void { + let start = now(); + const pixelData = image.getPixelData(); + + image.stats.lastGetPixelDataTime = now() - start; + + const numPixels = pixelData.length; + const minPixelValue = image.minPixelValue; + let canvasImageDataIndex = 0; + let storedPixelDataIndex = 0; + let pixelValue; + + // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes. + // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement + + // Added two paths (Int16Array, Uint16Array) to avoid polymorphic deoptimization in chrome. + start = now(); + if (pixelData instanceof Int16Array) { + if (minPixelValue < 0) { + while (storedPixelDataIndex < numPixels) { + pixelValue = lut[pixelData[storedPixelDataIndex++] + -minPixelValue]; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = 255; // Alpha + } + } else { + while (storedPixelDataIndex < numPixels) { + pixelValue = lut[pixelData[storedPixelDataIndex++]]; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = 255; // Alpha + } + } + } else if (pixelData instanceof Uint16Array) { + while (storedPixelDataIndex < numPixels) { + pixelValue = lut[pixelData[storedPixelDataIndex++]]; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = 255; // Alpha + } + } else if (minPixelValue < 0) { + while (storedPixelDataIndex < numPixels) { + pixelValue = lut[pixelData[storedPixelDataIndex++] + -minPixelValue]; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = 255; // Alpha + } + } else { + while (storedPixelDataIndex < numPixels) { + pixelValue = lut[pixelData[storedPixelDataIndex++]]; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = pixelValue; + canvasImageDataData[canvasImageDataIndex++] = 255; // Alpha + } + } + + image.stats.lastStoredPixelDataToCanvasImageDataTime = now() - start; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedRGBAPixelDataToCanvasImageData.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedRGBAPixelDataToCanvasImageData.ts new file mode 100644 index 0000000000..4d4c20d59a --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/storedRGBAPixelDataToCanvasImageData.ts @@ -0,0 +1,56 @@ +import now from './now'; +import { IImage } from '../../../../types'; + +/** + * Converts stored RGBA color pixel values to display pixel values using a LUT. + * + * @param {Image} image A Cornerstone Image Object + * @param {Array} lut Lookup table array + * @param {Uint8ClampedArray} canvasImageDataData canvasImageData.data buffer filled with white pixels + * + * @returns {void} + * @memberof Internal + */ +export default function ( + image: IImage, + lut: Uint8ClampedArray, + canvasImageDataData: Uint8ClampedArray +): void { + let start = now(); + const pixelData = image.getPixelData(); + + image.stats.lastGetPixelDataTime = now() - start; + + const minPixelValue = image.minPixelValue; + let canvasImageDataIndex = 0; + let storedPixelDataIndex = 0; + const numPixels = pixelData.length; + + // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes. + // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement + start = now(); + if (minPixelValue < 0) { + while (storedPixelDataIndex < numPixels) { + canvasImageDataData[canvasImageDataIndex++] = + lut[pixelData[storedPixelDataIndex++] + -minPixelValue]; // Red + canvasImageDataData[canvasImageDataIndex++] = + lut[pixelData[storedPixelDataIndex++] + -minPixelValue]; // Green + canvasImageDataData[canvasImageDataIndex++] = + lut[pixelData[storedPixelDataIndex++] + -minPixelValue]; // Blue + canvasImageDataData[canvasImageDataIndex++] = + pixelData[storedPixelDataIndex++]; + } + } else { + while (storedPixelDataIndex < numPixels) { + canvasImageDataData[canvasImageDataIndex++] = + lut[pixelData[storedPixelDataIndex++]]; // Red + canvasImageDataData[canvasImageDataIndex++] = + lut[pixelData[storedPixelDataIndex++]]; // Green + canvasImageDataData[canvasImageDataIndex++] = + lut[pixelData[storedPixelDataIndex++]]; // Blue + canvasImageDataData[canvasImageDataIndex++] = + pixelData[storedPixelDataIndex++]; + } + } + image.stats.lastStoredPixelDataToCanvasImageDataTime = now() - start; +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/transform.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/transform.ts new file mode 100644 index 0000000000..fc186d5f78 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/transform.ts @@ -0,0 +1,126 @@ +import { + CPUFallbackTransform, + Point2, + TransformMatrix2D, +} from '../../../../types'; + +// By Simon Sarris +// Www.simonsarris.com +// Sarris@acm.org +// +// Free to use and distribute at will +// So long as you are nice to people, etc + +// Simple class for keeping track of the current transformation matrix + +// For instance: +// Var t = new Transform(); +// T.rotate(5); +// Var m = t.m; +// Ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); + +// Is equivalent to: +// Ctx.rotate(5); + +// But now you can retrieve it :) + +// Remember that this does not account for any CSS transforms applied to the canvas +export class Transform implements CPUFallbackTransform { + private m: TransformMatrix2D; + + constructor() { + this.reset(); + } + + getMatrix(): TransformMatrix2D { + return this.m; + } + + reset(): void { + this.m = [1, 0, 0, 1, 0, 0]; + } + + clone(): CPUFallbackTransform { + const transform = new Transform(); + + transform.m[0] = this.m[0]; + transform.m[1] = this.m[1]; + transform.m[2] = this.m[2]; + transform.m[3] = this.m[3]; + transform.m[4] = this.m[4]; + transform.m[5] = this.m[5]; + + return transform; + } + + multiply(matrix: TransformMatrix2D): void { + const m11 = this.m[0] * matrix[0] + this.m[2] * matrix[1]; + const m12 = this.m[1] * matrix[0] + this.m[3] * matrix[1]; + + const m21 = this.m[0] * matrix[2] + this.m[2] * matrix[3]; + const m22 = this.m[1] * matrix[2] + this.m[3] * matrix[3]; + + const dx = this.m[0] * matrix[4] + this.m[2] * matrix[5] + this.m[4]; + const dy = this.m[1] * matrix[4] + this.m[3] * matrix[5] + this.m[5]; + + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + this.m[4] = dx; + this.m[5] = dy; + } + + invert(): void { + const d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]); + const m0 = this.m[3] * d; + const m1 = -this.m[1] * d; + const m2 = -this.m[2] * d; + const m3 = this.m[0] * d; + const m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]); + const m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]); + + this.m[0] = m0; + this.m[1] = m1; + this.m[2] = m2; + this.m[3] = m3; + this.m[4] = m4; + this.m[5] = m5; + } + + rotate(rad: number): void { + const c = Math.cos(rad); + const s = Math.sin(rad); + const m11 = this.m[0] * c + this.m[2] * s; + const m12 = this.m[1] * c + this.m[3] * s; + const m21 = this.m[0] * -s + this.m[2] * c; + const m22 = this.m[1] * -s + this.m[3] * c; + + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + } + + translate(x: number, y: number): void { + this.m[4] += this.m[0] * x + this.m[2] * y; + this.m[5] += this.m[1] * x + this.m[3] * y; + } + + scale(sx: number, sy: number) { + this.m[0] *= sx; + this.m[1] *= sx; + this.m[2] *= sy; + this.m[3] *= sy; + } + + transformPoint(point: Point2): Point2 { + const x = point[0]; + const y = point[1]; + + return [ + x * this.m[0] + y * this.m[2] + this.m[4], + x * this.m[1] + y * this.m[3] + this.m[5], + ]; + } +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/validator.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/validator.ts new file mode 100644 index 0000000000..c1d6ed81aa --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/cpuFallback/rendering/validator.ts @@ -0,0 +1,31 @@ +/** + * Check if the supplied parameter is undefined and throws and error + * @param {any} checkParam the parameter to validate for undefined + * @param {any} errorMsg the error message to be thrown + * @returns {void} + * @memberof internal + */ +export function validateParameterUndefined( + checkParam: any | undefined, + errorMsg: string +): void { + if (checkParam === undefined) { + throw new Error(errorMsg); + } +} + +/** + * Check if the supplied parameter is undefined or null and throws and error + * @param {any} checkParam the parameter to validate for undefined + * @param {any} errorMsg the error message to be thrown + * @returns {void} + * @memberof internal + */ +export function validateParameterUndefinedOrNull( + checkParam: any | null | undefined, + errorMsg: string +): void { + if (checkParam === undefined || checkParam === null) { + throw new Error(errorMsg); + } +} diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeActor.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeActor.ts index d2f01a4665..d2a412cca5 100644 --- a/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeActor.ts +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeActor.ts @@ -21,9 +21,9 @@ async function createVolumeActor( throw new Error(`imageVolume with uid: ${imageVolume.uid} does not exist`) } - const { vtkImageData, vtkOpenGLTexture } = imageVolume + const { imageData, vtkOpenGLTexture } = imageVolume - const volumeMapper = createVolumeMapper(vtkImageData, vtkOpenGLTexture) + const volumeMapper = createVolumeMapper(imageData, vtkOpenGLTexture) if (blendMode) { volumeMapper.setBlendMode(blendMode) diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeMapper.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeMapper.ts index df559b4a68..822df98a7f 100644 --- a/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeMapper.ts +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/createVolumeMapper.ts @@ -1,14 +1,14 @@ import { vtkSharedVolumeMapper } from '../vtkClasses' export default function createVolumeMapper( - vtkImageData: any, + imageData: any, vtkOpenGLTexture: any ): any { const volumeMapper = vtkSharedVolumeMapper.newInstance() - volumeMapper.setInputData(vtkImageData) + volumeMapper.setInputData(imageData) - const spacing = vtkImageData.getSpacing() + const spacing = imageData.getSpacing() // Set the sample distance to half the mean length of one side. This is where the divide by 6 comes from. // https://github.com/Kitware/VTK/blob/6b559c65bb90614fb02eb6d1b9e3f0fca3fe4b0b/Rendering/VolumeOpenGL2/vtkSmartVolumeMapper.cxx#L344 const sampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6 diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/viewportTypeToViewportClass.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/viewportTypeToViewportClass.ts new file mode 100644 index 0000000000..f558035a54 --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/viewportTypeToViewportClass.ts @@ -0,0 +1,12 @@ +// TODO -> Eventually we'll need to register to this list +import StackViewport from '../StackViewport'; +import VolumeViewport from '../VolumeViewport'; +import ViewportType from '../../constants/viewportType'; + +const viewportTypeToViewportClass = { + [ViewportType.ORTHOGRAPHIC]: VolumeViewport, + [ViewportType.PERSPECTIVE]: VolumeViewport, + [ViewportType.STACK]: StackViewport, +}; + +export default viewportTypeToViewportClass; diff --git a/packages/cornerstone-render/src/RenderingEngine/helpers/viewportTypeUsesCustomRenderingPipeline.ts b/packages/cornerstone-render/src/RenderingEngine/helpers/viewportTypeUsesCustomRenderingPipeline.ts new file mode 100644 index 0000000000..8ebb3e855d --- /dev/null +++ b/packages/cornerstone-render/src/RenderingEngine/helpers/viewportTypeUsesCustomRenderingPipeline.ts @@ -0,0 +1,7 @@ +import viewportTypeToViewportClass from './viewportTypeToViewportClass'; + +export default function viewportTypeUsesCustomRenderingPipeline( + viewportType: string +) { + return viewportTypeToViewportClass[viewportType].useCustomRenderingPipeline; +} diff --git a/packages/cornerstone-render/src/cache/classes/ImageVolume.ts b/packages/cornerstone-render/src/cache/classes/ImageVolume.ts index be4aec1645..410bd198a2 100644 --- a/packages/cornerstone-render/src/cache/classes/ImageVolume.ts +++ b/packages/cornerstone-render/src/cache/classes/ImageVolume.ts @@ -22,7 +22,7 @@ export class ImageVolume implements IImageVolume { sizeInBytes?: number // Seems weird to pass this in? Why not grab it from scalarData.byteLength spacing: Point3 numVoxels: number - vtkImageData?: any + imageData?: any vtkOpenGLTexture: any // No good way of referencing vtk classes as they aren't classes. loadStatus?: Record imageIds?: Array @@ -34,7 +34,7 @@ export class ImageVolume implements IImageVolume { this.spacing = props.spacing this.origin = props.origin this.direction = props.direction - this.vtkImageData = props.vtkImageData + this.imageData = props.imageData this.scalarData = props.scalarData this.sizeInBytes = props.sizeInBytes this.vtkOpenGLTexture = vtkStreamingOpenGLTexture.newInstance() diff --git a/packages/cornerstone-render/src/enums/flipDirection.ts b/packages/cornerstone-render/src/enums/flipDirection.ts deleted file mode 100644 index db1aa49bcd..0000000000 --- a/packages/cornerstone-render/src/enums/flipDirection.ts +++ /dev/null @@ -1,7 +0,0 @@ -// flip direction -enum FlipDirection { - HORIZONTAL = 0, - VERTICAL = 1, -} - -export default FlipDirection diff --git a/packages/cornerstone-render/src/index.ts b/packages/cornerstone-render/src/index.ts index 566a174fbf..d1d4582583 100644 --- a/packages/cornerstone-render/src/index.ts +++ b/packages/cornerstone-render/src/index.ts @@ -1,6 +1,5 @@ import EVENTS from './enums/events' import ERROR_CODES from './enums/errorCodes' -import FLIP_DIRECTION from './enums/flipDirection' // import ORIENTATION from './constants/orientation' import VIEWPORT_TYPE from './constants/viewportType' @@ -31,6 +30,7 @@ import { } from './imageLoader' import requestPoolManager from './requestPool/requestPoolManager' import { setMaxSimultaneousRequests } from './requestPool/getMaxSimultaneousRequests' +import cpuColormaps from './RenderingEngine/helpers/cpuFallback/colors/colormaps' import { createAndCacheVolume, registerVolumeLoader, @@ -39,6 +39,13 @@ import { import getEnabledElement from './getEnabledElement' import configuration from './configuration' import metaData from './metaData' +import { + init, + getShouldUseCPURendering, + isCornerstoneInitialized, + setUseCPURenderingOnlyForDebugOrTests, + resetCPURenderingOnlyForDebugOrTests, +} from './init' // Classes import Settings from './Settings' @@ -58,7 +65,6 @@ export { // enums ERROR_CODES, EVENTS, - FLIP_DIRECTION, // constants ORIENTATION, VIEWPORT_TYPE, @@ -106,4 +112,11 @@ export { requestPoolManager, setMaxSimultaneousRequests, ImageVolume, + // CPU Rendering + init, + isCornerstoneInitialized, + getShouldUseCPURendering, + setUseCPURenderingOnlyForDebugOrTests, + resetCPURenderingOnlyForDebugOrTests, + cpuColormaps, } diff --git a/packages/cornerstone-render/src/init.ts b/packages/cornerstone-render/src/init.ts new file mode 100644 index 0000000000..b801daa6c8 --- /dev/null +++ b/packages/cornerstone-render/src/init.ts @@ -0,0 +1,76 @@ +import { getGPUTier } from 'detect-gpu'; + +let csRenderInitialized = false; +let useCPURendering = false; + +// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/By_example/Detect_WebGL +function hasActiveWebGLContext() { + // Create canvas element. The canvas is not added to the + // document itself, so it is never displayed in the + // browser window. + const canvas = document.createElement('canvas'); + // Get WebGLRenderingContext from canvas element. + const gl = + canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + + // Report the result. + if (gl && gl instanceof WebGLRenderingContext) { + return true; + } + + return false; +} + +async function init(defaultConfiguration = {}): Promise { + if (csRenderInitialized) { + return csRenderInitialized; + } + + // detectGPU + const hasWebGLGontext = hasActiveWebGLContext(); + if (!hasWebGLGontext) { + useCPURendering = true; + console.log('CornerstoneRender: GPU not detected, using CPU rendering'); + } else { + const gpuTier = await getGPUTier(); + console.log( + 'CornerstoneRender: Using detect-gpu to get the GPU benchmark:', + gpuTier + ); + if (gpuTier.tier < 1) { + console.log( + 'CornerstoneRender: GPU is not powerful enough, using CPU rendering' + ); + useCPURendering = true; + } else { + console.log('CornerstoneRender: using GPU rendering'); + } + } + csRenderInitialized = true; + return csRenderInitialized; +} + +function setUseCPURenderingOnlyForDebugOrTests(status: boolean): void { + useCPURendering = status; + csRenderInitialized = true; +} + +function resetCPURenderingOnlyForDebugOrTests() { + useCPURendering = !hasActiveWebGLContext(); +} + +function getShouldUseCPURendering(): boolean { + return useCPURendering; +} + +function isCornerstoneInitialized(): boolean { + return csRenderInitialized; +} + +export { + init, + getShouldUseCPURendering, + isCornerstoneInitialized, + setUseCPURenderingOnlyForDebugOrTests, + resetCPURenderingOnlyForDebugOrTests, +}; diff --git a/packages/cornerstone-render/src/types/CPUFallbackColormap.ts b/packages/cornerstone-render/src/types/CPUFallbackColormap.ts new file mode 100644 index 0000000000..4a6961b3f1 --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUFallbackColormap.ts @@ -0,0 +1,22 @@ +import Point4 from './Point4'; +import CPUFallbackLookupTable from './CPUFallbackLookupTable'; + +type CPUFallbackColormap = { + getId: () => string; + getColorSchemeName: () => string; + setColorSchemeName: (name: string) => void; + getNumberOfColors: () => number; + setNumberOfColors: (numColors: number) => void; + getColor: (index: number) => Point4; + getColorRepeating: (index: number) => Point4; + setColor: (index: number, rgba: Point4) => void; + addColor: (rgba: Point4) => void; + insertColor: (index: number, rgba: Point4) => void; + removeColor: (index: number) => void; + clearColors: () => void; + buildLookupTable: (lut: CPUFallbackLookupTable) => void; + createLookupTable: () => CPUFallbackLookupTable; + isValidIndex: (index: number) => boolean; +}; + +export default CPUFallbackColormap; diff --git a/packages/cornerstone-render/src/types/CPUFallbackColormapData.ts b/packages/cornerstone-render/src/types/CPUFallbackColormapData.ts new file mode 100644 index 0000000000..37f9c3b849 --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUFallbackColormapData.ts @@ -0,0 +1,12 @@ +import Point4 from './Point4'; + +type CPUFallbackColormapData = { + name: string; + numOfColors?: number; + colors?: Point4[]; + segmentedData?: unknown; + numColors?: number; + gamma?: number; +}; + +export default CPUFallbackColormapData; diff --git a/packages/cornerstone-render/src/types/CPUFallbackColormapsData.ts b/packages/cornerstone-render/src/types/CPUFallbackColormapsData.ts new file mode 100644 index 0000000000..305f45b045 --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUFallbackColormapsData.ts @@ -0,0 +1,7 @@ +import CPUFallbackColormapData from './CPUFallbackColormapData'; + +type CPUFallbackColormapsData = { + [key: string]: CPUFallbackColormapData; +}; + +export default CPUFallbackColormapsData; diff --git a/packages/cornerstone-render/src/types/CPUFallbackEnabledElement.ts b/packages/cornerstone-render/src/types/CPUFallbackEnabledElement.ts new file mode 100644 index 0000000000..2a7c75aec2 --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUFallbackEnabledElement.ts @@ -0,0 +1,68 @@ +import Point2 from './Point2'; +import Point3 from './Point2'; +import IImage from './IImage'; +import CPUFallbackViewport from './CPUFallbackViewport'; +import CPUFallbackTransform from './CPUFallbackTransform'; +import CPUFallbackColormap from './CPUFallbackColormap'; +import CPUFallbackRenderingTools from './CPUFallbackRenderingTools'; + +type CPUFallbackEnabledElement = { + scale?: number; + pan?: Point2; + zoom?: number; + rotation?: number; + image?: IImage; + canvas?: HTMLCanvasElement; + viewport?: CPUFallbackViewport; + colormap?: CPUFallbackColormap; + options?: { + [key: string]: unknown; + colormap?: CPUFallbackColormap; + }; + renderingTools?: CPUFallbackRenderingTools; + transform?: CPUFallbackTransform; + invalid?: boolean; + needsRedraw?: boolean; + metadata?: { + direction?: Float32Array; + dimensions?: Point3; + spacing?: Point3; + origin?: Point3; + imagePlaneModule?: { + frameOfReferenceUID: string; + rows: number; + columns: number; + imageOrientationPatient: number[]; + rowCosines: Point3; + columnCosines: Point3; + imagePositionPatient: number[]; + sliceThickness?: number; + sliceLocation?: number; + pixelSpacing: Point2; + rowPixelSpacing: number; + columnPixelSpacing: number; + }; + imagePixelModule?: { + samplesPerPixel: number; + photometricInterpretation: string; + rows: number; + columns: number; + bitsAllocated: number; + bitsStored: number; + highBit: number; + pixelRepresentation: number; + planarConfiguration?: number; + pixelAspectRatio?: number; + smallestPixelValue?: number; + largestPixelValue?: number; + redPaletteColorLookupTableDescriptor?: number[]; + greenPaletteColorLookupTableDescriptor?: number[]; + bluePaletteColorLookupTableDescriptor?: number[]; + redPaletteColorLookupTableData: number[]; + greenPaletteColorLookupTableData: number[]; + bluePaletteColorLookupTableData: number[]; + }; + }; +}; + +export default CPUFallbackEnabledElement; diff --git a/packages/cornerstone-render/src/types/CPUFallbackLUT.ts b/packages/cornerstone-render/src/types/CPUFallbackLUT.ts new file mode 100644 index 0000000000..93cdd66f28 --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUFallbackLUT.ts @@ -0,0 +1,5 @@ +type CPUFallbackLUT = { + lut: number[]; +}; + +export default CPUFallbackLUT; diff --git a/packages/cornerstone-render/src/types/CPUFallbackLookupTable.ts b/packages/cornerstone-render/src/types/CPUFallbackLookupTable.ts new file mode 100644 index 0000000000..6019fd9137 --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUFallbackLookupTable.ts @@ -0,0 +1,17 @@ +import Point4 from './Point4'; + +interface CPUFallbackLookupTable { + setNumberOfTableValues: (number: number) => void; + setRamp: (ramp: string) => void; + setTableRange: (start: number, end: number) => void; + setHueRange: (start: number, end: number) => void; + setSaturationRange: (start: number, end: number) => void; + setValueRange: (start: number, end: number) => void; + setRange: (start: number, end: number) => void; + setAlphaRange: (start: number, end: number) => void; + getColor: (scalar: number) => Point4; + build: (force: boolean) => void; + setTableValue(index: number, rgba: Point4); +} + +export default CPUFallbackLookupTable; diff --git a/packages/cornerstone-render/src/types/CPUFallbackRenderingTools.ts b/packages/cornerstone-render/src/types/CPUFallbackRenderingTools.ts new file mode 100644 index 0000000000..32ba971fcf --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUFallbackRenderingTools.ts @@ -0,0 +1,33 @@ +import CPUFallbackLookupTable from './CPUFallbackLookupTable'; +import CPUFallbackLUT from './CPUFallbackLUT'; + +type CPUFallbackRenderingTools = { + renderCanvas?: HTMLCanvasElement; + lastRenderedIsColor?: boolean; + lastRenderedImageId?: string; + lastRenderedViewport?: { + windowWidth: number | number[]; + windowCenter: number | number[]; + invert: boolean; + rotation: number; + hflip: boolean; + vflip: boolean; + modalityLUT: CPUFallbackLUT; + voiLUT: CPUFallbackLUT; + colormap: unknown; + }; + renderCanvasContext?: { + putImageData: ( + renderCanvasData: unknown, + dx: number, + dy: number + ) => unknown; + }; + colormapId?: string; + colorLut?: CPUFallbackLookupTable; + renderCanvasData?: { + data: Uint8ClampedArray; + }; +}; + +export default CPUFallbackRenderingTools; diff --git a/packages/cornerstone-render/src/types/CPUFallbackTransform.ts b/packages/cornerstone-render/src/types/CPUFallbackTransform.ts new file mode 100644 index 0000000000..acf345930f --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUFallbackTransform.ts @@ -0,0 +1,16 @@ +import Point2 from './Point2'; +import TransformMatrix2D from './TransformMatrix2D'; + +interface CPUFallbackTransform { + reset: () => void; + clone: () => CPUFallbackTransform; + multiply: (matrix: TransformMatrix2D) => void; + getMatrix: () => TransformMatrix2D; + invert: () => void; + rotate: (rad: number) => void; + translate: (x: number, y: number) => void; + scale: (sx: number, sy: number) => void; + transformPoint: (point: Point2) => Point2; +} + +export default CPUFallbackTransform; diff --git a/packages/cornerstone-render/src/types/CPUFallbackViewport.ts b/packages/cornerstone-render/src/types/CPUFallbackViewport.ts new file mode 100644 index 0000000000..b8070ce9f4 --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUFallbackViewport.ts @@ -0,0 +1,27 @@ +import CPUFallbackViewportDisplayedArea from './CPUFallbackViewportDisplayedArea'; +import CPUFallbackColormap from './CPUFallbackColormap'; +import CPUFallbackLUT from './CPUFallbackLUT'; + +type CPUFallbackViewport = { + scale?: number; + translation?: { + x: number; + y: number; + }; + voi?: { + windowWidth: number; + windowCenter: number; + }; + invert?: boolean; + pixelReplication?: boolean; + rotation?: number; + hflip?: boolean; + vflip?: boolean; + modalityLUT?: CPUFallbackLUT; + voiLUT?: CPUFallbackLUT; + colormap?: CPUFallbackColormap; + displayedArea?: CPUFallbackViewportDisplayedArea; + modality?: string; +}; + +export default CPUFallbackViewport; diff --git a/packages/cornerstone-render/src/types/CPUFallbackViewportDisplayedArea.ts b/packages/cornerstone-render/src/types/CPUFallbackViewportDisplayedArea.ts new file mode 100644 index 0000000000..7a874c8720 --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUFallbackViewportDisplayedArea.ts @@ -0,0 +1,15 @@ +type CPUFallbackViewportDisplayedArea = { + tlhc: { + x: number; + y: number; + }; + brhc: { + x: number; + y: number; + }; + rowPixelSpacing: number; + columnPixelSpacing: number; + presentationSizeMode: string; +}; + +export default CPUFallbackViewportDisplayedArea; diff --git a/packages/cornerstone-render/src/types/CPUIImageData.ts b/packages/cornerstone-render/src/types/CPUIImageData.ts new file mode 100644 index 0000000000..67ca414c9b --- /dev/null +++ b/packages/cornerstone-render/src/types/CPUIImageData.ts @@ -0,0 +1,20 @@ +import { Point3, Scaling } from '../types'; + +type IImageData = { + dimensions: Point3; + direction: Float32Array; + spacing: Point3; + origin: Point3; + imageData: { + worldToIndex?: (point: Point3) => Point3; + indexToWorld?: (point: Point3) => Point3; + getWorldToIndex?: () => number[]; + getIndexToWorld?: () => number[]; + getSpacing?: () => Point3; + }; + metadata: { Modality: string }; + scalarData: number[]; + scaling: Scaling; +}; + +export default IImageData; diff --git a/packages/cornerstone-render/src/types/FlipDirection.ts b/packages/cornerstone-render/src/types/FlipDirection.ts new file mode 100644 index 0000000000..afd8bf3957 --- /dev/null +++ b/packages/cornerstone-render/src/types/FlipDirection.ts @@ -0,0 +1,7 @@ +// flip direction +type FlipDirection = { + flipHorizontal: boolean; + flipVertical: boolean; +}; + +export default FlipDirection; diff --git a/packages/cornerstone-render/src/types/IImage.ts b/packages/cornerstone-render/src/types/IImage.ts index ee90fe179a..de9ac2bee4 100644 --- a/packages/cornerstone-render/src/types/IImage.ts +++ b/packages/cornerstone-render/src/types/IImage.ts @@ -1,3 +1,7 @@ +import CPUFallbackLUT from './CPUFallbackLUT' +import CPUFallbackColormap from './CPUFallbackColormap' +import CPUFallbackEnabledElement from './CPUFallbackEnabledElement' + interface IImage { imageId: string sharedCacheKey?: string @@ -5,8 +9,8 @@ interface IImage { maxPixelValue: number slope: number intercept: number - windowCenter: number[] - windowWidth: number[] + windowCenter: number[] | number + windowWidth: number[] | number getPixelData: () => Array getCanvas: () => HTMLCanvasElement rows: number @@ -16,11 +20,18 @@ interface IImage { color: boolean rgba: boolean numComps: number + render?: ( + enabledElement: CPUFallbackEnabledElement, + invalidated: boolean + ) => unknown columnPixelSpacing: number rowPixelSpacing: number sliceThickness?: number invert: boolean sizeInBytes: number + modalityLUT?: CPUFallbackLUT + voiLUT?: CPUFallbackLUT + colormap?: CPUFallbackColormap scaling?: { PET?: { // @TODO: Do these values exist? @@ -31,6 +42,22 @@ interface IImage { suvbwToSuvbsa?: number } } + stats?: { + lastStoredPixelDataToCanvasImageDataTime?: number + lastGetPixelDataTime?: number + lastPutImageDataTime?: number + lastLutGenerateTime?: number + lastRenderedViewport?: unknown + lastRenderTime?: number + } + cachedLut?: { + windowWidth?: number | number[] + windowCenter?: number | number[] + invert?: boolean + lutArray?: Uint8ClampedArray + modalityLUT?: unknown + voiLUT?: CPUFallbackLUT + } } export default IImage diff --git a/packages/cornerstone-render/src/types/IImageData.ts b/packages/cornerstone-render/src/types/IImageData.ts index 77225b78e0..9f9d2b9745 100644 --- a/packages/cornerstone-render/src/types/IImageData.ts +++ b/packages/cornerstone-render/src/types/IImageData.ts @@ -4,10 +4,10 @@ import { Point3, Scaling } from '../types' type IImageData = { dimensions: Point3 direction: Float32Array - spacing: Float32Array - origin: Float32Array + spacing: Point3 + origin: Point3 scalarData: Float32Array - vtkImageData: vtkImageData + imageData: vtkImageData metadata: { Modality: string } scaling?: Scaling } diff --git a/packages/cornerstone-render/src/types/IImageVolume.ts b/packages/cornerstone-render/src/types/IImageVolume.ts index d4ba58771c..a28e44b506 100644 --- a/packages/cornerstone-render/src/types/IImageVolume.ts +++ b/packages/cornerstone-render/src/types/IImageVolume.ts @@ -21,7 +21,7 @@ interface IImageVolume { sizeInBytes?: number spacing: Point3 numVoxels: number - vtkImageData?: vtkImageData + imageData?: vtkImageData vtkOpenGLTexture: any loadStatus?: Record imageIds?: Array diff --git a/packages/cornerstone-render/src/types/IViewport.ts b/packages/cornerstone-render/src/types/IViewport.ts index 321b7b14a5..4888564291 100644 --- a/packages/cornerstone-render/src/types/IViewport.ts +++ b/packages/cornerstone-render/src/types/IViewport.ts @@ -4,7 +4,6 @@ import Point2 from './Point2' import Point3 from './Point3' import ViewportInputOptions from './ViewportInputOptions' import { ActorEntry } from './IActor' -import { vtkSlabCamera } from '../RenderingEngine/vtkClasses' interface IViewport { uid: string @@ -35,7 +34,6 @@ interface IViewport { reset(immediate: boolean): void resetCamera(): void getCanvas(): HTMLCanvasElement - getVtkActiveCamera(): vtkCamera | vtkSlabCamera getCamera(): ICamera setCamera(cameraInterface: ICamera): void _getCorners(bounds: Array): Array[] diff --git a/packages/cornerstone-render/src/types/IVolume.ts b/packages/cornerstone-render/src/types/IVolume.ts index e9e2a43c37..310f6466dd 100644 --- a/packages/cornerstone-render/src/types/IVolume.ts +++ b/packages/cornerstone-render/src/types/IVolume.ts @@ -11,7 +11,7 @@ interface IVolume { direction: Array scalarData: Float32Array | Uint8Array sizeInBytes?: number - vtkImageData?: vtkImageData + imageData?: vtkImageData scaling?: { PET?: { // @TODO: Do these values exist? diff --git a/packages/cornerstone-render/src/types/Point4.ts b/packages/cornerstone-render/src/types/Point4.ts new file mode 100644 index 0000000000..4d223a10a2 --- /dev/null +++ b/packages/cornerstone-render/src/types/Point4.ts @@ -0,0 +1,6 @@ +/** + * This represents a 4-vector or RGBA value. + */ +type Point4 = [number, number, number, number]; + +export default Point4; diff --git a/packages/cornerstone-render/src/types/StackProperties.ts b/packages/cornerstone-render/src/types/StackProperties.ts index e066d56435..c2504fe502 100644 --- a/packages/cornerstone-render/src/types/StackProperties.ts +++ b/packages/cornerstone-render/src/types/StackProperties.ts @@ -5,6 +5,8 @@ type StackProperties = { invert?: boolean interpolationType?: number rotation?: number + flipHorizontal?: boolean + flipVertical?: boolean } export default StackProperties diff --git a/packages/cornerstone-render/src/types/TransformMatrix2D.ts b/packages/cornerstone-render/src/types/TransformMatrix2D.ts new file mode 100644 index 0000000000..6232c2398a --- /dev/null +++ b/packages/cornerstone-render/src/types/TransformMatrix2D.ts @@ -0,0 +1,3 @@ +type TransformMatrix2D = [number, number, number, number, number, number]; + +export default TransformMatrix2D; diff --git a/packages/cornerstone-render/src/types/index.ts b/packages/cornerstone-render/src/types/index.ts index 3813382e60..a86f5ba363 100644 --- a/packages/cornerstone-render/src/types/index.ts +++ b/packages/cornerstone-render/src/types/index.ts @@ -18,9 +18,11 @@ import type Metadata from './Metadata' import type Orientation from './Orientation' import type Point2 from './Point2' import type Point3 from './Point3' +import type Point4 from './Point4' import type IStreamingImageVolume from './IStreamingImageVolume' import type ViewportInputOptions from './ViewportInputOptions' import type IImageData from './IImageData' +import type CPUIImageData from './CPUIImageData' import type IImage from './IImage' import type { PetScaling, @@ -29,6 +31,20 @@ import type { } from './ScalingParameters' import type StackProperties from './StackProperties' import type IViewportUID from './IViewportUID' +import type FlipDirection from './FlipDirection' + +// CPU types +import type CPUFallbackEnabledElement from './CPUFallbackEnabledElement' +import type CPUFallbackViewport from './CPUFallbackViewport' +import type CPUFallbackTransform from './CPUFallbackTransform' +import type CPUFallbackColormapData from './CPUFallbackColormapData' +import type CPUFallbackViewportDisplayedArea from './CPUFallbackViewportDisplayedArea' +import type CPUFallbackColormapsData from './CPUFallbackColormapsData' +import type CPUFallbackColormap from './CPUFallbackColormap' +import type TransformMatrix2D from './TransformMatrix2D' +import type CPUFallbackLookupTable from './CPUFallbackLookupTable' +import type CPUFallbackLUT from './CPUFallbackLUT' +import type CPUFallbackRenderingTools from './CPUFallbackRenderingTools' export type { ICamera, @@ -43,6 +59,7 @@ export type { IStreamingImageVolume, IImage, IImageData, + CPUIImageData, ImageLoaderFn, VolumeLoaderFn, IRegisterImageLoader, @@ -61,7 +78,21 @@ export type { Orientation, Point2, Point3, + Point4, ViewportInputOptions, VOIRange, VOI, + FlipDirection, + // CPU fallback types + CPUFallbackEnabledElement, + CPUFallbackViewport, + CPUFallbackTransform, + CPUFallbackColormapData, + CPUFallbackViewportDisplayedArea, + CPUFallbackColormapsData, + CPUFallbackColormap, + TransformMatrix2D, + CPUFallbackLookupTable, + CPUFallbackLUT, + CPUFallbackRenderingTools, } diff --git a/packages/cornerstone-render/src/utilities/getRuntimeId.ts b/packages/cornerstone-render/src/utilities/getRuntimeId.ts index 71063c9fdf..783f78e0b4 100644 --- a/packages/cornerstone-render/src/utilities/getRuntimeId.ts +++ b/packages/cornerstone-render/src/utilities/getRuntimeId.ts @@ -18,6 +18,7 @@ export default function getRuntimeId( max?: number ): string { return getNextRuntimeId( + // @ts-ignore context !== null && typeof context === 'object' ? context : GLOBAL_CONTEXT, LAST_RUNTIME_ID, (typeof max === 'number' && max > 0 ? max : DEFAULT_MAX) >>> 0 diff --git a/packages/cornerstone-render/src/utilities/index.ts b/packages/cornerstone-render/src/utilities/index.ts index 146d964864..74de71128d 100644 --- a/packages/cornerstone-render/src/utilities/index.ts +++ b/packages/cornerstone-render/src/utilities/index.ts @@ -11,6 +11,7 @@ import isEqual from './isEqual' import testUtils from './testUtils' import createUint8SharedArray from './createUint8SharedArray' import createFloat32SharedArray from './createFloat32SharedArray' +import windowLevel from './windowLevel' export { invertRgbTransferFunction, @@ -26,4 +27,5 @@ export { createFloat32SharedArray, createUint8SharedArray, testUtils, + windowLevel, } diff --git a/packages/cornerstone-render/src/utilities/testUtils.js b/packages/cornerstone-render/src/utilities/testUtils.js index fd9dbf5d9b..5329fc0e4e 100644 --- a/packages/cornerstone-render/src/utilities/testUtils.js +++ b/packages/cornerstone-render/src/utilities/testUtils.js @@ -63,14 +63,14 @@ function makeTestRGB(rows, columns, barStart, barWidth) { * 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 from right and span 20 pixels + * 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] = + const [_, rows, columns, barStart, barWidth, x_spacing, y_spacing, rgb, PT] = imageURI.split('_').map((v) => parseFloat(v)) let pixelData @@ -81,12 +81,25 @@ const fakeImageLoader = (imageId) => { 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 { @@ -130,6 +143,8 @@ function fakeMetaDataProvider(type, imageId) { const imagePlaneModule = { rows, columns, + width: rows, + heigth: columns, imageOrientationPatient: [1, 0, 0, 0, 1, 0], rowCosines: [1, 0, 0], columnCosines: [0, 1, 0], @@ -230,7 +245,7 @@ const fakeVolumeLoader = (volumeId) => { direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], scalarData: pixelData, sizeInBytes: pixelData.byteLength, - vtkImageData: imageData, + imageData: imageData, imageIds: [], }) @@ -271,14 +286,14 @@ function compareImages(imageDataURL, baseline, outputName) { // If the error is greater than 1%, fail the test // and download the difference image if (mismatch > 1) { - console.debug(mismatch) + console.log(mismatch) const diff = data.getImageDataUrl() //downloadURI(diff, outputName) reject(new Error(`mismatch between images for ${outputName}`)) } else { - console.debug(`Images match for ${outputName}`) + console.log(`Images match for ${outputName}`) resolve() } }) @@ -307,7 +322,7 @@ function canvasPointsToPagePoints(DomCanvasElement, canvasPoint) { * @returns pageX, pageY, clientX, clientY, worldCoordinate */ function createNormalizedMouseEvent(imageData, index, canvas, viewport) { - const tempWorld1 = imageData.indexToWorldVec3(index) + const tempWorld1 = imageData.indexToWorld(index) const tempCanvasPoint1 = viewport.worldToCanvas(tempWorld1) const canvasPoint1 = tempCanvasPoint1.map((p) => Math.round(p)) const [pageX, pageY] = canvasPointsToPagePoints(canvas, canvasPoint1) diff --git a/packages/cornerstone-render/src/utilities/windowLevel.ts b/packages/cornerstone-render/src/utilities/windowLevel.ts new file mode 100644 index 0000000000..07df56af59 --- /dev/null +++ b/packages/cornerstone-render/src/utilities/windowLevel.ts @@ -0,0 +1,30 @@ +function toWindowLevel( + low: number, + high: number +): { + windowWidth: number; + windowCenter: number; +} { + const windowWidth = Math.abs(low - high); + const windowCenter = low + windowWidth / 2; + + return { windowWidth, windowCenter }; +} + +function toLowHighRange( + windowWidth: number, + windowCenter: number +): { + lower: number; + upper: number; +} { + const lower = windowCenter - windowWidth / 2.0; + const upper = windowCenter + windowWidth / 2.0; + + return { lower, upper }; +} + +export default { + toWindowLevel, + toLowHighRange, +}; diff --git a/packages/cornerstone-render/src/volumeLoader.ts b/packages/cornerstone-render/src/volumeLoader.ts index 67ffb06eae..6d032f0cc8 100644 --- a/packages/cornerstone-render/src/volumeLoader.ts +++ b/packages/cornerstone-render/src/volumeLoader.ts @@ -128,7 +128,7 @@ export function loadVolume( volumeLoadObject = loadVolumeFromVolumeLoader(volumeId, options) return volumeLoadObject.promise.then((volume: Types.IImageVolume) => { - volume.vtkImageData = createInternalVTKRepresentation(volume) + volume.imageData = createInternalVTKRepresentation(volume) return volume }) } @@ -161,8 +161,8 @@ export function createAndCacheVolume( volumeLoadObject = loadVolumeFromVolumeLoader(volumeId, options) - volumeLoadObject.promise.then((volume) => { - volume.vtkImageData = createInternalVTKRepresentation(volume) + volumeLoadObject.promise.then((volume: Types.IImageVolume) => { + volume.imageData = createInternalVTKRepresentation(volume) }) cache.putVolumeLoadObject(volumeId, volumeLoadObject).catch((err) => { diff --git a/packages/cornerstone-render/test/RenderingEngineAPI_test.js b/packages/cornerstone-render/test/RenderingEngineAPI_test.js index 511b2e9b38..3e031fc25b 100644 --- a/packages/cornerstone-render/test/RenderingEngineAPI_test.js +++ b/packages/cornerstone-render/test/RenderingEngineAPI_test.js @@ -23,384 +23,252 @@ const customOrientationViewportUID = 'OFF_AXIS_VIEWPORT' const DOMElements = [] -describe('RenderingEngine API:', function () { - beforeEach(function () { - this.renderingEngine = new RenderingEngine(renderingEngineUID) +describe('RenderingEngine', () => { + beforeAll(() => { + // initialize the library + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + describe('RenderingEngine API:', function () { + beforeEach(function () { + this.renderingEngine = new RenderingEngine(renderingEngineUID) - this.canvasAxial = document.createElement('canvas') + this.canvasAxial = document.createElement('canvas') - this.canvasAxial.width = 256 - this.canvasAxial.height = 512 + this.canvasAxial.width = 256 + this.canvasAxial.height = 512 - this.canvasSagittal = document.createElement('canvas') + this.canvasSagittal = document.createElement('canvas') - this.canvasSagittal.width = 1024 - this.canvasSagittal.height = 1024 + this.canvasSagittal.width = 1024 + this.canvasSagittal.height = 1024 - this.canvasCustomOrientation = document.createElement('canvas') + this.canvasCustomOrientation = document.createElement('canvas') - this.canvasCustomOrientation.width = 63 - this.canvasCustomOrientation.height = 87 + this.canvasCustomOrientation.width = 63 + this.canvasCustomOrientation.height = 87 - this.renderingEngine.setViewports([ - { - sceneUID: scene1UID, - viewportUID: axialViewportUID, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: this.canvasAxial, - defaultOptions: { - orientation: ORIENTATION.AXIAL, + this.renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: axialViewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: this.canvasAxial, + defaultOptions: { + orientation: ORIENTATION.AXIAL, + }, }, - }, - { - sceneUID: scene1UID, - viewportUID: sagittalViewportUID, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: this.canvasSagittal, - defaultOptions: { - orientation: ORIENTATION.SAGITTAL, + { + sceneUID: scene1UID, + viewportUID: sagittalViewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: this.canvasSagittal, + defaultOptions: { + orientation: ORIENTATION.SAGITTAL, + }, }, - }, - { - sceneUID: scene2UID, - viewportUID: customOrientationViewportUID, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: this.canvasCustomOrientation, - defaultOptions: { - orientation: { sliceNormal: [0, 0, 1], viewUp: [0, 1, 0] }, + { + sceneUID: scene2UID, + viewportUID: customOrientationViewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: this.canvasCustomOrientation, + defaultOptions: { + orientation: { sliceNormal: [0, 0, 1], viewUp: [0, 1, 0] }, + }, }, - }, - ]) - }) + ]) + }) - afterEach(function () { - this.renderingEngine.destroy() - cache.purgeCache() - }) + afterEach(function () { + this.renderingEngine.destroy() + cache.purgeCache() + }) - it('Add multiple scenes to the viewport and have api access to both', function () { - let scene1 = this.renderingEngine.getScene(scene1UID) + it('Add multiple scenes to the viewport and have api access to both', function () { + let scene1 = this.renderingEngine.getScene(scene1UID) - let scene2 = this.renderingEngine.getScene(scene2UID) + let scene2 = this.renderingEngine.getScene(scene2UID) - expect(scene1).toBeTruthy() - expect(scene2).toBeTruthy() + expect(scene1).toBeTruthy() + expect(scene2).toBeTruthy() - let scenes = this.renderingEngine.getScenes() + let scenes = this.renderingEngine.getScenes() - expect(scenes.length).toBe(2) + expect(scenes.length).toBe(2) - this.renderingEngine.removeScene(scene1UID) + this.renderingEngine.removeScene(scene1UID) - scene1 = this.renderingEngine.getScene(scene1UID) - scenes = this.renderingEngine.getScenes() + scene1 = this.renderingEngine.getScene(scene1UID) + scenes = this.renderingEngine.getScenes() - expect(scene1).not.toBeTruthy() - expect(scenes.length).toBe(1) - }) + expect(scene1).not.toBeTruthy() + expect(scenes.length).toBe(1) + }) - it('should be able to access the viewports for a scene', function () { - const scene1 = this.renderingEngine.getScene(scene1UID) + it('should be able to access the viewports for a scene', function () { + const scene1 = this.renderingEngine.getScene(scene1UID) - const scene1AxialViewport = scene1.getViewport(axialViewportUID) - const scene1Viewports = scene1.getViewports() + const scene1AxialViewport = scene1.getViewport(axialViewportUID) + const scene1Viewports = scene1.getViewports() - expect(scene1AxialViewport).toBeTruthy() - expect(scene1Viewports).toBeTruthy() - expect(scene1Viewports.length).toEqual(2) - }) + expect(scene1AxialViewport).toBeTruthy() + expect(scene1Viewports).toBeTruthy() + expect(scene1Viewports.length).toEqual(2) + }) - it('should be able to destroy the rendering engine', function () { - this.renderingEngine.destroy() + it('should be able to destroy the rendering engine', function () { + this.renderingEngine.destroy() - expect(function () { - this.renderingEngine.getScenes() - }).toThrow() - expect(function () { - this.renderingEngine.getViewports() - }).toThrow() - }) + expect(function () { + this.renderingEngine.getScenes() + }).toThrow() + expect(function () { + this.renderingEngine.getViewports() + }).toThrow() + }) + + it('should be able to handle destroy of an engine that has been destroyed', function () { + this.renderingEngine.destroy() + const response = this.renderingEngine.destroy() + expect(response).toBeUndefined() + }) - it('should be able to handle destroy of an engine that has been destroyed', function () { - this.renderingEngine.destroy() - const response = this.renderingEngine.destroy() - expect(response).toBeUndefined() + it('Take an orientation given by AXIAL as well as set manually by sliceNormal and viewUp', function () { + const scene1 = this.renderingEngine.getScene(scene1UID) + const scene2 = this.renderingEngine.getScene(scene2UID) + + const scene1AxialViewport = scene1.getViewport(axialViewportUID) + const scene2CustomOrientationViewport = scene2.getViewport( + customOrientationViewportUID + ) + + const scene1DefaultOptions = scene1AxialViewport.defaultOptions + const scene1Orientation = scene1DefaultOptions.orientation + const scene2DefaultOptions = + scene2CustomOrientationViewport.defaultOptions + const scene2Orientation = scene2DefaultOptions.orientation + + expect(scene1Orientation.viewUp.length).toEqual(3) + expect(scene1Orientation.sliceNormal.length).toEqual(3) + expect(scene2Orientation.viewUp.length).toEqual(3) + expect(scene2Orientation.sliceNormal.length).toEqual(3) + }) }) - it('Take an orientation given by AXIAL as well as set manually by sliceNormal and viewUp', function () { - const scene1 = this.renderingEngine.getScene(scene1UID) - const scene2 = this.renderingEngine.getScene(scene2UID) + describe('RenderingEngine Enable/Disable API:', function () { + beforeEach(function () { + this.renderingEngine = new RenderingEngine(renderingEngineUID) - const scene1AxialViewport = scene1.getViewport(axialViewportUID) - const scene2CustomOrientationViewport = scene2.getViewport( - customOrientationViewportUID - ) + this.canvasAxial = document.createElement('canvas') - const scene1DefaultOptions = scene1AxialViewport.defaultOptions - const scene1Orientation = scene1DefaultOptions.orientation - const scene2DefaultOptions = scene2CustomOrientationViewport.defaultOptions - const scene2Orientation = scene2DefaultOptions.orientation + this.canvasAxial.width = 256 + this.canvasAxial.height = 512 - expect(scene1Orientation.viewUp.length).toEqual(3) - expect(scene1Orientation.sliceNormal.length).toEqual(3) - expect(scene2Orientation.viewUp.length).toEqual(3) - expect(scene2Orientation.sliceNormal.length).toEqual(3) - }) -}) + this.canvasSagittal = document.createElement('canvas') -describe('RenderingEngine Enable/Disable API:', function () { - beforeEach(function () { - this.renderingEngine = new RenderingEngine(renderingEngineUID) + this.canvasSagittal.width = 1024 + this.canvasSagittal.height = 1024 - this.canvasAxial = document.createElement('canvas') + this.canvasCustomOrientation = document.createElement('canvas') - this.canvasAxial.width = 256 - this.canvasAxial.height = 512 + this.canvasCustomOrientation.width = 63 + this.canvasCustomOrientation.height = 87 + }) - this.canvasSagittal = document.createElement('canvas') + afterEach(function () { + this.renderingEngine.destroy() + }) - this.canvasSagittal.width = 1024 - this.canvasSagittal.height = 1024 + it('should be able to successfully use enable api', function () { + const viewportInputEntries = [ + { + sceneUID: scene1UID, + viewportUID: axialViewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: this.canvasAxial, + defaultOptions: { + orientation: ORIENTATION.AXIAL, + }, + }, + { + sceneUID: scene1UID, + viewportUID: sagittalViewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: this.canvasSagittal, + defaultOptions: { + orientation: ORIENTATION.SAGITTAL, + }, + }, + { + sceneUID: scene2UID, + viewportUID: customOrientationViewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: this.canvasCustomOrientation, + defaultOptions: { + orientation: { sliceNormal: [0, 0, 1], viewUp: [0, 1, 0] }, + }, + }, + ] - this.canvasCustomOrientation = document.createElement('canvas') + this.renderingEngine.enableElement(viewportInputEntries[0]) - this.canvasCustomOrientation.width = 63 - this.canvasCustomOrientation.height = 87 - }) + let scene1 = this.renderingEngine.getScene(scene1UID) + let scene2 = this.renderingEngine.getScene(scene2UID) - afterEach(function () { - this.renderingEngine.destroy() - }) + expect(scene1).toBeTruthy() + expect(scene1.uid).toBe(scene1UID) + expect(scene2).toBeUndefined() + }) - it('should be able to successfully use enable api', function () { - const viewportInputEntries = [ - { + it('should not enable element without canvas', function () { + const entry = { sceneUID: scene1UID, viewportUID: axialViewportUID, type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: this.canvasAxial, defaultOptions: { orientation: ORIENTATION.AXIAL, }, - }, - { + } + + const enable = function () { + this.renderingEngine.enableElement(entry) + } + expect(enable).toThrow() + }) + + it('should successfully use disable element API', function () { + const entry = { sceneUID: scene1UID, - viewportUID: sagittalViewportUID, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: this.canvasSagittal, - defaultOptions: { - orientation: ORIENTATION.SAGITTAL, - }, - }, - { - sceneUID: scene2UID, - viewportUID: customOrientationViewportUID, + viewportUID: axialViewportUID, type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: this.canvasCustomOrientation, + canvas: this.canvasAxial, defaultOptions: { - orientation: { sliceNormal: [0, 0, 1], viewUp: [0, 1, 0] }, + orientation: ORIENTATION.AXIAL, }, - }, - ] - - this.renderingEngine.enableElement(viewportInputEntries[0]) - - let scene1 = this.renderingEngine.getScene(scene1UID) - let scene2 = this.renderingEngine.getScene(scene2UID) - - expect(scene1).toBeTruthy() - expect(scene1.uid).toBe(scene1UID) - expect(scene2).toBeUndefined() - }) + } - it('should not enable element without canvas', function () { - const entry = { - sceneUID: scene1UID, - viewportUID: axialViewportUID, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - defaultOptions: { - orientation: ORIENTATION.AXIAL, - }, - } - - const enable = function () { this.renderingEngine.enableElement(entry) - } - expect(enable).toThrow() - }) - - it('should successfully use disable element API', function () { - const entry = { - sceneUID: scene1UID, - viewportUID: axialViewportUID, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: this.canvasAxial, - defaultOptions: { - orientation: ORIENTATION.AXIAL, - }, - } - - this.renderingEngine.enableElement(entry) - let viewport1 = this.renderingEngine.getViewport(axialViewportUID) - expect(viewport1).toBeTruthy() - - this.renderingEngine.disableElement(axialViewportUID) - viewport1 = this.renderingEngine.getViewport(axialViewportUID) - expect(viewport1).toBeUndefined() - }) - - it('should successfully get StackViewports', function () { - const entry = { - sceneUID: undefined, - viewportUID: axialViewportUID, - type: VIEWPORT_TYPE.STACK, - canvas: this.canvasAxial, - defaultOptions: { - orientation: ORIENTATION.AXIAL, - }, - } - - this.renderingEngine.enableElement(entry) - const stackViewports = this.renderingEngine.getStackViewports() - expect(stackViewports.length).toBe(1) - }) -}) - -describe('Scene API:', function () { - beforeEach(function () { - this.renderingEngine = new RenderingEngine(renderingEngineUID) - - this.canvasAxial = document.createElement('canvas') - - this.canvasAxial.style.width = '256px' - this.canvasAxial.style.height = '512px' - - document.body.appendChild(this.canvasAxial) - DOMElements.push(this.canvasAxial) + let viewport1 = this.renderingEngine.getViewport(axialViewportUID) + expect(viewport1).toBeTruthy() - this.canvasSagittal = document.createElement('canvas') - - this.canvasSagittal.style.width = '256px' - this.canvasSagittal.style.height = '512px' - - document.body.appendChild(this.canvasSagittal) - DOMElements.push(this.canvasSagittal) - - this.canvasCustomOrientation = document.createElement('canvas') - - this.canvasCustomOrientation.style.width = '63px' - this.canvasCustomOrientation.style.height = '87px' - - document.body.appendChild(this.canvasCustomOrientation) - DOMElements.push(this.canvasCustomOrientation) + this.renderingEngine.disableElement(axialViewportUID) + viewport1 = this.renderingEngine.getViewport(axialViewportUID) + expect(viewport1).toBeUndefined() + }) - this.renderingEngine.setViewports([ - { - sceneUID: scene1UID, + it('should successfully get StackViewports', function () { + const entry = { + sceneUID: undefined, viewportUID: axialViewportUID, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, + type: VIEWPORT_TYPE.STACK, canvas: this.canvasAxial, defaultOptions: { orientation: ORIENTATION.AXIAL, - background: [1, 0, 1], - }, - }, - { - sceneUID: scene1UID, - viewportUID: sagittalViewportUID, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: this.canvasSagittal, - defaultOptions: { - orientation: ORIENTATION.SAGITTAL, - background: [1, 1, 0], - }, - }, - { - sceneUID: scene2UID, - viewportUID: customOrientationViewportUID, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: this.canvasCustomOrientation, - defaultOptions: { - orientation: { sliceNormal: [0, 0, 1], viewUp: [0, 1, 0] }, }, - }, - ]) - }) - - afterEach(function () { - this.renderingEngine.destroy() - cache.purgeCache() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) } + + this.renderingEngine.enableElement(entry) + const stackViewports = this.renderingEngine.getStackViewports() + expect(stackViewports.length).toBe(1) }) }) - - // it('should be able to setVolumes with more than one volume', function (done) { - // const volumeId1 = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - // const volumeId2 = 'fakeVolumeLoader:volumeURI_100_100_8_1_1_1_0' - - // let canvasRender = 0 - - // const eventHandler = () => { - // canvasRender += 1 - // if (canvasRender !== 2) { - // return - // } - // const ctScene = this.renderingEngine.getScene(scene1UID) - // const axialActor = ctScene.getVolumeActor(volumeId1) - // expect(axialActor).toBeTruthy() - - // done() - // } - - // this.canvasAxial.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) - // this.canvasSagittal.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) - - // try { - // createAndCacheVolume(volumeId1, { imageIds: [] }).then(() => { - // createAndCacheVolume(volumeId2, { imageIds: [] }).then(() => { - // const ctScene = this.renderingEngine.getScene(scene1UID) - // ctScene - // .setVolumes([{ volumeUID: volumeId1 }, { volumeUID: volumeId2 }]) - // .then(() => { - // ctScene.render() - // }) - // }) - // }) - // } catch (e) { - // done.fail(e) - // } - // }) - - // it('should be able to remove viewport from scene', function (done) { - // const volumeId1 = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - // const eventHandler = () => { - // const ctScene = this.renderingEngine.getScene(scene1UID) - // let viewports = ctScene.getViewports() - // expect(viewports.length).toBe(2) // 2 viewports for CT scene - - // ctScene.removeViewportByUID(axialViewportUID) - - // viewports = ctScene.getViewports() - // expect(viewports.length).toBe(1) // removed one viewport - // expect(viewports[0]).toBe(ctScene.getViewport(sagittalViewportUID)) - // done() - // } - - // this.canvasAxial.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) - - // try { - // createAndCacheVolume(volumeId1, { imageIds: [] }).then(() => { - // const ctScene = this.renderingEngine.getScene(scene1UID) - // ctScene.setVolumes([{ volumeUID: volumeId1 }]).then(() => { - // ctScene.render() - // }) - // }) - // } catch (e) { - // done.fail(e) - // } - // }) }) diff --git a/packages/cornerstone-render/test/cache_test.js b/packages/cornerstone-render/test/cache_test.js index f06f2892c0..f228190c0e 100644 --- a/packages/cornerstone-render/test/cache_test.js +++ b/packages/cornerstone-render/test/cache_test.js @@ -6,654 +6,661 @@ import { createFloat32SharedArray } from '../src/utilities' const { cache, Utilities, ERROR_CODES } = cornerstone const { StreamingImageVolume } = cornerstoneStreamingImageVolumeLoader -describe('Set maximum cache size', function () { - afterEach(function () { - cache.purgeCache() +describe('Cache', () => { + beforeAll(() => { + // initialize the library + cornerstone.init() }) - it('should start by allocating 1GB of cache size', function () { - // Arrange - const maximumSizeInBytes = 1073741824 // 1GB + describe('Set maximum cache size', function () { + afterEach(function () { + cache.purgeCache() + }) - expect(cache.getMaxCacheSize()).toBe(maximumSizeInBytes) - }) - - it('should fail if numBytes is not defined', function () { - expect(cache.setMaxCacheSize.bind(cache, undefined)).toThrow() - }) - - it('should fail if numBytes is not a number', function () { - expect(cache.setMaxCacheSize.bind(cache, '10000')).toThrow() - }) -}) + it('should start by allocating 1GB of cache size', function () { + // Arrange + const maximumSizeInBytes = 1073741824 // 1GB -describe('Image Cache: Store, retrieve, and remove imagePromises from the cache', function () { - beforeEach(function () { - // Arrange - this.image = { - imageId: 'anImageId', - sizeInBytes: 100, - } - - this.imageLoadObject = { - promise: Promise.resolve(this.image), - cancelFn: undefined, - } - }) - - afterEach(function () { - cache.purgeCache() - }) + expect(cache.getMaxCacheSize()).toBe(maximumSizeInBytes) + }) - it('should allow image promises to be added to the cache (putImageLoadObject)', async function () { - const image = this.image - const imageLoadObject = this.imageLoadObject + it('should fail if numBytes is not defined', function () { + expect(cache.setMaxCacheSize.bind(cache, undefined)).toThrow() + }) - cache.putImageLoadObject(image.imageId, imageLoadObject) - await imageLoadObject.promise - const cacheSize = cache.getCacheSize() - - expect(cacheSize).toBe(image.sizeInBytes) - - const imageLoad = cache.getImageLoadObject(image.imageId) - expect(imageLoad).toBeDefined() + it('should fail if numBytes is not a number', function () { + expect(cache.setMaxCacheSize.bind(cache, '10000')).toThrow() + }) }) - it('should throw an error if imageId is not defined (putImageLoadObject)', function () { - expect(function () { - cache.putImageLoadObject(undefined, this.imageLoadObject) - }).toThrow() - }) + describe('Image Cache: Store, retrieve, and remove imagePromises from the cache', function () { + beforeEach(function () { + // Arrange + this.image = { + imageId: 'anImageId', + sizeInBytes: 100, + } - it('should throw an error if imagePromise is not defined (putImageLoadObject)', function () { - expect(function () { - cache.putImageLoadObject(this.image.imageId, undefined) - }).toThrow() - }) + this.imageLoadObject = { + promise: Promise.resolve(this.image), + cancelFn: undefined, + } + }) - it('should throw an error if imageId is already in the cache (putImageLoadObject)', async function () { - const image = this.image - const imageLoadObject = this.imageLoadObject + afterEach(function () { + cache.purgeCache() + }) - cache.putImageLoadObject(image.imageId, imageLoadObject) - await imageLoadObject.promise + it('should allow image promises to be added to the cache (putImageLoadObject)', async function () { + const image = this.image + const imageLoadObject = this.imageLoadObject - expect(function () { cache.putImageLoadObject(image.imageId, imageLoadObject) - }).toThrow() - }) - - it('should allow image promises to be retrieved from the cache (getImageLoadObject()', async function () { - const image = this.image - const imageLoadObject = this.imageLoadObject + await imageLoadObject.promise + const cacheSize = cache.getCacheSize() - cache.putImageLoadObject(image.imageId, imageLoadObject) - await imageLoadObject.promise + expect(cacheSize).toBe(image.sizeInBytes) - const retrievedImageLoadObject = cache.getImageLoadObject(image.imageId) + const imageLoad = cache.getImageLoadObject(image.imageId) + expect(imageLoad).toBeDefined() + }) - expect(retrievedImageLoadObject).toBe(imageLoadObject) - }) + it('should throw an error if imageId is not defined (putImageLoadObject)', function () { + expect(function () { + cache.putImageLoadObject(undefined, this.imageLoadObject) + }).toThrow() + }) - it('should throw an error if imageId is not defined (getImageLoadObject()', function () { - expect(function () { - cache.getImageLoadObject(undefined) - }).toThrow() - }) + it('should throw an error if imagePromise is not defined (putImageLoadObject)', function () { + expect(function () { + cache.putImageLoadObject(this.image.imageId, undefined) + }).toThrow() + }) - it('should fail silently to retrieve a promise for an imageId not in the cache', function () { - const retrievedImageLoadObject = cache.getImageLoadObject( - 'AnImageIdNotInCache' - ) + it('should throw an error if imageId is already in the cache (putImageLoadObject)', async function () { + const image = this.image + const imageLoadObject = this.imageLoadObject - expect(retrievedImageLoadObject).toBeUndefined() - }) + cache.putImageLoadObject(image.imageId, imageLoadObject) + await imageLoadObject.promise - it('should allow cachedObject to be removed (removeImageLoadObject)', async function () { - const image = this.image - const imageLoadObject = this.imageLoadObject + expect(function () { + cache.putImageLoadObject(image.imageId, imageLoadObject) + }).toThrow() + }) - cache.putImageLoadObject(image.imageId, imageLoadObject) - await imageLoadObject.promise + it('should allow image promises to be retrieved from the cache (getImageLoadObject()', async function () { + const image = this.image + const imageLoadObject = this.imageLoadObject - expect(cache.getCacheSize()).not.toBe(0) - cache.removeImageLoadObject(image.imageId) + cache.putImageLoadObject(image.imageId, imageLoadObject) + await imageLoadObject.promise - expect(cache.getCacheSize()).toBe(0) + const retrievedImageLoadObject = cache.getImageLoadObject(image.imageId) - expect(cache.getImageLoadObject(this.image.imageId)).toBeUndefined() - }) + expect(retrievedImageLoadObject).toBe(imageLoadObject) + }) - it('should fail if imageId is not defined (removeImageLoadObject)', function () { - expect(function () { - cache.removeImageLoadObject(undefined) - }).toThrow() - }) + it('should throw an error if imageId is not defined (getImageLoadObject()', function () { + expect(function () { + cache.getImageLoadObject(undefined) + }).toThrow() + }) - it('should fail if imageId is not in cache (removeImageLoadObject)', function () { - expect(function () { - cache.removeImageLoadObject('RandomImageId') - }).toThrow() - }) + it('should fail silently to retrieve a promise for an imageId not in the cache', function () { + const retrievedImageLoadObject = cache.getImageLoadObject( + 'AnImageIdNotInCache' + ) - it('should fail if resolved image does not have sizeInBytes (putImageLoadObject)', async function () { - const image1 = { - imageId: 'anImageId1', - sizeInBytes: undefined, - } + expect(retrievedImageLoadObject).toBeUndefined() + }) - const imageLoadObject1 = { - promise: Promise.resolve(image1), - cancelFn: undefined, - } + it('should allow cachedObject to be removed (removeImageLoadObject)', async function () { + const image = this.image + const imageLoadObject = this.imageLoadObject - await expectAsync( - cache.putImageLoadObject(image1.imageId, imageLoadObject1) - ).toBeRejected() + cache.putImageLoadObject(image.imageId, imageLoadObject) + await imageLoadObject.promise - expect(cache.getImageLoadObject(image1.imageId)).not.toBeDefined() + expect(cache.getCacheSize()).not.toBe(0) + cache.removeImageLoadObject(image.imageId) - const cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(0) - }) + expect(cache.getCacheSize()).toBe(0) - it("should fail if resolved image's sizeInBytes is not a number(putImageLoadObject)", async function () { - const image1 = { - imageId: 'anImageId1', - sizeInBytes: '123', - } + expect(cache.getImageLoadObject(this.image.imageId)).toBeUndefined() + }) - const imageLoadObject1 = { - promise: Promise.resolve(image1), - cancelFn: undefined, - } + it('should fail if imageId is not defined (removeImageLoadObject)', function () { + expect(function () { + cache.removeImageLoadObject(undefined) + }).toThrow() + }) - await expectAsync( - cache.putImageLoadObject(image1.imageId, imageLoadObject1) - ).toBeRejected() + it('should fail if imageId is not in cache (removeImageLoadObject)', function () { + expect(function () { + cache.removeImageLoadObject('RandomImageId') + }).toThrow() + }) - expect(cache.getImageLoadObject(image1.imageId)).not.toBeDefined() + it('should fail if resolved image does not have sizeInBytes (putImageLoadObject)', async function () { + const image1 = { + imageId: 'anImageId1', + sizeInBytes: undefined, + } - const cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(0) - }) + const imageLoadObject1 = { + promise: Promise.resolve(image1), + cancelFn: undefined, + } - it('should not cache the imageId if the imageId has been decached before loading(putImageLoadObject)', async function () { - const image1 = { - imageId: 'anImageId1', - sizeInBytes: 1234, - } + await expectAsync( + cache.putImageLoadObject(image1.imageId, imageLoadObject1) + ).toBeRejected() - const imageLoadObject1 = { - promise: Promise.resolve(image1), - cancelFn: undefined, - } + expect(cache.getImageLoadObject(image1.imageId)).not.toBeDefined() - const promise = cache.putImageLoadObject(image1.imageId, imageLoadObject1) + const cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(0) + }) - cache.removeImageLoadObject(image1.imageId) + it("should fail if resolved image's sizeInBytes is not a number(putImageLoadObject)", async function () { + const image1 = { + imageId: 'anImageId1', + sizeInBytes: '123', + } - await promise + const imageLoadObject1 = { + promise: Promise.resolve(image1), + cancelFn: undefined, + } - expect(cache.getImageLoadObject(image1.imageId)).not.toBeDefined() + await expectAsync( + cache.putImageLoadObject(image1.imageId, imageLoadObject1) + ).toBeRejected() - const cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(0) - }) + expect(cache.getImageLoadObject(image1.imageId)).not.toBeDefined() - it('should be able to purge the entire cache', async function () { - const image = this.image - const imageLoadObject = this.imageLoadObject + const cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(0) + }) - cache.putImageLoadObject(image.imageId, imageLoadObject) - await imageLoadObject.promise + it('should not cache the imageId if the imageId has been decached before loading(putImageLoadObject)', async function () { + const image1 = { + imageId: 'anImageId1', + sizeInBytes: 1234, + } - cache.purgeCache() + const imageLoadObject1 = { + promise: Promise.resolve(image1), + cancelFn: undefined, + } - expect(cache.getCacheSize()).toBe(0) - }) + const promise = cache.putImageLoadObject(image1.imageId, imageLoadObject1) - it('should successfully caching an image when there is enough volatile + unallocated space', async function () { - const maxCacheSize = cache.getMaxCacheSize() + cache.removeImageLoadObject(image1.imageId) - const image1SizeInBytes = maxCacheSize - 10000 - const image2SizeInBytes = 9000 + await promise - // Act - const image1 = { - imageId: 'anImageId1', - sizeInBytes: image1SizeInBytes, - } + expect(cache.getImageLoadObject(image1.imageId)).not.toBeDefined() - const imageLoadObject1 = { - promise: Promise.resolve(image1), - cancelFn: undefined, - } + const cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(0) + }) - const image2 = { - imageId: 'anImageId2', - sizeInBytes: image2SizeInBytes, - } + it('should be able to purge the entire cache', async function () { + const image = this.image + const imageLoadObject = this.imageLoadObject - const imageLoadObject2 = { - promise: Promise.resolve(image2), - cancelFn: undefined, - } + cache.putImageLoadObject(image.imageId, imageLoadObject) + await imageLoadObject.promise - cache.putImageLoadObject(image1.imageId, imageLoadObject1) - await imageLoadObject1.promise + cache.purgeCache() - let cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(image1.sizeInBytes) + expect(cache.getCacheSize()).toBe(0) + }) - cache.putImageLoadObject(image2.imageId, imageLoadObject2) - await imageLoadObject2.promise + it('should successfully caching an image when there is enough volatile + unallocated space', async function () { + const maxCacheSize = cache.getMaxCacheSize() - cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(image1.sizeInBytes + image2.sizeInBytes) - }) + const image1SizeInBytes = maxCacheSize - 10000 + const image2SizeInBytes = 9000 - it('should unsuccessfully caching an image when there is not enough volatile + unallocated space', async function () { - const maxCacheSize = cache.getMaxCacheSize() - - const volumeSizeInBytes = maxCacheSize - 10000 - const image1SizeInBytes = 11000 - - const volumeId = 'aVolumeId' - - const dimensions = [10, 10, 10] - const scalarData = createFloat32SharedArray( - dimensions[0] * dimensions[1] * dimensions[2] - ) - - // Arrange - const volume = new StreamingImageVolume( - // ImageVolume properties - { - uid: volumeId, - spacing: [1, 1, 1], - origin: [0, 0, 0], - direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], - dimensions, - sizeInBytes: volumeSizeInBytes, - scalarData, - metadata: { - voiLut: [ - { windowCenter: 500, windowWidth: 500 }, - { windowCenter: 1500, windowWidth: 1500 }, - ], - PhotometricInterpretation: 'MONOCHROME2', - }, - }, - // Streaming properties - { - imageIds: ['imageid1', 'imageid2'], - loadStatus: { - loaded: false, - loading: false, - cachedFrames: [], - callbacks: [], - }, + // Act + const image1 = { + imageId: 'anImageId1', + sizeInBytes: image1SizeInBytes, } - ) - const volumeLoadObject = { - promise: Promise.resolve(volume), - cancelFn: undefined, - } - - const image1 = { - imageId: 'anImageId1', - sizeInBytes: image1SizeInBytes, - } - - const imageLoadObject1 = { - promise: Promise.resolve(image1), - cancelFn: undefined, - } + const imageLoadObject1 = { + promise: Promise.resolve(image1), + cancelFn: undefined, + } - cache.putVolumeLoadObject(volume.uid, volumeLoadObject) - await volumeLoadObject.promise + const image2 = { + imageId: 'anImageId2', + sizeInBytes: image2SizeInBytes, + } - let cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(volume.sizeInBytes) + const imageLoadObject2 = { + promise: Promise.resolve(image2), + cancelFn: undefined, + } - await expectAsync( cache.putImageLoadObject(image1.imageId, imageLoadObject1) - ).toBeRejectedWithError(ERROR_CODES.CACHE_SIZE_EXCEEDED) + await imageLoadObject1.promise + + let cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(image1.sizeInBytes) + + cache.putImageLoadObject(image2.imageId, imageLoadObject2) + await imageLoadObject2.promise + + cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(image1.sizeInBytes + image2.sizeInBytes) + }) + + it('should unsuccessfully caching an image when there is not enough volatile + unallocated space', async function () { + const maxCacheSize = cache.getMaxCacheSize() + + const volumeSizeInBytes = maxCacheSize - 10000 + const image1SizeInBytes = 11000 + + const volumeId = 'aVolumeId' + + const dimensions = [10, 10, 10] + const scalarData = createFloat32SharedArray( + dimensions[0] * dimensions[1] * dimensions[2] + ) + + // Arrange + const volume = new StreamingImageVolume( + // ImageVolume properties + { + uid: volumeId, + spacing: [1, 1, 1], + origin: [0, 0, 0], + direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], + dimensions, + sizeInBytes: volumeSizeInBytes, + scalarData, + metadata: { + voiLut: [ + { windowCenter: 500, windowWidth: 500 }, + { windowCenter: 1500, windowWidth: 1500 }, + ], + PhotometricInterpretation: 'MONOCHROME2', + }, + }, + // Streaming properties + { + imageIds: ['imageid1', 'imageid2'], + loadStatus: { + loaded: false, + loading: false, + cachedFrames: [], + callbacks: [], + }, + } + ) + + const volumeLoadObject = { + promise: Promise.resolve(volume), + cancelFn: undefined, + } - expect(cache.getImageLoadObject(image1.imageId)).not.toBeDefined() + const image1 = { + imageId: 'anImageId1', + sizeInBytes: image1SizeInBytes, + } - cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(volume.sizeInBytes) - }) -}) + const imageLoadObject1 = { + promise: Promise.resolve(image1), + cancelFn: undefined, + } -describe('Volume Cache: ', function () { - beforeEach(function () { - const imageIds = [ - 'fakeImageLoader:imageId1', - 'fakeImageLoader:imageId2', - 'fakeImageLoader:imageId3', - 'fakeImageLoader:imageId4', - 'fakeImageLoader:imageId5', - ] - - const volumeId = 'aVolumeId' - - const dimensions = [10, 10, 10] - const scalarData = createFloat32SharedArray( - dimensions[0] * dimensions[1] * dimensions[2] - ) - // Arrange - this.volume = new StreamingImageVolume( - // ImageVolume properties - { - uid: volumeId, - spacing: [1, 1, 1], - origin: [0, 0, 0], - direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], - dimensions, - sizeInBytes: 1000000, - scalarData, - metadata: { - voiLut: [ - { windowCenter: 500, windowWidth: 500 }, - { windowCenter: 1500, windowWidth: 1500 }, - ], - PhotometricInterpretation: 'MONOCHROME2', - }, - }, - // Streaming properties - { - imageIds, - loadStatus: { - loaded: false, - loading: false, - cachedFrames: [], - callbacks: [], + cache.putVolumeLoadObject(volume.uid, volumeLoadObject) + await volumeLoadObject.promise + + let cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(volume.sizeInBytes) + + await expectAsync( + cache.putImageLoadObject(image1.imageId, imageLoadObject1) + ).toBeRejectedWithError(ERROR_CODES.CACHE_SIZE_EXCEEDED) + + expect(cache.getImageLoadObject(image1.imageId)).not.toBeDefined() + + cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(volume.sizeInBytes) + }) + }) + + describe('Volume Cache: ', function () { + beforeEach(function () { + const imageIds = [ + 'fakeImageLoader:imageId1', + 'fakeImageLoader:imageId2', + 'fakeImageLoader:imageId3', + 'fakeImageLoader:imageId4', + 'fakeImageLoader:imageId5', + ] + + const volumeId = 'aVolumeId' + + const dimensions = [10, 10, 10] + const scalarData = createFloat32SharedArray( + dimensions[0] * dimensions[1] * dimensions[2] + ) + // Arrange + this.volume = new StreamingImageVolume( + // ImageVolume properties + { + uid: volumeId, + spacing: [1, 1, 1], + origin: [0, 0, 0], + direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], + dimensions, + sizeInBytes: 1000000, + scalarData, + metadata: { + voiLut: [ + { windowCenter: 500, windowWidth: 500 }, + { windowCenter: 1500, windowWidth: 1500 }, + ], + PhotometricInterpretation: 'MONOCHROME2', + }, }, + // Streaming properties + { + imageIds, + loadStatus: { + loaded: false, + loading: false, + cachedFrames: [], + callbacks: [], + }, + } + ) + + this.volumeLoadObject = { + promise: Promise.resolve(this.volume), + cancelFn: undefined, } - ) - - this.volumeLoadObject = { - promise: Promise.resolve(this.volume), - cancelFn: undefined, - } - }) + }) - afterEach(function () { - cache.purgeCache() - }) - - it('should allow volume promises to be added to the cache (putVolumeLoadObject)', async function () { - const volume = this.volume - const volumeLoadObject = this.volumeLoadObject + afterEach(function () { + cache.purgeCache() + }) - cache.putVolumeLoadObject(volume.uid, volumeLoadObject) - await volumeLoadObject.promise + it('should allow volume promises to be added to the cache (putVolumeLoadObject)', async function () { + const volume = this.volume + const volumeLoadObject = this.volumeLoadObject - const cacheSize = cache.getCacheSize() + cache.putVolumeLoadObject(volume.uid, volumeLoadObject) + await volumeLoadObject.promise - expect(cacheSize).toBe(volume.sizeInBytes) + const cacheSize = cache.getCacheSize() - const volumeLoad = cache.getVolumeLoadObject(volume.uid) - expect(volumeLoad).toBeDefined() - }) + expect(cacheSize).toBe(volume.sizeInBytes) - it('should throw an error if volumeId is not defined (putVolumeLoadObject)', function () { - expect(function () { - cache.putVolumeLoadObject(undefined, this.volumeLoadObject) - }).toThrow() - }) + const volumeLoad = cache.getVolumeLoadObject(volume.uid) + expect(volumeLoad).toBeDefined() + }) - it('should throw an error if volumeLoadObject is not defined (putVolumeLoadObject)', function () { - // Assert - expect(function () { - cache.putVolumeLoadObject.bind(cache, this.volume.uid, undefined) - }).toThrow() - }) + it('should throw an error if volumeId is not defined (putVolumeLoadObject)', function () { + expect(function () { + cache.putVolumeLoadObject(undefined, this.volumeLoadObject) + }).toThrow() + }) - it('should throw an error if volumeId is already in the cache (putVolumeLoadObject)', async function () { - // Arrange - cache.putImageLoadObject(this.volume.uid, this.volumeLoadObject) - await this.volumeLoadObject.promise + it('should throw an error if volumeLoadObject is not defined (putVolumeLoadObject)', function () { + // Assert + expect(function () { + cache.putVolumeLoadObject.bind(cache, this.volume.uid, undefined) + }).toThrow() + }) - // Assert - expect(function () { + it('should throw an error if volumeId is already in the cache (putVolumeLoadObject)', async function () { + // Arrange cache.putImageLoadObject(this.volume.uid, this.volumeLoadObject) - }).toThrow() - }) - - it('should allow volume promises to be retrieved from the cache (getVolumeLoadObject()', async function () { - const volume = this.volume - const volumeLoadObject = this.volumeLoadObject - - // Act - cache.putVolumeLoadObject(volume.uid, volumeLoadObject) - await volumeLoadObject.promise + await this.volumeLoadObject.promise - // Assert - const retrievedVolumeLoadObject = cache.getVolumeLoadObject(volume.uid) + // Assert + expect(function () { + cache.putImageLoadObject(this.volume.uid, this.volumeLoadObject) + }).toThrow() + }) - expect(retrievedVolumeLoadObject).toBe(volumeLoadObject) - }) - - it('should throw an error if volumeId is not defined (getVolumeLoadObject()', function () { - // Assert - expect(function () { - cache.getVolumeLoadObject(undefined) - }).toThrow() - }) - - it('should fail silently to retrieve a promise for an volumeId not in the cache', function () { - // Act - const retrievedVolumeLoadObject = cache.getVolumeLoadObject( - 'AVolumeIdNotInCache' - ) - - // Assert - expect(retrievedVolumeLoadObject).toBeUndefined() - }) + it('should allow volume promises to be retrieved from the cache (getVolumeLoadObject()', async function () { + const volume = this.volume + const volumeLoadObject = this.volumeLoadObject - it('should allow cachedObject to be removed for volume (removeVolumeLoadObject)', async function () { - const volume = this.volume - const volumeLoadObject = this.volumeLoadObject - - // Arrange - cache.putVolumeLoadObject(volume.uid, volumeLoadObject) - await volumeLoadObject.promise - - expect(cache.getCacheSize()).not.toBe(0) - // Act - cache.removeVolumeLoadObject(volume.uid) - - // Assert - expect(cache.getCacheSize()).toBe(0) - - expect(cache.getVolumeLoadObject(this.volume.uid)).toBeUndefined() - }) + // Act + cache.putVolumeLoadObject(volume.uid, volumeLoadObject) + await volumeLoadObject.promise - it('should fail if volumeId is not defined (removeVolumeLoadObject)', function () { - expect(function () { - cache.removeVolumeLoadObject(undefined) - }).toThrow() - }) + // Assert + const retrievedVolumeLoadObject = cache.getVolumeLoadObject(volume.uid) - it('should fail if imageId is not in cache (removeImagePromise)', function () { - expect(function () { - cache.removeVolumeLoadObject('RandomImageId') - }).toThrow() - }) + expect(retrievedVolumeLoadObject).toBe(volumeLoadObject) + }) - it('should be able to purge the entire cache', async function () { - const volume = this.volume - const volumeLoadObject = this.volumeLoadObject + it('should throw an error if volumeId is not defined (getVolumeLoadObject()', function () { + // Assert + expect(function () { + cache.getVolumeLoadObject(undefined) + }).toThrow() + }) - // Arrange - await cache.putVolumeLoadObject(volume.uid, volumeLoadObject) + it('should fail silently to retrieve a promise for an volumeId not in the cache', function () { + // Act + const retrievedVolumeLoadObject = cache.getVolumeLoadObject( + 'AVolumeIdNotInCache' + ) - cache.purgeCache() + // Assert + expect(retrievedVolumeLoadObject).toBeUndefined() + }) - expect(cache.getCacheSize()).toBe(0) - }) + it('should allow cachedObject to be removed for volume (removeVolumeLoadObject)', async function () { + const volume = this.volume + const volumeLoadObject = this.volumeLoadObject - it('should successfully caching a volume when there is enough volatile + unallocated space', async function () { - const maxCacheSize = cache.getMaxCacheSize() - - const image1SizeInBytes = maxCacheSize - 1 - const volumeSizeInBytes = maxCacheSize - - const volumeId = 'aVolumeId' - - const dimensions = [10, 10, 10] - const scalarData = createFloat32SharedArray( - dimensions[0] * dimensions[1] * dimensions[2] - ) - - // Arrange - const volume = new StreamingImageVolume( - // ImageVolume properties - { - uid: volumeId, - spacing: [1, 1, 1], - origin: [0, 0, 0], - direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], - dimensions, - sizeInBytes: volumeSizeInBytes, - scalarData, - metadata: { - PhotometricInterpretation: 'MONOCHROME2', - }, - }, - // Streaming properties - { - imageIds: ['imageid1', 'imageid2'], - loadStatus: { - loaded: false, - loading: false, - cachedFrames: [], - callbacks: [], + // Arrange + cache.putVolumeLoadObject(volume.uid, volumeLoadObject) + await volumeLoadObject.promise + + expect(cache.getCacheSize()).not.toBe(0) + // Act + cache.removeVolumeLoadObject(volume.uid) + + // Assert + expect(cache.getCacheSize()).toBe(0) + + expect(cache.getVolumeLoadObject(this.volume.uid)).toBeUndefined() + }) + + it('should fail if volumeId is not defined (removeVolumeLoadObject)', function () { + expect(function () { + cache.removeVolumeLoadObject(undefined) + }).toThrow() + }) + + it('should fail if imageId is not in cache (removeImagePromise)', function () { + expect(function () { + cache.removeVolumeLoadObject('RandomImageId') + }).toThrow() + }) + + it('should be able to purge the entire cache', async function () { + const volume = this.volume + const volumeLoadObject = this.volumeLoadObject + + // Arrange + await cache.putVolumeLoadObject(volume.uid, volumeLoadObject) + + cache.purgeCache() + + expect(cache.getCacheSize()).toBe(0) + }) + + it('should successfully caching a volume when there is enough volatile + unallocated space', async function () { + const maxCacheSize = cache.getMaxCacheSize() + + const image1SizeInBytes = maxCacheSize - 1 + const volumeSizeInBytes = maxCacheSize + + const volumeId = 'aVolumeId' + + const dimensions = [10, 10, 10] + const scalarData = createFloat32SharedArray( + dimensions[0] * dimensions[1] * dimensions[2] + ) + + // Arrange + const volume = new StreamingImageVolume( + // ImageVolume properties + { + uid: volumeId, + spacing: [1, 1, 1], + origin: [0, 0, 0], + direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], + dimensions, + sizeInBytes: volumeSizeInBytes, + scalarData, + metadata: { + PhotometricInterpretation: 'MONOCHROME2', + }, }, + // Streaming properties + { + imageIds: ['imageid1', 'imageid2'], + loadStatus: { + loaded: false, + loading: false, + cachedFrames: [], + callbacks: [], + }, + } + ) + + const volumeLoadObject = { + promise: Promise.resolve(volume), + cancelFn: undefined, } - ) - - const volumeLoadObject = { - promise: Promise.resolve(volume), - cancelFn: undefined, - } - const image1 = { - imageId: 'anImageId1', - sizeInBytes: image1SizeInBytes, - } - - const imageLoadObject1 = { - promise: Promise.resolve(image1), - cancelFn: undefined, - } - - cache.putImageLoadObject(image1.imageId, imageLoadObject1) - await imageLoadObject1.promise - - let cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(image1.sizeInBytes) - - expect(function () { - cache.putVolumeLoadObject(volume.uid, volumeLoadObject) - }).not.toThrow() + const image1 = { + imageId: 'anImageId1', + sizeInBytes: image1SizeInBytes, + } - await volumeLoadObject.promise - cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(volume.sizeInBytes) // it should remove the image (volatile) - }) + const imageLoadObject1 = { + promise: Promise.resolve(image1), + cancelFn: undefined, + } - it('should unsuccessfully cache a volume when there is not enough volatile + unallocated space', async function () { - const maxCacheSize = cache.getMaxCacheSize() - - const volume1SizeInBytes = maxCacheSize - 10000 - const volume2SizeInBytes = maxCacheSize - - const dimensions = [10, 10, 10] - const scalarData = createFloat32SharedArray( - dimensions[0] * dimensions[1] * dimensions[2] - ) - - const volumeId1 = 'aVolumeId1' - const volumeId2 = 'aVolumeId2' - - // Arrange - const volume1 = new StreamingImageVolume( - // ImageVolume properties - { - uid: volumeId1, - spacing: [1, 1, 1], - origin: [0, 0, 0], - direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], - dimensions, - scalarData, - sizeInBytes: volume1SizeInBytes, - metadata: {}, - }, - // Streaming properties - { - imageIds: ['imageid1', 'imageid2'], - loadStatus: { - loaded: false, - loading: false, - cachedFrames: [], - callbacks: [], + cache.putImageLoadObject(image1.imageId, imageLoadObject1) + await imageLoadObject1.promise + + let cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(image1.sizeInBytes) + + expect(function () { + cache.putVolumeLoadObject(volume.uid, volumeLoadObject) + }).not.toThrow() + + await volumeLoadObject.promise + cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(volume.sizeInBytes) // it should remove the image (volatile) + }) + + it('should unsuccessfully cache a volume when there is not enough volatile + unallocated space', async function () { + const maxCacheSize = cache.getMaxCacheSize() + + const volume1SizeInBytes = maxCacheSize - 10000 + const volume2SizeInBytes = maxCacheSize + + const dimensions = [10, 10, 10] + const scalarData = createFloat32SharedArray( + dimensions[0] * dimensions[1] * dimensions[2] + ) + + const volumeId1 = 'aVolumeId1' + const volumeId2 = 'aVolumeId2' + + // Arrange + const volume1 = new StreamingImageVolume( + // ImageVolume properties + { + uid: volumeId1, + spacing: [1, 1, 1], + origin: [0, 0, 0], + direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], + dimensions, + scalarData, + sizeInBytes: volume1SizeInBytes, + metadata: {}, }, + // Streaming properties + { + imageIds: ['imageid1', 'imageid2'], + loadStatus: { + loaded: false, + loading: false, + cachedFrames: [], + callbacks: [], + }, + } + ) + + const volumeLoadObject1 = { + promise: Promise.resolve(volume1), + cancelFn: undefined, } - ) - - const volumeLoadObject1 = { - promise: Promise.resolve(volume1), - cancelFn: undefined, - } - - const volume2 = new StreamingImageVolume( - // ImageVolume properties - { - uid: volumeId2, - spacing: [1, 1, 1], - origin: [0, 0, 0], - direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], - sizeInBytes: volume2SizeInBytes, - dimensions, - scalarData, - metadata: {}, - }, - // Streaming properties - { - imageIds: ['imageid11', 'imageid22'], - loadStatus: { - loaded: false, - loading: false, - cachedFrames: [], - callbacks: [], + + const volume2 = new StreamingImageVolume( + // ImageVolume properties + { + uid: volumeId2, + spacing: [1, 1, 1], + origin: [0, 0, 0], + direction: [1, 0, 0, 0, 1, 0, 0, 0, 1], + sizeInBytes: volume2SizeInBytes, + dimensions, + scalarData, + metadata: {}, }, + // Streaming properties + { + imageIds: ['imageid11', 'imageid22'], + loadStatus: { + loaded: false, + loading: false, + cachedFrames: [], + callbacks: [], + }, + } + ) + + const volumeLoadObject2 = { + promise: Promise.resolve(volume2), + cancelFn: undefined, } - ) - - const volumeLoadObject2 = { - promise: Promise.resolve(volume2), - cancelFn: undefined, - } - const promise1 = cache.putVolumeLoadObject(volume1.uid, volumeLoadObject1) - await promise1 + const promise1 = cache.putVolumeLoadObject(volume1.uid, volumeLoadObject1) + await promise1 - let cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(volume1.sizeInBytes) + let cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(volume1.sizeInBytes) - await expectAsync( - cache.putImageLoadObject(volume2.uid, volumeLoadObject2) - ).toBeRejectedWithError(ERROR_CODES.CACHE_SIZE_EXCEEDED) + await expectAsync( + cache.putImageLoadObject(volume2.uid, volumeLoadObject2) + ).toBeRejectedWithError(ERROR_CODES.CACHE_SIZE_EXCEEDED) - expect(cache.getVolumeLoadObject(volume2.uid)).not.toBeDefined() + expect(cache.getVolumeLoadObject(volume2.uid)).not.toBeDefined() - cacheSize = cache.getCacheSize() - expect(cacheSize).toBe(volume1.sizeInBytes) + cacheSize = cache.getCacheSize() + expect(cacheSize).toBe(volume1.sizeInBytes) + }) }) }) diff --git a/packages/cornerstone-render/test/groundTruth/calibrated_1_5_imageURI_11_11_4_1_1_1_0_1.png b/packages/cornerstone-render/test/groundTruth/calibrated_1_5_imageURI_11_11_4_1_1_1_0_1.png index cf6bd5050e2e621bcd8296e9106b6a91783bffb5..2c051734242dd3db2d61ffa6bc4cd41549aa4d25 100644 GIT binary patch literal 3027 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6+*)7d$|)7e=epeR2rGbfdS zL1SWaLV}Qoj>d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>SH^+=8PClV-%kJdhB0z#MXj zVR7KW?b!}j9SS1FQ?9+5dUEr2+0B*T>o2FT53YH$-{9(EhILb}JW5gc%ib}05_gxE zgoK1}L(bDrlb8f{vz`#sWU}HqF^@56x$})|hc_x5zZoKYJti1Ws0GS-ob&Ku+%%uz zrj@71qn92zEA}cJ5PXo4k|ZeMc3|a_=1Et&gaqEsi4TaJGIL(wq6Zv23qx4X?@6(h zWmRTYR%T-hsBjLc-^a-3?q65(jf-25~};jyffj$wOaBD;Yc%QM9zA@ht4K4=;l z$izfUh?v4r&hXfL+9Q)>Wscd^*38VWxBUNa-)?NMA=}8{&)$Fk|G$6x|NsBkV+Rh@ zbT>XcV9vmOCei)Cl1;$iWr#_Q@J#ddWzYh$IT%Z7zd0R2=Vqz z1{Say1_nDI&A`C4fC(nT$gqGJ!3IfgZQ1$&sDwK+B%&n3*T*V3KUXg?B|j-uuOhbq zq=3Pu!V1XDO)W`OsL0L9E4HezRRXK90GK4GQ<#=IWDQi$wiq3C7Jno3LtY6lk!VTY?YKi7Qq3;oh6xR2%GYXq22;| zP#+|tZ>VRW4>udA)dnPL|$gwsCpZHP;emy zA0%$TqQJN?h@DF z7UMaWq$97gWXa_2Rej#mcDyUTe_Wo4MU7Q%a&+`8hQ{p;Oa&Y)ZLA^(3VwvCFJQRC z(3r@`Y9O=kDyIg6xkEyP10zp+ekl{z0bT(Ku>gj{52NP-wJ?ur84WQiB$z*+AOBhK zL6qsbW&i8Kb;nEBz5ey}>+j9|@8jqHe|!G@Z@crqE8jh@wX6U8@6F@%_2>V;c)tJV z-SrW7_g~chP`K^+JK6vLjekD=XmNjEZT0`Zf4;un{N?xa&y{um{`~y+`{(QH>HnYp zzW?{#>VQ8tML)zh+Sezr~xy`!cJLRtLT=)9=_4nuP z_SM(^+it)A{coH1&v(xM{__tg?&j~c`@eYp|7Yd<6N>-4Y&`JQYUQJ!y?zZ23J>-( zFwR@fa*I{Op}Ogrx&Vhp1H<`XrngL73v3-f3v;lDI5a%hY%F7By)bXVPfivlE`XX=dAe-3JVyRZ2UR5aBDPtV>#=uAP_KW{b=A1>ZI3L&*=K#?$7(X)E`9L$>IF3@^0zWd@a{k7A#9sHH*>#>coSa!N}(QA$)IaN_tR$lx5Y0td#r}-I~ zv{~a=5}w}U^?e}E$oN5kLq|-Y;pk~|D3 z*7slkc)nj={`%j#`1jv`udl!N-GBe*pFe;8KD_&3z4nKq*y#*BYu=xpu77?0ufPBP z{{H&)_4WT>e!soFeSY2heeWAz#(u096=+y^_V3F2fBWByHY`-#`7EA^$>56yO9ICw zQ+HODgpO#2Hig*Y5C?{~9jv2*!#O}6JbhbLE%Y5kjz6*2wSkAMADA^ZQ&kB^0S z-+%vFxBvgI=e7U8SkIq7f2p)#n9>JeVaYVX!_s*{fCB^9^eh#ol?@C_g3bwQhzK+s zS}N7)prydDa!N4^7c0wxf|YJc6Sz1Ww4TZ|Ix#Y8#H^agv4n|ff|sTHsP@sI8^+1z b59_O4mKh(<6dwaNO&B~~{an^LB{Ts5pCsoc diff --git a/packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_100_100_1_1_0.png b/packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_100_100_1_1_0.png new file mode 100644 index 0000000000000000000000000000000000000000..2ebfabaad1e19c5aa38f9eab12b426047dbcc609 GIT binary patch literal 2995 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6+*)7d$|)7e=epeR2rGbfdS zL1SWaLV}Qoj>d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>XoR(FG?Hrp$ z4U7zok`fqN&0W4emwr%?BJ$XwL)66BAi|H$=$_&cp<)S%GKR;F335(8Pc505=PNNE zKG@3I>DpPqZcvx3>1S-#v|;CVV}lKTzyJR)wqR!7t;T%#{_Fq$|KI!f|Ns3|9-cGX zB_#N2m>Fz%;+HJ{y6-Ffc7(f{8FPEMP{kL6TcrwmtwV;m!<+D2ed(u}aR*)k{ptPfFFR$SnXV zV6dsM0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~o`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6Vxu2ckY8Bflgc7z#z1$)0&7sYL}v zzz_lJHiGKLtpeSwpw#00oKleH(7enNJD34z!sxo3^K*fg78fJC7+DOe-bNo3Tu8wO zi5sveFmmj;Z1mv~ZO64N_L~6%1N&M}7srqa#cl5n&nq)kcqk8L`;p{4De=b%=br>mdKI;Vst08WAsl>h($ literal 0 HcmV?d00001 diff --git a/packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_100_100_1_1_0_hotIron.png b/packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_100_100_1_1_0_hotIron.png new file mode 100644 index 0000000000000000000000000000000000000000..cff9a15d311d7e001613cae20ad135cb98d56580 GIT binary patch literal 3094 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6+*)7d$|)7e=epeR2rGbfdS zL1SWaLV}Qoj>d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>XoR(FG?Hrp$ z4U7zok`fqN&0W4emwr%?BJ$XwL)66BAi|H$=$_&cp<)S%GKR;F335(8Pc505=PNNE zKG@3I>DpPqZcvx3>1S-#v|;CVV}lKTzyJR)wqR!7t;T%#{_Fq$|KI!f|Ns3|9-cGX zB_#N2m>Fz%;+HJ{y6-Ffc7(f{8FPEMP{kL6TcrwmtwV;m!<+D2ed(u}aR*)k{ptPfFFR$SnXV zV6dsM0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~o`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6Vxu2ckY8Bflgc7z#z1$)0&7sYL}v zzz_lJHiGKLtpeSwpw#00oKleH(7enNJD34z!sxo3^K*fg78fJC7+DOe-bNo3Tu8wO zi5sveFmmj;Z1mv~ZO64N_L~7P#R+)2IEGX(zP;kedpJPA`QVZ%r{+6$YwC8C|CpA> zxMt19C%=nh&p+)GtgwG&v8<ibeGTw?xCeEAC9jOl%3uv>Z9VFtIH#|2lD0`)JUO pCY#YLGg@4X7L}vb#NenR?KPLPou1Blxf9ed^mO%eS?83{1OTMlD(L_K literal 0 HcmV?d00001 diff --git a/packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_100_100_1_1_0_voi.png b/packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_100_100_1_1_0_voi.png new file mode 100644 index 0000000000000000000000000000000000000000..0cf439b2362b32ed37c9cf0e2ad8b5880a6df484 GIT binary patch literal 2998 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6+*)7d$|)7e=epeR2rGbfdS zL1SWaLV}Qoj>d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>XoR(FG?Hrp$ z4U7zok`fqN&0W4emwr%?BJ$XwL)66BAi|H$=$_&cp<)S%GKR;F335(8Pc505=PNNE zKG@3I>DpPqZcvx3>1S-#v|;CVV}lKTzyJR)wqR!7t;T%#{_Fq$|KI!f|Ns3|9-cGX zB_#N2m>Fz%;+HJ{y6-Ffc7(f{8FPEMP{kL6TcrwmtwV;m!<+D2ed(u}aR*)k{ptPfFFR$SnXV zV6dsM0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~o`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6Vxu2ckY8Bflgc7z#z1$)0&7sYL}v zzz_lJHiGKLtpeSwpw#00oKleH(7enNJD34z!sxo3^K*fg78fJC7+DOe-bNo3Tu8wO zi5sveFmmj;Z1mv~ZO64N_L~6%1N#O~7srqa#W%}o_xUS3)BmuZv&`%$x0+kP!r{ZnY)~n~xrLR-VR!t?QSNAH qj3$QByf9i6jFyC>B_VA}!bbh_t*rlzxnFpL8g8DhelF{r5}E+RRC>1n literal 0 HcmV?d00001 diff --git a/packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_50_10_1_1_0.png b/packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_50_10_1_1_0.png new file mode 100644 index 0000000000000000000000000000000000000000..685a1ad5fb56519b528584beb3dcdad8c1059645 GIT binary patch literal 2996 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6+*)7d$|)7e=epeR2rGbfdS zL1SWaLV}Qoj>d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>XoR(FG?Hrp$ z4U7zok`fqN&0W4emwr%?BJ$XwL)66BAi|H$=$_&cp<)S%GKR;F335(8Pc505=PNNE zKG@3I>DpPqZcvx3>1S-#v|;CVV}lKTzyJR)wqR!7t;T%#{_Fq$|KI!f|Ns3|9-cGX zB_#N2m>Fz%;+HJ{y6-Ffc7(f{8FPEMP{kL6TcrwmtwV;m!<+D2ed(u}aR*)k{ptPfFFR$SnXV zV6dsM0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~o`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6Vxu2ckY8Bflgc7z#z1$)0&7sYL}v zzz_lJHiGKLtpeSwpw#00oKleH(7enNJD34z!sxo3^K*fg78fJC7+DOe-bNo3Tu8wO zi5sveFmmj;Z1mv~ZO64N_L~6%1N%Bp7srqa#^(wtrSMFb)WrI z7q)$8+;`sc{&o31zxT;+-LdZVMf>?2A}n^TldOakHn8#>D4Qtc!6DIbWadB_VA}!aBidc0d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>XoR(FG?Hrp$ z4U7zok`fqN&0W4emwr%?BJ$XwL)66BAi|H$=$_&cp<)S%GKR;F335(8Pc505=PNNE zKG@3I>DpPqZcvx3>1S-#v|;CVV}lKTzyJR)wqR!7t;T%#{_Fq$|KI!f|Ns3|9-cGX zB_#N2m>Fz%;+HJ{y6-Ffc7(f{8FPEMP{kL6TcrwmtwV;m!<+D2ed(u}aR*)k{ptPfFFR$SnXV zV6dsM0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~o`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6Vxu2ckY8Bflgc7z#z1$)0&7sYL}v zzz_lJHiGKLtpeSwpw#00oKleH(7enNJD34z!sxo3^K*fg78fJC7+DOe-bNo3Tu8wO zi5sveFmmj;Z1mv~ZO64N_L~7P#c6rEIEGX(zP)lV^Du*mi=(RU4)&dC)6>2$kP>>s z=Txzw{i5rOMSss6s?isGU@;T literal 0 HcmV?d00001 diff --git a/packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_50_10_1_1_0_rotate.png b/packages/cornerstone-render/test/groundTruth/cpu_imageURI_256_256_50_10_1_1_0_rotate.png new file mode 100644 index 0000000000000000000000000000000000000000..62d0b2e34facfb288ab5bcc8cc136cdd07719bfe GIT binary patch literal 2680 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6+*)7d$|)7e=epeR2rGbfdS zL1SWaLV}Qoj>d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>XoR(FG?Hrp$ z4U7zok`fqN&0W4emwr%?BJ$XwL)66BAi|H$=$_&cp<)S%GKR;F335(8Pc505=PNNE zKG@3I>DpPqZcvx3>1S-#v|;CVV}lKTzyJR)wqR!7t;T%#{_Fq$|KI!f|Ns3|9-cGX zB_#N2m>Fz%;+HJ{y6-Ffc7(f{8FPEMP{kL6TcrwmtwV;m!<+D2ed(u}aR*)k{ptPfFFR$SnXV zV6dsM0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~o`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6Vxu2ckY8Bflgc7z#z1$)0&7sYL}v zzz_lJHiGKLtpeSwpw#00oKleH(7enNJD34z!sxo3^K*fg78fJC7+DOe-bNo3Tu8wO zi5sveFmmj;Z1mv~ZO64N_L~6%16z@&i(^Oyl+vgI9Lubu_?std%?ofAgv%EX29_9ldF0H z!ySgk4GoMO)Ag+wnIEumw6O9x?5MfODZyawprDh$aOi2b-vNdlj7*7)%mRAxB}{Aw zcm+7PB^nBTE)q6i$ZK$jIKa?yT6`|h3g%HO=o?VU^U}FkI9yndZPK~;B1u@m;9BWY zhX#j`1grC(`_jZ`JH#dI`tKjd$k4c=VL!vgM@Q8@a7Zwe(;#9S3uDDunOGJvJ+|n} zDA~vd_^#Ds(sXSFrCipqmJI(mW<>W>{bp>XoR(FG?Hrp$ z4U7zok`fqN&0W4emwr%?BJ$XwL)66BAi|H$=$_&cp<)S%GKR;F335(8Pc505=PNNE zKG@3I>DpPqZcvx3>1S-#v|;CVV}lKTzyJR)wqR!7t;T%#{_Fq$|KI!f|Ns3|9-cGX zB_#N2m>Fz%;+HJ{y6-Ffc7(f{8FPEMP{kL6TcrwmtwV;m!<+D2ed(u}aR*)k{ptPfFFR$SnXV zV6dsM0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~o`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6Vxu2ckY8Bflgc7z#z1$)0&7sYL}v zzz_lJHiGKLtpeSwpw#00oKleH(7enNJD34z!sxo3^K*fg78fJC7+DOe-bNo3Tu8wO zi5sveFmmj;Z1mv~ZO64N_L~6%1N$dW7srqa#tUv$0?EJ2E+kZc+ulZo&n>_FNMxE1N|JVM1YI(j`=D6Xv z+Wr6bzdyP;H(GFZy4`0sE&+uJoDvN^aR!ZX2N=GXUubYpOJFdnI>=JR$b8|x12fai z2F4RFScPA(@;KBp^Kzsxu_-JT_gNrr!0?yXWK{ELfQ=@S(Tp)#7>t&lqcsNAYRUZC W43Yn5HTlFqO+QapKbLh*2~7apy6uPn literal 0 HcmV?d00001 diff --git a/packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_64_0_10_5_5_0.png b/packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_64_0_10_5_5_0.png new file mode 100644 index 0000000000000000000000000000000000000000..89248061f5675152f649d2cc91885cf8ab744303 GIT binary patch literal 3097 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6+*)7d$|)7e=epeR2rGbfdS zL1SWaLV}Qoj>d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>XoR(FG?Hrp$ z4U7zok`fqN&0W4emwr%?BJ$XwL)66BAi|H$=$_&cp<)S%GKR;F335(8Pc505=PNNE zKG@3I>DpPqZcvx3>1S-#v|;CVV}lKTzyJR)wqR!7t;T%#{_Fq$|KI!f|Ns3|9-cGX zB_#N2m>Fz%;+HJ{y6-Ffc7(f{8FPEMP{kL6TcrwmtwV;m!<+D2ed(u}aR*)k{ptPfFFR$SnXV zV6dsM0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~o`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6Vxu2ckY8Bflgc7z#z1$)0&7sYL}v zzz_lJHiGKLtpeSwpw#00oKleH(7enNJD34z!sxo3^K*fg78fJC7+DOe-bNo3Tu8wO zi5sveFmmj;Z1mv~ZO64N_L~7P#R+@5IEGX(zP)mq^KgKOv!nW7^NjBgq&}^FsG~Gv zyWu?!L-8*awf8=@zu))%@ve1`ZO-5S{`ai;K~sa7cfur>mdKI;Vst0G>Y|{Qv*} literal 0 HcmV?d00001 diff --git a/packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_64_20_5_1_1_0.png b/packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_64_20_5_1_1_0.png new file mode 100644 index 0000000000000000000000000000000000000000..840584332c151faca92401d9df284e1fa415b755 GIT binary patch literal 9251 zcmeAS@N?(olHy`uVBq!ia0y~yU^)ZDZ5&KMk&6XU?hFi!PMOZm0iMpz3IRp=X_+~x z3=A3*lM@nzJajZpBqk;#oH(nk$yHPy)X~uslu&={zzKzu2aYZ{nJ{HWOw0obfd|YX zhZq(I9^9VoaMht8QamNB_T6^wa^IhG%%<*pewpKD5mT@&*8?w4T|c%<^$ix9I~S~I zX>2rPJQ1d+ugS16x*=zx(t^beZzL7UtXlT69r0`0z~6ASg~gKjW_UwR^qB5`VNZT!LCK<*)H#M=wM3y;TRvMmbH5f0R`^h@hb>>S+Rj$4R0$!mX&UjzD z9B5!%sgL- z`S8J3)=t;X0(OJCWKBO~v!)F@w;LO5@caG$f3XEK^KLcf!}nkR|NsBqzyJU5r}FTe z*)Ac$SHsL;!xO(``PY5_fq@#68sVAd>&u`8WOFdEFtRZ)16hm=zyN0yg0dNbYz>5X zdnN-5SPcV%9gqgb&jKcx2-BGb%y4!aNOJ0}FfdWk9dNvV1j zxdk8v3^o;3KxS@gNuokUZcbjYRfVk*ScMgk4HDK@QUEI{$+lIB@C{IK&M!(;Fx4~B zGf=YQQczH^DN0GR3UYCSY6tRcl`=|73as??%gf94%8m8%i_-NCEiEne4UF`SjC6r2 zbc-wVN)jt{^NN)rhQQ2mNi9w;$}A|!%+FH*nVXoDUs__Tqy(}E4j}F<$xK7olvfP( z7SMzGAQ^o_Jp+BX*+8u}AWsb3~j(FQN$4TK-33h}U>aSW+oe0$9>Pl{2Z<)R9&5bLukGF`z; zwn5uADSe->n<~fpDdL|Md&(=G)uZt-JpEYTi5d7Z=Jnd|Mk~jYwY^%``*{vAAfKEDZWyZk%?s=$DQ}z_4m8<*Ds3M$ig9@ z;LvbjN5e$JI)+!mKsH08*MSo&Ql1Kd*@cYC$$uEGr~}0rnWi>O3_N*C70iCjPr-Ru&+!B(H*%#P>#06Hc^z?Xz z>4Di2;s!>?_#J9N;tBzr5|UHb#DLibdI>2#@(sVhj$9yQV5Akf6UuRm#fLcFJu1#v`WO&)z4*}Q$iB}@^XAD literal 0 HcmV?d00001 diff --git a/packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_64_30_10_5_5_0.png b/packages/cornerstone-render/test/groundTruth/cpu_imageURI_64_64_30_10_5_5_0.png new file mode 100644 index 0000000000000000000000000000000000000000..e3c01d4cb1d375c02dddf7269038e1c674fb2f3a GIT binary patch literal 3118 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6+*)7d$|)7e=epeR2rGbfdS zL1SWaLV}Qoj>d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>XoR(FG?Hrp$ z4U7zok`fqN&0W4emwr%?BJ$XwL)66BAi|H$=$_&cp<)S%GKR;F335(8Pc505=PNNE zKG@3I>DpPqZcvx3>1S-#v|;CVV}lKTzyJR)wqR!7t;T%#{_Fq$|KI!f|Ns3|9-cGX zB_#N2m>Fz%;+HJ{y6-Ffc7(f{8FPEMP{kL6TcrwmtwV;m!<+D2ed(u}aR*)k{ptPfFFR$SnXV zV6dsM0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~o`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6Vxu2ckY8Bflgc7z#z1$)0&7sYL}v zzz_lJHiGKLtpeSwpw#00oKleH(7enNJD34z!sxo3^K*fg78fJC7+DOe-bNo3Tu8wO zi5sveFmmj;Z1mv~ZO64N_L~7P#c6oDIEGX(zP)lVmnl%h`J!jk0`?a@^V;QBC<=-+ zuD|ee_Qf?hbJc%7uiO7$_TF{Q7`^F_tMpxyN@A>cFzrNe) zfB5~=qmzXrgoVf9sZf=G1cyWe<1P;-QAXx~oh@=KY)otk4SJItTn{jW=q+S#Y;0hZ zSkSE!AZWla)%%5Gfd_^#Ds(sXSFrCipqmJI(mW<>W>{bp>XoR(FG?Hrp$ z4U7zok`fqN&0W4emwr%?BJ$XwL)66BAi|H$=$_&cp<)S%GKR;F335(8Pc505=PNNE zKG@3I>DpPqZcvx3>1S-#v|;CVV}lKTzyJR)wqR!7t;T%#{_Fq$|KI!f|Ns3|9-cGX zB_#N2m>Fz%;+HJ{y6-Ffc7(f{8FPEMP{kL6TcrwmtwV;m!<+D2ed(u}aR*)k{ptPfFFR$SnXV zV6dsM0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~o`I4b zmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJp<7&; zSCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6Vxu2ckY8Bflgc7z#z1$)0&7sYL}v zzz_lJHiGKLtpeSwpw#00oKleH(7enNJD34z!sxo3^K*fg78fJC7+DOe-bNo3Tu8wO zi5sveFmmj;Z1mv~ZO64N_L~7P#ff>kIEGX(zP)m=w>dz>IdGEL3r4HO|M`q&T@i>Z zFZ{>0aZ~BMC)YpQ%s;H`@L)O9^GdnvrLx~^>-YcvfAqWFv`d`~jlB(wH!QprCx{p@ zoZPJ8nxLG(V0JEuLxq*c;ggP+;0+FmhThF0OrDI)6KuMJSj?E%5+3WcIs_eHP&vOy vWK{cT(2XXW(JV7sT#Oc#qt(RVs3PkH%~=0%>CSQnH4i;q{an^LB{Ts5KI`Kd literal 0 HcmV?d00001 diff --git a/packages/cornerstone-render/test/groundTruth/imageURI_100_100_0_10_1_1_1_linear_color.png b/packages/cornerstone-render/test/groundTruth/imageURI_100_100_0_10_1_1_1_linear_color.png index 307b2871efe0489fa1c81256d30548dc50df43bc..70e60003725734d40383fd15769e7a2613e349de 100644 GIT binary patch literal 7893 zcmeHMeNYou5Pz3Ipb-JJQxL11pj8Vd0dyE*AV{c!6-A^q&IAY_0a77Jlh6b~3F1^L zosr`A7!q1-)plweZ7UX$pjAkQ9IJl0n6$9oL$Q<7?iMO&0Vkalv?@uZ^ z(hM7?^c3dhObJ|OJbI|A?&kKX{!g@7sq1&`*RA=c$J^)YbWhWnyx5tZq14b&8ZA}L zORqQ!`7U1;;k%W;G>Tf)mj^$XnGhbatuot-HOu3#*}tXhqF7HCM6%u$@N4q z^q%{Raznijlr5oBEkC*4+-*^;;~OGb&Suo@9(>|1y_JKgbZQw{8##IV&Fkw0EojzY<(IiFEkX;Kz$|S z#;!d<;TU9+IGwNLr>tb>N{boUdD0vaLtQMx0f3`sV`8yLnGLIpizEuRIyQtrurV1| zGeckkqTCQ0!cR$sc~ZFuUd&j;V1>ldVHoDf^YYngiOVM3vAfuiLZwp1W-?VO6+;!p zkje{~5iv0_OjaZ_GIAk?Sg0tKD6`cIC5q5-k{3LQB1NuTEK`c55*X*r&XJZVV?#pl zKrgTHc`C*EuOdkl6OV;GAQQJRBN!~^OK!}SgKOEV5XtDW7Q@cHR}fsZb7>6AHJ{Cz(QK~6yn0*(hC{bl3gPkxRqRoT8be+>C$>h>~s z1V9uIZ+}>}mLl!GdCDew%O8%{fXYq^P2*JwaAS9a29i~MJ9Y)f#T&s>pK-{Hw^=}P zT$#0>Km`Kb1XC4-W}LidAZ!{=1W}Dq_(1WHq#8kE>+hRHL_8r-({MVCaWXv+MDu?& ztRzrAz(mmbH!g*!J+u$)yNL;Dpq}Xj9VL3c5OfRs&qfl_Sb#$%rqx69^wG=+zq{@| zA{tFoK+s{6H=b{Dc2q3!8yFI7wp_Azln!68p-wH+VE@5Xv3Z~S&JqVCb*Vdc7POqz zqr;Ae!74eMJ9MAJHtS5LXr!i7Z_d&qzar-=D_i}Oc~#o9auxd#p)<>;SUrVbr- zJnCFMtFOd-*>9xpj6#=Ty1chjVSQxnvLMJP#olXkI&1+~ZCXwf+JK^tYti=Ej@{2T z7IuUo>jo{Yr7p+FXm>^PrhARLx_O;r{WkQW&EQrKEFU(P=mie9+_kpZ8K7BlJA+6b zD_jVC_J#%#DLCTcLmFT*CFILMmH}A?Zj*@@UYks8G@gN&K!j*;pIO$A7k#brU}A&%4Yo-*}%s}oV6t{!c4 zu--b<@98#_^`gUz?Oa#CmYek{*EOtNeXm7VUp91|!1wnY`@h zTxS|l+2)egicPUm=4Mq#%??Ai`RL${yB#Mj zgL}Cd=HBzxLhguruJd8}^NNtDQJLEzU0Id{I)X52&#qk kbBRNXcI@Ef|7vhkZwN9o+K;QB;pdvkAFN6|mLS~nH#}W#_W%F@ literal 13246 zcmeI3T}%{L6vxjlE(nScwZ)?Cnx?kYrrkD%APPI!VvuTJL$#ELy7VKZt!cnift6ur zDXFQo*raHPwaZf4it#}oG=T^*tB70D$dr_i&?37?n^_R{gUc>6yR$PL;khq;=tFZJ zZf0`sWNv2W&b{aS&+nWv6%`~Wr6&PEa^b%G5&*4ws)g0@>S3{VI0CR03iJ0ItUWwG zdhbfOrtIVtBW^EmYgk|KVqa&sbGWHmKk<3>hr3rLf1LVd;zhl8pZ`czdrnzF^SO~* z_J{SOq7?6{V@}A69WEv*qs^vG$_QLc>Pdjg*=X3ceZ_EG1A)|yraX;qV{M}jJjvD> z`F-880i2tc?Iut1<6z4Yeu_3fwrl{Xe004juOaI#fJsk_ulUVn13=HRU89>U*FbHf z`36+GCNHH?Gf17mwYX^U=xRt+F!;zWpN~=P+!_&or>~H1!xL#50?_`<9 z;;X@NMUcGWJ>nBXWMGVC2)!Uh$hw`fp{ES2k>VZKMKBY4$L923G-8vEK+)n|Iaw$r;a0p7b5Sqro6zdZ#L=heCFD_w7f zye-GRi?h9Qus$A+4}FvZz5{B{(hH{yuyx(8L5*|MWn-M}hWB~`)I5uigI@1W$dyICd2EG<}!idSvI1-BAzX4_K)mOks3rz}Q6vRES`p#%L{ ztXmly6Xukl7_{k#WV(bA*7$o%KEg~&6@}jrHEztq_NY8nL z!%vxW^pt~%l72Eo{@@$Oga9K=6Ixd99TbT)UCOext!_D-X^av(uMRrTmEL3{;?#aR z?9cV*>M4DWK38oNWig(xMx;4$NbRDSpB)s%Oy>IKcOBU}r+My;^p(<#yoT!lKV~;Q wzf$&*u9kXa_CDt*S*hc|zJGWxPDZd_^#Ds(sXSFrCipqmJI(mW<>W>{bp>SH^+=8PClV-%kJdhB0z#MXj zVR7KW?b!}j9SS1FQ?9+5dUEr2+0B*T>o2FT53YH$-{9(EhILb}JW5gc%ib}05_gxE zgoK1}L(bDrlb8f{vz`#sWU}HqF^@56x$})|hc_x5zZoKYJti1Ws0GS-ob&Ku+%%uz zrj@71qn92zEA}cJ5PXo4k|ZeMc3|a_=1Et&gaqEsi4TaJGIL(wq6Zv23qx4X?@6(h zWmRTYR%T-hsBjLc-^a-3?q65(jf-25~};jyffj$wOaBD;Yc%QM9zA@ht4K4=;l z$izfUh?v4r&hXfL+9Q)>Wscd^*38VWxBUNa-)?NMA=}8{&)$Fk|G$6x|NsBkV+Rh@ zbT>XcV9vmOCei)Cl1;$iWr#_Q@J#ddWzYh$IT%Z7zd0R2=Vqz z1{Say1_nDI&A`C4fC(nT$gqGJ!3IfgZQ1$&sDwK+B%&n3*T*V3KUXg?B|j-uuOhbq zq=3Pu!V1XDO)W`OsL0L9E4HezRRXK90GK4GQ<#=IWDQi$wiq3C7Jno3LtY6lk!VTY?YKi7Qq3;oh6xR2%GYXq22;| zP#+|tZ>VRW4>udA)dnPL|$gwsCpZHP;emy zA0%$TqQJCm{|liG~LkL)8oznGHBS6*LkUn%u)@9$?`) zprX>apn;J?E;f}(*nna31ePWyHie3kshp!4M+0Xx<&5Tz(V}CtcpP4J#}7u)?Vftl Sc}#tvwwI@?pUXO@geCw9<8Q?P literal 2553 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJFF1D-C9Ar*0NuWamUabR$9 zG{5)%e+aYVj;D{fzuznfIJAWK+KX!0;~$GF_K8*OzkfXV`L1=3ZO*%&tGyq;e|O`5 z?GHi?&pyrj^|QX5b-_+$?Rr)ggZ~jtzieCGfK56EPgg&ebxsLQ0Csejp8x;= diff --git a/packages/cornerstone-render/test/groundTruth/imageURI_11_11_4_1_1_1_0_nearest_invert_90deg.png b/packages/cornerstone-render/test/groundTruth/imageURI_11_11_4_1_1_1_0_nearest_invert_90deg.png index e046aab8d292c08827678b80581d1345acf6c10a..be7fae0969c84ec5621f2d29d628073973b5a51c 100644 GIT binary patch literal 2774 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6+*)7d$|)7e=epeR2rGbfdS zL1SWaLV}Qoj>d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>SH^+=8PClV-%kJdhB0z#MXj zVR7KW?b!}j9SS1FQ?9+5dUEr2+0B*T>o2FT53YH$-{9(EhILb}JW5gc%ib}05_gxE zgoK1}L(bDrlb8f{vz`#sWU}HqF^@56x$})|hc_x5zZoKYJti1Ws0GS-ob&Ku+%%uz zrj@71qn92zEA}cJ5PXo4k|ZeMc3|a_=1Et&gaqEsi4TaJGIL(wq6Zv23qx4X?@6(h zWmRTYR%T-hsBjLc-^a-3?q65(jf-25~};jyffj$wOaBD;Yc%QM9zA@ht4K4=;l z$izfUh?v4r&hXfL+9Q)>Wscd^*38VWxBUNa-)?NMA=}8{&)$Fk|G$6x|NsBkV+Rh@ zbT>XcV9vmOCei)Cl1;$iWr#_Q@J#ddWzYh$IT%Z7zd0R2=Vqz z1{Say1_nDI&A`C4fC(nT$gqGJ!3IfgZQ1$&sDwK+B%&n3*T*V3KUXg?B|j-uuOhbq zq=3Pu!V1XDO)W`OsL0L9E4HezRRXK90GK4GQ<#=IWDQi$wiq3C7Jno3LtY6lk!VTY?YKi7Qq3;oh6xR2%GYXq22;| zP#+|tZ>VRW4>udA)dnPL|$gwsCpZHP;emy zA0%$TqQJ;Ut zU}!nsey)LG2P4}dHWmdNyAn2*1H1+TW(o`sKQ1&@V90A|+`!1l(a%4Zk+Fc8r-hfp zp`zvm4@ZM^fn;k`x3TGY=0OoeQsV4OfbJ#ANT{Q zFMl4qW@A}U$N1wiFtOO$|JcRIxFVk6-xfwjkMsQd@*Egmi8t8iI?z~Kqpfi!;{!-%kiY5bGAgTu`RhAG#N&oftG_$jQg0GKvUy-q$4bo3uifdF7~e)@Ieb712A z$HJij6#Vq-#%G`x>X}%k00k>+-&8U)O|WNVItBFJ9=mUQ7#UB@XJAZaW(tUtufEs7 r@Wj8NVG|?csCA=(Ihs_cn-Rpj-m!_E6n=Ob)Ufe%^>bP0l+XkKW}L%1 literal 1689 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G!U;i$lZxy-8q?;3=C|+o-U3d z6>)FxI`T3Y@Gx(jX8*!Zs`s5eFDrPK(cN zU@Tx}ImE=K5VP+E3r~Zzf`FI-!^2Om>In>Y7#cS;Fmg=Sw*p$hHfqIaK#iajb>Q5Y zLOV diff --git a/packages/cornerstone-render/test/groundTruth/imageURI_256_256_50_10_1_1_0.png b/packages/cornerstone-render/test/groundTruth/imageURI_256_256_50_10_1_1_0.png index 824b80a6e3181b2c0150b43b21808b00a1dcc200..775f71ffe943212082803f87588d6aa68dcaa31e 100644 GIT binary patch literal 2694 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6+*)7d$|)7e=epeR2rGbfdS zL1SWaLV}Qoj>d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>SH^+=8PClV-%kJdhB0z#MXj zVR7KW?b!}j9SS1FQ?9+5dUEr2+0B*T>o2FT53YH$-{9(EhILb}JW5gc%ib}05_gxE zgoK1}L(bDrlb8f{vz`#sWU}HqF^@56x$})|hc_x5zZoKYJti1Ws0GS-ob&Ku+%%uz zrj@71qn92zEA}cJ5PXo4k|ZeMc3|a_=1Et&gaqEsi4TaJGIL(wq6Zv23qx4X?@6(h zWmRTYR%T-hsBjLc-^a-3?q65(jf-25~};jyffj$wOaBD;Yc%QM9zA@ht4K4=;l z$izfUh?v4r&hXfL+9Q)>Wscd^*38VWxBUNa-)?NMA=}8{&)$Fk|G$6x|NsBkV+Rh@ zbT>XcV9vmOCei)Cl1;$iWr#_Q@J#ddWzYh$IT%Z7zd0R2=Vqz z1{Say1_nDI&A`C4fC(nT$gqGJ!3IfgZQ1$&sDwK+B%&n3*T*V3KUXg?B|j-uuOhbq zq=3Pu!V1XDO)W`OsL0L9E4HezRRXK90GK4GQ<#=IWDQi$wiq3C7Jno3LtY6lk!VTY?YKi7Qq3;oh6xR2%GYXq22;| zP#+|tZ>VRW4>udA)dnPL|$gwsCpZHP;emy zA0%$TqQJ8=3(dklQ5ABsL zANDrPZTa)^mdA!gqT7M=!a z1pzSwhKHYA)e{)*Ff?vxVC0ytZv}J@8yW72IKa?yT6`|h3g%HO@I~BcKoOb3M+0g! zpoVoB)mWQXoTT8eK=XmP!{+jpoH4*sUJKD}psLd!*zXWQCc)I$ztaD0e F0sydAT!8=p literal 2241 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJEa4o?@ykczmsw>IX=IEt`b z+^ZqUQ+w=?j(>xXl#_6TWbp5ldYNt6UaXQ;d)I&e{fV8CMb7hqDnoxw`C`Ez-VO{M z4F{SzISw?--!EZdX<+8CnbE*t^W&qhy90v-0jWP)4C`N)%B*_-{{8F6j~~y!|M%z5 z%G%mm+3jc9l|M{A@cY-@|8K+_UhiT#5Wj?rk@3gJN&i>_0&n-VHZX7;c+@#tgOO37 zVa2U9T^1Gx1~LCTE}R?<3@%sAvK18=m^M^AQuA_PU{P4LEs`6g@TjnPAQO`UL*Ul4 zQ5-;pI`ejP3JEYUcC5<1<^)uj@bQT66rjltA-S8yKnlCn^A>?jUX(R^s{l}8giWEa zD$rzs(A#T9oiG{-qiJC@M~s#Wqow1psv{czOEo-YzN64^z>+2I{p-JW&x>m{nKd_^#Ds(sXSFrCipqmJI(mW<>W>{bp>SH^+=8PClV-%kJdhB0z#MXj zVR7KW?b!}j9SS1FQ?9+5dUEr2+0B*T>o2FT53YH$-{9(EhILb}JW5gc%ib}05_gxE zgoK1}L(bDrlb8f{vz`#sWU}HqF^@56x$})|hc_x5zZoKYJti1Ws0GS-ob&Ku+%%uz zrj@71qn92zEA}cJ5PXo4k|ZeMc3|a_=1Et&gaqEsi4TaJGIL(wq6Zv23qx4X?@6(h zWmRTYR%T-hsBjLc-^a-3?q65(jf-25~};jyffj$wOaBD;Yc%QM9zA@ht4K4=;l z$izfUh?v4r&hXfL+9Q)>Wscd^*38VWxBUNa-)?NMA=}8{&)$Fk|G$6x|NsBkV+Rh@ zbT>XcV9vmOCei)Cl1;$iWr#_Q@J#ddWzYh$IT%Z7zd0R2=Vqz z1{Say1_nDI&A`C4fC(nT$gqGJ!3IfgZQ1$&sDwK+B%&n3*T*V3KUXg?B|j-uuOhbq zq=3Pu!V1XDO)W`OsL0L9E4HezRRXK90GK4GQ<#=IWDQi$wiq3C7Jno3LtY6lk!VTY?YKi7Qq3;oh6xR2%GYXq22;| zP#+|tZ>VRW4>udA)dnPL|$gwsCpZHP;emy zA0%$TqQJJgN5lb! zn3#PpSa=%B8x9<0VterDldJlNuhC7d_^#Ds(sXSFrCipqmJI(mW<>W>{bp>SH^+=8PClV-%kJdhB0z#MXj zVR7KW?b!}j9SS1FQ?9+5dUEr2+0B*T>o2FT53YH$-{9(EhILb}JW5gc%ib}05_gxE zgoK1}L(bDrlb8f{vz`#sWU}HqF^@56x$})|hc_x5zZoKYJti1Ws0GS-ob&Ku+%%uz zrj@71qn92zEA}cJ5PXo4k|ZeMc3|a_=1Et&gaqEsi4TaJGIL(wq6Zv23qx4X?@6(h zWmRTYR%T-hsBjLc-^a-3?q65(jf-25~};jyffj$wOaBD;Yc%QM9zA@ht4K4=;l z$izfUh?v4r&hXfL+9Q)>Wscd^*38VWxBUNa-)?NMA=}8{&)$Fk|G$6x|NsBkV+Rh@ zbT>XcV9vmOCei)Cl1;$iWr#_Q@J#ddWzYh$IT%Z7zd0R2=Vqz z1{Say1_nDI&A`C4fC(nT$gqGJ!3IfgZQ1$&sDwK+B%&n3*T*V3KUXg?B|j-uuOhbq zq=3Pu!V1XDO)W`OsL0L9E4HezRRXK90GK4GQ<#=IWDQi$wiq3C7Jno3LtY6lk!VTY?YKi7Qq3;oh6xR2%GYXq22;| zP#+|tZ>VRW4>udA)dnPL|$gwsCpZHP;emy zA0%$TqQJK~mvE0wZI^hmULh92jcYIBX&u802bdRtpO-%x6SstE-#-;Op04 z@9aN*=M?zi&$R#l`tvoL?lY+MD>iOxVA%3L+nOoZVU#->8l#C}G%t)61w*hTeDHkE zztkUf`~QCmwpVg^;MI7&PJ2INj(jjvCL^QA{Od(5z=D2YxCgfH7kcyc*YR?mx6fw^ zDHH@e_`7xgq4W6+?EMb|S@MJh8gA=7s^yRo5ol<>A?NzQjFEB2TcvyL2OJq0Ejr%P zRmqQU=1hNIOS|PoU-%8Ih$9c=GtAHR;dsKzqEMg2E%ZZ2fk=KGzopr0Hf7Dv;Y7A literal 0 HcmV?d00001 diff --git a/packages/cornerstone-render/test/groundTruth/test.png b/packages/cornerstone-render/test/groundTruth/test.png deleted file mode 100644 index fcbe2d2e239bdeb5c7c5ce2f3eed24c8135c613d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2393 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJDv1y2{pkczmsx4iSD9Yvf2 zzy1@y%V@+s!R6nHeJ!smvb<|tg~Vlk?2`Zf_rqaEKAD#XSQ*@(y}Ky$<1GV&1`|79 z83O|!-~G#$3=CEW8k=Pq8k!G(FX3Zo;465Lz|QdC!3W#9%>Il8wt9Md4>taG{qI*8 z`=_q??!PDUe0Tc4|BdEn*t5cm;e(QL?b+or3=RG_zklRsK5%^R=abigX0HGIDfT`K z!~0FAmdKI;Vst E07OmBI{*Lx diff --git a/packages/cornerstone-render/test/imageLoader_test.js b/packages/cornerstone-render/test/imageLoader_test.js index 3003f81542..70df4463bd 100644 --- a/packages/cornerstone-render/test/imageLoader_test.js +++ b/packages/cornerstone-render/test/imageLoader_test.js @@ -1,4 +1,5 @@ import * as cornerstone3D from '../src/index' +import * as cornerstone from '@ohif/cornerstone-render' const { registerImageLoader, @@ -9,150 +10,160 @@ const { cache, } = cornerstone3D -describe('imageLoader -- ', function () { - beforeEach(function () { - const [rows1, columns1] = [100, 100] - const scalarData1 = new Uint8Array(rows1[0] * columns1[1]) - this.image1 = { - imageId: 'image1', - getPixelData: scalarData1, - sizeInBytes: scalarData1.byteLength, - rows: rows1, - columns: columns1, - } - - this.exampleImageLoader1 = (imageId, options) => { - console.log('loading via exampleImageLoader1') - console.log(options) - - return { - promise: Promise.resolve(this.image1), - cancelFn: undefined, - } - } - - // Another image loader - const [rows2, columns2] = [200, 200] - const scalarData2 = new Uint8Array(rows2[0] * columns2[1]) - - this.image2 = { - imageId: 'image2', - getPixelData: scalarData2, - sizeInBytes: scalarData2.byteLength, - rows: rows2, - columns: columns2, - } - - this.exampleImageLoader2 = (imageId, options) => { - console.log('loading via exampleImageLoader2') - console.log(options) - - return { - promise: Promise.resolve(this.image2), - cancelFn: undefined, - } - } - - this.exampleScheme1 = 'example1' - this.exampleScheme2 = 'example2' - - this.exampleScheme1ImageId = `${this.exampleScheme1}://image1` - this.exampleScheme2ImageId = `${this.exampleScheme2}://image2` - - // this.options = {} +describe('Cache', () => { + beforeAll(() => { + // initialize the library + cornerstone.init() }) + describe('imageLoader -- ', function () { + beforeEach(function () { + const [rows1, columns1] = [100, 100] + const scalarData1 = new Uint8Array(rows1[0] * columns1[1]) + this.image1 = { + imageId: 'image1', + getPixelData: scalarData1, + sizeInBytes: scalarData1.byteLength, + rows: rows1, + columns: columns1, + } - afterEach(function () { - cache.purgeCache() - }) + this.exampleImageLoader1 = (imageId, options) => { + console.log('loading via exampleImageLoader1') + console.log(options) - describe('imageLoader registration module', function () { - afterEach(function () { - cache.purgeCache() - }) - it('allows registration of new image loader', async function () { - registerImageLoader(this.exampleScheme1, this.exampleImageLoader1) - registerImageLoader(this.exampleScheme2, this.exampleImageLoader2) + return { + promise: Promise.resolve(this.image1), + cancelFn: undefined, + } + } - await loadAndCacheImage(this.exampleScheme1ImageId, this.options) + // Another image loader + const [rows2, columns2] = [200, 200] + const scalarData2 = new Uint8Array(rows2[0] * columns2[1]) - await loadAndCacheImage(this.exampleScheme2ImageId, this.options) + this.image2 = { + imageId: 'image2', + getPixelData: scalarData2, + sizeInBytes: scalarData2.byteLength, + rows: rows2, + columns: columns2, + } - expect(cache.getImageLoadObject(this.exampleScheme1ImageId)).toBeDefined() - expect(cache.getImageLoadObject(this.exampleScheme2ImageId)).toBeDefined() - }) + this.exampleImageLoader2 = (imageId, options) => { + console.log('loading via exampleImageLoader2') + console.log(options) - it('allows registration of unknown image loader', function () { - let oldUnknownImageLoader = registerUnknownImageLoader( - this.exampleImageLoader1 - ) + return { + promise: Promise.resolve(this.image2), + cancelFn: undefined, + } + } - expect(oldUnknownImageLoader).not.toBeDefined() + this.exampleScheme1 = 'example1' + this.exampleScheme2 = 'example2' - // Check that it returns the old value for the unknown image loader - oldUnknownImageLoader = registerUnknownImageLoader( - this.exampleImageLoader1 - ) + this.exampleScheme1ImageId = `${this.exampleScheme1}://image1` + this.exampleScheme2ImageId = `${this.exampleScheme2}://image2` - expect(oldUnknownImageLoader).toBe(this.exampleImageLoader1) + // this.options = {} }) - }) - describe('imageLoader loading module', function () { afterEach(function () { cache.purgeCache() }) - it('allows loading with storage in image cache (loadImage)', async function () { - registerImageLoader(this.exampleScheme1, this.exampleImageLoader1) - const imageLoadObject = loadAndCacheImage( - this.exampleScheme1ImageId, - this.options - ) + describe('imageLoader registration module', function () { + afterEach(function () { + cache.purgeCache() + }) + it('allows registration of new image loader', async function () { + registerImageLoader(this.exampleScheme1, this.exampleImageLoader1) + registerImageLoader(this.exampleScheme2, this.exampleImageLoader2) - await expectAsync(imageLoadObject).toBeResolvedTo(this.image1) - }) + await loadAndCacheImage(this.exampleScheme1ImageId, this.options) - it('allows loading without storage in image cache (loadAndCacheImage)', async function () { - registerImageLoader(this.exampleScheme2, this.exampleImageLoader2) - const imageLoadObject = loadImage( - this.exampleScheme2ImageId, - this.options - ) + await loadAndCacheImage(this.exampleScheme2ImageId, this.options) - await expectAsync(imageLoadObject).toBeResolvedTo(this.image2) - }) + expect( + cache.getImageLoadObject(this.exampleScheme1ImageId) + ).toBeDefined() + expect( + cache.getImageLoadObject(this.exampleScheme2ImageId) + ).toBeDefined() + }) - it('falls back to the unknownImageLoader if no appropriate scheme is present', async function () { - registerImageLoader(this.exampleScheme1, this.exampleImageLoader1) - registerUnknownImageLoader(this.exampleImageLoader2) - const imageLoadObject = loadAndCacheImage( - this.exampleScheme2ImageId, - this.options - ) + it('allows registration of unknown image loader', function () { + let oldUnknownImageLoader = registerUnknownImageLoader( + this.exampleImageLoader1 + ) - await expectAsync(imageLoadObject).toBeResolvedTo(this.image2) + expect(oldUnknownImageLoader).not.toBeDefined() + + // Check that it returns the old value for the unknown image loader + oldUnknownImageLoader = registerUnknownImageLoader( + this.exampleImageLoader1 + ) + + expect(oldUnknownImageLoader).toBe(this.exampleImageLoader1) + }) }) - }) - describe('imageLoader cancelling images', function () { - afterEach(function () { - cache.purgeCache() + describe('imageLoader loading module', function () { + afterEach(function () { + cache.purgeCache() + }) + + it('allows loading with storage in image cache (loadImage)', async function () { + registerImageLoader(this.exampleScheme1, this.exampleImageLoader1) + const imageLoadObject = loadAndCacheImage( + this.exampleScheme1ImageId, + this.options + ) + + await expectAsync(imageLoadObject).toBeResolvedTo(this.image1) + }) + + it('allows loading without storage in image cache (loadAndCacheImage)', async function () { + registerImageLoader(this.exampleScheme2, this.exampleImageLoader2) + const imageLoadObject = loadImage( + this.exampleScheme2ImageId, + this.options + ) + + await expectAsync(imageLoadObject).toBeResolvedTo(this.image2) + }) + + it('falls back to the unknownImageLoader if no appropriate scheme is present', async function () { + registerImageLoader(this.exampleScheme1, this.exampleImageLoader1) + registerUnknownImageLoader(this.exampleImageLoader2) + const imageLoadObject = loadAndCacheImage( + this.exampleScheme2ImageId, + this.options + ) + + await expectAsync(imageLoadObject).toBeResolvedTo(this.image2) + }) }) - it('allows loading with storage in image cache (loadAndCacheImage)', async function () { - registerImageLoader(this.exampleScheme1, this.exampleImageLoader1) - const imageLoadObject = loadAndCacheImage( - this.exampleScheme1ImageId, - this.options - ) + describe('imageLoader cancelling images', function () { + afterEach(function () { + cache.purgeCache() + }) + + it('allows loading with storage in image cache (loadAndCacheImage)', async function () { + registerImageLoader(this.exampleScheme1, this.exampleImageLoader1) + const imageLoadObject = loadAndCacheImage( + this.exampleScheme1ImageId, + this.options + ) - await expectAsync(imageLoadObject).toBeResolvedTo(this.image1) + await expectAsync(imageLoadObject).toBeResolvedTo(this.image1) + }) }) - }) - afterEach(() => { - unregisterAllImageLoaders() - cache.purgeCache() + afterEach(() => { + unregisterAllImageLoaders() + cache.purgeCache() + }) }) }) diff --git a/packages/cornerstone-render/test/metaDataProvider_test.js b/packages/cornerstone-render/test/metaDataProvider_test.js index 765f5d97d8..856938c69a 100644 --- a/packages/cornerstone-render/test/metaDataProvider_test.js +++ b/packages/cornerstone-render/test/metaDataProvider_test.js @@ -64,6 +64,11 @@ const metadataProvider2 = (type, imageId) => { } describe('metaData Provider', function () { + beforeAll(() => { + // initialize the library + cornerstone3D.init(); + }); + beforeEach(() => { metaData.removeAllProviders() }) diff --git a/packages/cornerstone-render/test/renderingCore_stack_test.js b/packages/cornerstone-render/test/renderingCore_stack_test.js deleted file mode 100644 index cb0118c485..0000000000 --- a/packages/cornerstone-render/test/renderingCore_stack_test.js +++ /dev/null @@ -1,749 +0,0 @@ -import * as cornerstone3D from '../src/index' -import * as csTools3d from '../../cornerstone-tools/src/index' - -// nearest neighbor interpolation -import * as imageURI_64_33_20_5_1_1_0_nearest from './groundTruth/imageURI_64_33_20_5_1_1_0_nearest.png' -import * as imageURI_64_64_20_5_1_1_0_nearest from './groundTruth/imageURI_64_64_20_5_1_1_0_nearest.png' -import * as imageURI_64_64_30_10_5_5_0_nearest from './groundTruth/imageURI_64_64_30_10_5_5_0_nearest.png' -import * as imageURI_256_256_100_100_1_1_0_nearest from './groundTruth/imageURI_256_256_100_100_1_1_0_nearest.png' -import * as imageURI_256_256_100_100_1_1_0_CT_nearest from './groundTruth/imageURI_256_256_100_100_1_1_0_CT_nearest.png' -import * as imageURI_64_64_54_10_5_5_0_nearest from './groundTruth/imageURI_64_64_54_10_5_5_0_nearest.png' -import * as imageURI_64_64_0_10_5_5_0_nearest from './groundTruth/imageURI_64_64_0_10_5_5_0_nearest.png' -import * as imageURI_100_100_0_10_1_1_1_nearest_color from './groundTruth/imageURI_100_100_0_10_1_1_1_nearest_color.png' -import * as imageURI_11_11_4_1_1_1_0_nearest_invert_90deg from './groundTruth/imageURI_11_11_4_1_1_1_0_nearest_invert_90deg.png' - -// linear interpolation -import * as imageURI_11_11_4_1_1_1_0 from './groundTruth/imageURI_11_11_4_1_1_1_0.png' -import * as imageURI_256_256_50_10_1_1_0 from './groundTruth/imageURI_256_256_50_10_1_1_0.png' -import * as imageURI_100_100_0_10_1_1_1_linear_color from './groundTruth/imageURI_100_100_0_10_1_1_1_linear_color.png' -import * as calibrated_1_5_imageURI_11_11_4_1_1_1_0_1 from './groundTruth/calibrated_1_5_imageURI_11_11_4_1_1_1_0_1.png' - -import { setCTWWWC } from '../../demo/src/helpers/transferFunctionHelpers' -// import { User } from ... doesn't work right now since we don't have named exports set up -const { - Utilities: { calibrateImageSpacing }, -} = csTools3d - -const { - cache, - RenderingEngine, - VIEWPORT_TYPE, - ORIENTATION, - INTERPOLATION_TYPE, - Utilities, - registerImageLoader, - unregisterAllImageLoaders, - metaData, - EVENTS, -} = cornerstone3D - -const { calibratedPixelSpacingMetadataProvider } = Utilities - -const { fakeImageLoader, fakeMetaDataProvider, compareImages } = - Utilities.testUtils - -const renderingEngineUID = Utilities.uuidv4() - -const scene1UID = 'SCENE_1' -const viewportUID = 'VIEWPORT' - -const AXIAL = 'AXIAL' - -const DOMElements = [] - -function createCanvas(renderingEngine, orientation, width, height) { - const canvas = document.createElement('canvas') - - canvas.style.width = `${width}px` - canvas.style.height = `${height}px` - document.body.appendChild(canvas) - DOMElements.push(canvas) - - renderingEngine.setViewports([ - { - sceneUID: scene1UID, - viewportUID: viewportUID, - type: VIEWPORT_TYPE.STACK, - canvas: canvas, - defaultOptions: { - background: [1, 0, 1], // pinkish background - }, - }, - ]) - return canvas -} - -describe('Stack Viewport Nearest Neighbor Interpolation --- ', function () { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) - - afterEach(function () { - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) - }) - - it('Should render one stack viewport of square size properly: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - // imageId : imageLoaderScheme: imageURI_rows_colums_barStart_barWidth_xSpacing_ySpacing_rgbFlag - const imageId = 'fakeImageLoader:imageURI_64_64_20_5_1_1_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - imageURI_64_64_20_5_1_1_0_nearest, - 'imageURI_64_64_20_5_1_1_0_nearest' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId], 0) - vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should render one stack viewport of rectangle size properly: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId = 'fakeImageLoader:imageURI_64_33_20_5_1_1_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - imageURI_64_33_20_5_1_1_0_nearest, - 'imageURI_64_33_20_5_1_1_0_nearest' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId], 0) - vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should render one stack viewport of square size and 5mm spacing properly: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - - compareImages( - image, - imageURI_64_64_30_10_5_5_0_nearest, - 'imageURI_64_64_30_10_5_5_0_nearest' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId], 0) - vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should use enableElement API to render one stack viewport of square size and 5mm spacing properly: nearest', function (done) { - const canvas = document.createElement('canvas') - - canvas.style.width = `256px` - canvas.style.height = `256px` - document.body.appendChild(canvas) - DOMElements.push(canvas) - - // const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0' - - this.renderingEngine.enableElement({ - sceneUID: scene1UID, - viewportUID: viewportUID, - type: VIEWPORT_TYPE.STACK, - canvas: canvas, - defaultOptions: { - background: [1, 0, 1], // pinkish background - }, - }) - - const vp = this.renderingEngine.getViewport(viewportUID) - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - - compareImages( - image, - imageURI_64_64_30_10_5_5_0_nearest, - 'imageURI_64_64_30_10_5_5_0_nearest' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId], 0) - vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should render one stack viewport, first slice correctly: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_0_10_5_5_0' - const imageId2 = 'fakeImageLoader:imageURI_64_64_10_20_5_5_0' - const imageId3 = 'fakeImageLoader:imageURI_64_64_20_30_5_5_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - - compareImages( - image, - imageURI_64_64_0_10_5_5_0_nearest, - 'imageURI_64_64_0_10_5_5_0_nearest' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId1, imageId2, imageId3], 0) - vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - it('Should render one stack viewport, last slice correctly: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_0_10_5_5_0' - const imageId2 = 'fakeImageLoader:imageURI_64_64_10_20_5_5_0' - const imageId3 = 'fakeImageLoader:imageURI_64_64_54_10_5_5_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - - compareImages( - image, - imageURI_64_64_54_10_5_5_0_nearest, - 'imageURI_64_64_54_10_5_5_0_nearest' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId1, imageId2, imageId3], 2) - vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should render one stack viewport with CT presets correctly: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - - compareImages( - image, - imageURI_256_256_100_100_1_1_0_CT_nearest, - 'imageURI_256_256_100_100_1_1_0_CT_nearest' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId], 0) - vp.setProperties({ - voiRange: { lower: -160, upper: 240 }, - interpolationType: INTERPOLATION_TYPE.NEAREST, - }) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should render one stack viewport with multiple imageIds of different size and different spacing: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0' - const imageId2 = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - - compareImages( - image, - imageURI_256_256_100_100_1_1_0_nearest, - 'imageURI_256_256_100_100_1_1_0_nearest' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId1, imageId2], 0) - vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should render one stack viewport with multiple imageIds of different size and different spacing, second slice: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0' - const imageId2 = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - - compareImages( - image, - imageURI_64_64_30_10_5_5_0_nearest, - 'imageURI_64_64_30_10_5_5_0_nearest' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId1, imageId2], 1) - vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) -}) - -describe('Stack Viewport Linear Interpolation --- ', () => { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) - - afterEach(function () { - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) - }) - - it('Should render one stack viewport with linear interpolation correctly', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - imageURI_11_11_4_1_1_1_0, - 'imageURI_11_11_4_1_1_1_0' - ).then(done, done.fail) - }) - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should render one stack viewport with multiple images with linear interpolation correctly', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0' - const imageId2 = 'fakeImageLoader:imageURI_256_256_50_10_1_1_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - // downloadURI(image, 'imageURI_256_256_50_10_1_1_0') - compareImages( - image, - imageURI_256_256_50_10_1_1_0, - 'imageURI_256_256_50_10_1_1_0' - ).then(done, done.fail) - }) - try { - vp.setStack([imageId1, imageId2], 1) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) -}) - -describe('Color Stack Images', () => { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) - - afterEach(function () { - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) - }) - - it('Should render color images: linear', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 512, 512) - - // color image generation with 10 strips of different colors - const imageId1 = 'fakeImageLoader:imageURI_100_100_0_10_1_1_1' - const vp = this.renderingEngine.getViewport(viewportUID) - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - // downloadURI(image, 'imageURI_100_100_0_10_1_1_1_linear_color') - compareImages( - image, - imageURI_100_100_0_10_1_1_1_linear_color, - 'imageURI_100_100_0_10_1_1_1_linear_color' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should render color images: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 512, 512) - - // color image generation with 10 strips of different colors - const imageId1 = 'fakeImageLoader:imageURI_100_100_0_10_1_1_1' - const vp = this.renderingEngine.getViewport(viewportUID) - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - // downloadURI(image, 'imageURI_100_100_0_10_1_1_1_nearest_color') - compareImages( - image, - imageURI_100_100_0_10_1_1_1_nearest_color, - 'imageURI_100_100_0_10_1_1_1_nearest_color' - ).then(done, done.fail) - }) - - try { - vp.setStack([imageId1], 0) - vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) -}) - -describe('Stack Viewport Calibration and Scaling --- ', () => { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - metaData.addProvider( - calibratedPixelSpacingMetadataProvider.get.bind( - calibratedPixelSpacingMetadataProvider - ), - 11000 - ) - }) - - afterEach(function () { - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) - }) - - it('Should be able to render a stack viewport with PET modality scaling', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0_1' - - const vp = this.renderingEngine.getViewport(viewportUID) - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - expect(vp.scaling.PET).toEqual({ - suvbwToSuvlbm: 1, - suvbwToSuvbsa: 1, - }) - done() - }) - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should be able to calibrate the pixel spacing', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - canvas.addEventListener(EVENTS.IMAGE_RENDERED, (evt) => { - calibratedPixelSpacingMetadataProvider.add(imageId1, [2, 2]) - - // Use the second image for calibration. It will decoded into 2x2 pixel spacing - vp.calibrateSpacing(imageId1) - }) - - canvas.addEventListener(EVENTS.IMAGE_SPACING_CALIBRATED, (evt) => { - const { rowScale, columnScale } = evt.detail - expect(rowScale).toBe(2) - expect(columnScale).toBe(2) - done() - }) - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) -}) - -describe('Stack Viewport setProperties API --- ', () => { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - metaData.addProvider( - calibratedPixelSpacingMetadataProvider.get.bind( - calibratedPixelSpacingMetadataProvider - ), - 11000 - ) - }) - - it('Should be able to use setPropertise API', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0' - - const vp = this.renderingEngine.getViewport(viewportUID) - - const subscribeToImageRendered = () => { - canvas.addEventListener(EVENTS.IMAGE_RENDERED, (evt) => { - const image = canvas.toDataURL('image/png') - - let props = vp.getProperties() - expect(props.rotation).toBe(90) - expect(props.interpolationType).toBe(INTERPOLATION_TYPE.NEAREST) - expect(props.invert).toBe(true) - - compareImages( - image, - imageURI_11_11_4_1_1_1_0_nearest_invert_90deg, - 'imageURI_11_11_4_1_1_1_0_nearest_invert_90deg' - ).then(done, done.fail) - }) - } - - try { - vp.setStack([imageId1], 0) - subscribeToImageRendered() - vp.setProperties({ - interpolationType: INTERPOLATION_TYPE.NEAREST, - invert: true, - rotation: 90, - }) - - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - // it('Should be able to resetProperties API', function (done) { - // const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - // const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0' - - // const vp = this.renderingEngine.getViewport(viewportUID) - // canvas.addEventListener(EVENTS.IMAGE_RENDERED, (evt) => { - // let props = vp.getProperties() - // expect(props.rotation).toBe(90) - // expect(props.interpolationType).toBe(INTERPOLATION_TYPE.NEAREST) - // expect(props.invert).toBe(true) - - // vp.resetProperties() - // props = vp.getProperties() - // expect(props.rotation).toBe(0) - // expect(props.interpolationType).toBe(INTERPOLATION_TYPE.LINEAR) - // expect(props.invert).toBe(false) - // done() - // }) - // try { - // vp.setStack([imageId1], 0) - // vp.setProperties({ - // interpolationType: INTERPOLATION_TYPE.NEAREST, - // invert: true, - // rotation: 90, - // }) - - // this.renderingEngine.render() - // } catch (e) { - // done.fail(e) - // } - // }) -}) - -describe('Calibration ', () => { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - metaData.addProvider( - calibratedPixelSpacingMetadataProvider.get.bind( - calibratedPixelSpacingMetadataProvider - ), - 11000 - ) - }) - - afterEach(function () { - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) - }) - - it('Should be able to calibrate an image', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0_1' - - const vp = this.renderingEngine.getViewport(viewportUID) - - const firstCallback = () => { - console.debug('Render Callback') - canvas.removeEventListener(EVENTS.IMAGE_RENDERED, firstCallback) - canvas.addEventListener(EVENTS.IMAGE_RENDERED, secondCallback) - const imageId = this.renderingEngine - .getViewport(viewportUID) - .getCurrentImageId() - - calibrateImageSpacing(imageId, this.renderingEngine, 1, 5) - } - const secondCallback = () => { - const image = canvas.toDataURL('image/png') - console.debug('Calibrate Image callback') - compareImages( - image, - calibrated_1_5_imageURI_11_11_4_1_1_1_0_1, - 'calibrated_1_5_imageURI_11_11_4_1_1_1_0_1' - ).then(done, done.fail) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, firstCallback) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should be able to fire imageCalibrated event with expected data', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) - - const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0_1' - - const vp = this.renderingEngine.getViewport(viewportUID) - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const imageId = this.renderingEngine - .getViewport(viewportUID) - .getCurrentImageId() - - calibrateImageSpacing(imageId, this.renderingEngine, 1, 5) - }) - canvas.addEventListener(EVENTS.IMAGE_SPACING_CALIBRATED, (evt) => { - expect(evt.detail).toBeDefined() - expect(evt.detail.rowScale).toBe(1) - expect(evt.detail.columnScale).toBe(5) - expect(evt.detail.viewportUID).toBe(viewportUID) - done() - }) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) -}) diff --git a/packages/cornerstone-render/test/renderingCore_volume_test.js b/packages/cornerstone-render/test/renderingCore_volume_test.js deleted file mode 100644 index a310c66483..0000000000 --- a/packages/cornerstone-render/test/renderingCore_volume_test.js +++ /dev/null @@ -1,639 +0,0 @@ -import * as cornerstone3D from '../src/index' -// import { User } from ... doesn't work right now since we don't have named exports set up - -// nearest neighbor interpolation -import * as volumeURI_100_100_10_1_1_1_0_axial_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_axial_nearest.png' -import * as volumeURI_100_100_10_1_1_1_0_sagittal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_sagittal_nearest.png' -import * as volumeURI_100_100_10_1_1_1_0_coronal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_coronal_nearest.png' -import * as volumeURI_100_100_10_1_1_1_1_color_coronal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_1_color_coronal_nearest.png' - -// linear interpolation -import * as volumeURI_100_100_10_1_1_1_0_axial_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_axial_linear.png' -import * as volumeURI_100_100_10_1_1_1_0_sagittal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_sagittal_linear.png' -import * as volumeURI_100_100_10_1_1_1_0_coronal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_coronal_linear.png' -import * as volumeURI_100_100_10_1_1_1_1_color_coronal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_1_color_coronal_linear.png' - -const { - cache, - RenderingEngine, - VIEWPORT_TYPE, - ORIENTATION, - unregisterAllImageLoaders, - metaData, - EVENTS, - registerVolumeLoader, - createAndCacheVolume, - Utilities, -} = cornerstone3D - -const { fakeMetaDataProvider, compareImages, fakeVolumeLoader } = - Utilities.testUtils - -const renderingEngineUID = Utilities.uuidv4() - -const scene1UID = 'SCENE_1' -const viewportUID = 'VIEWPORT' - -const AXIAL = 'AXIAL' -const SAGITTAL = 'SAGITTAL' -const CORONAL = 'CORONAL' - -const DOMElements = [] - -function createCanvas(renderingEngine, orientation) { - const canvasAxial = document.createElement('canvas') - - canvasAxial.style.width = '1000px' - canvasAxial.style.height = '1000px' - document.body.appendChild(canvasAxial) - DOMElements.push(canvasAxial) - - renderingEngine.setViewports([ - { - sceneUID: scene1UID, - viewportUID: viewportUID, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvasAxial, - defaultOptions: { - orientation: ORIENTATION[orientation], - background: [1, 0, 1], // pinkish background - }, - }, - ]) - return canvasAxial -} - -describe('Volume Viewport Axial Nearest Neighbor and Linear Interpolation --- ', function () { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - - metaData.addProvider(fakeMetaDataProvider, 10000) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - }) - - afterEach(function () { - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) - }) - - it('should successfully load a volume: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL) - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_0_axial_nearest, - 'volumeURI_100_100_10_1_1_1_0_axial_nearest' - ).then(done, done.fail) - }) - - const callback = ({ volumeActor }) => - volumeActor.getProperty().setInterpolationTypeToNearest() - - try { - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId, callback }]) - ctScene.render() - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) - - it('should successfully load a volume: linear', function (done) { - const canvas = createCanvas(this.renderingEngine, AXIAL) - - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_0_axial_linear, - 'volumeURI_100_100_10_1_1_1_0_axial_linear' - ).then(done, done.fail) - }) - - try { - createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId }]) - ctScene.render() - }) - } catch (e) { - done.fail(e) - } - }) -}) - -describe('Volume Viewport Sagittal Nearest Neighbor and Linear Interpolation --- ', function () { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - - metaData.addProvider(fakeMetaDataProvider, 10000) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - }) - - afterEach(function () { - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) - }) - - it('should successfully load a volume: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, SAGITTAL) - - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_0_sagittal_nearest, - 'volumeURI_100_100_10_1_1_1_0_sagittal_nearest' - ).then(done, done.fail) - }) - - const callback = ({ volumeActor }) => - volumeActor.getProperty().setInterpolationTypeToNearest() - - try { - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId, callback }]) - ctScene.render() - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) - - it('should successfully load a volume: linear', function (done) { - const canvas = createCanvas(this.renderingEngine, SAGITTAL) - - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_0_sagittal_linear, - 'volumeURI_100_100_10_1_1_1_0_sagittal_linear' - ).then(done, done.fail) - }) - - try { - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId }]) - ctScene.render() - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) -}) - -describe('Volume Viewport Sagittal Coronal Neighbor and Linear Interpolation --- ', function () { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - - metaData.addProvider(fakeMetaDataProvider, 10000) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - }) - - afterEach(function () { - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) - }) - - it('should successfully load a volume: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, CORONAL) - - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' - ).then(done, done.fail) - }) - - const callback = ({ volumeActor }) => - volumeActor.getProperty().setInterpolationTypeToNearest() - - try { - // we don't set imageIds as we are mocking the imageVolume to - // return the volume immediately - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId, callback }]) - ctScene.render() - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) - - it('should successfully load a volume: linear', function (done) { - const canvas = createCanvas(this.renderingEngine, CORONAL) - - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_linear, - 'volumeURI_100_100_10_1_1_1_0_coronal_linear' - ).then(done, done.fail) - }) - - try { - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId }]) - ctScene.render() - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) -}) - -describe('Rendering Scenes API', function () { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - - metaData.addProvider(fakeMetaDataProvider, 10000) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - }) - - afterEach(function () { - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) - }) - - it('should successfully use renderScenes API to load image', function (done) { - const canvas = createCanvas(this.renderingEngine, CORONAL) - - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' - ).then(done, done.fail) - }) - - const callback = ({ volumeActor }) => - volumeActor.getProperty().setInterpolationTypeToNearest() - - try { - // we don't set imageIds as we are mocking the imageVolume to - // return the volume immediately - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - // const scenes = this.renderingEngine.getScenes() - ctScene.setVolumes([{ volumeUID: volumeId, callback }]) - this.renderingEngine.renderScenes([scene1UID]) - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) - - it('Should be able to filter viewports based on volumeUID', function (done) { - const canvas = createCanvas(this.renderingEngine, CORONAL) - - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const viewport = this.renderingEngine.getViewport(viewportUID) - const viewports = - this.renderingEngine.getViewportsContainingVolumeUID(volumeId) - - expect(viewports.length).toBe(1) - expect(viewports[0]).toBe(viewport) - - const scenes = this.renderingEngine.getScenesContainingVolume(volumeId) - const sceneViewport = scenes[0].getViewports()[0] - expect(scenes.length).toBe(1) - expect(sceneViewport).toBe(viewport) - done() - }) - - const callback = ({ volumeActor }) => - volumeActor.getProperty().setInterpolationTypeToNearest() - - try { - // we don't set imageIds as we are mocking the imageVolume to - // return the volume immediately - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId, callback }]) - this.renderingEngine.renderScenes([scene1UID]) - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) - - it('should successfully use renderViewports API to load image', function (done) { - const canvas = createCanvas(this.renderingEngine, CORONAL) - - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' - ).then(done, done.fail) - }) - - const callback = ({ volumeActor }) => - volumeActor.getProperty().setInterpolationTypeToNearest() - - try { - // we don't set imageIds as we are mocking the imageVolume to - // return the volume immediately - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - // const scenes = this.renderingEngine.getScenes() - ctScene.setVolumes([{ volumeUID: volumeId, callback }]) - this.renderingEngine.renderViewports([viewportUID]) - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) - - it('should successfully use renderViewport API to load image', function (done) { - const canvas = createCanvas(this.renderingEngine, CORONAL) - - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' - ).then(done, done.fail) - }) - - const callback = ({ volumeActor }) => - volumeActor.getProperty().setInterpolationTypeToNearest() - - try { - // we don't set imageIds as we are mocking the imageVolume to - // return the volume immediately - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - // const scenes = this.renderingEngine.getScenes() - ctScene.setVolumes([{ volumeUID: volumeId, callback }]) - this.renderingEngine.renderViewport(viewportUID) - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) - - it('should successfully debug the offscreen canvas', function (done) { - const canvas = createCanvas(this.renderingEngine, CORONAL) - - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - const offScreen = this.renderingEngine._debugRender() - expect(offScreen).toEqual(image) - done() - }) - - const callback = ({ volumeActor }) => - volumeActor.getProperty().setInterpolationTypeToNearest() - - try { - // we don't set imageIds as we are mocking the imageVolume to - // return the volume immediately - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - // const scenes = this.renderingEngine.getScenes() - ctScene.setVolumes([{ volumeUID: volumeId, callback }]) - this.renderingEngine.renderViewport(viewportUID) - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) - - it('should successfully render frameOfReference', function (done) { - const canvas = createCanvas(this.renderingEngine, CORONAL) - - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' - ).then(done, done.fail) - }) - - const callback = ({ volumeActor }) => - volumeActor.getProperty().setInterpolationTypeToNearest() - - try { - // we don't set imageIds as we are mocking the imageVolume to - // return the volume immediately - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - // const scenes = this.renderingEngine.getScenes() - ctScene.setVolumes([{ volumeUID: volumeId, callback }]).then(() => { - this.renderingEngine.renderFrameOfReference( - 'Volume_Frame_Of_Reference' - ) - }) - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) -}) - -describe('Volume Viewport Color images Neighbor and Linear Interpolation --- ', function () { - beforeEach(function () { - cache.purgeCache() - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - - metaData.addProvider(fakeMetaDataProvider, 10000) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - }) - - afterEach(function () { - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) - }) - - it('should successfully load a color volume: nearest', function (done) { - const canvas = createCanvas(this.renderingEngine, CORONAL) - - // fake volume generator follows the pattern of - // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_1' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_1_color_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_1_color_coronal_nearest' - ).then(done, done.fail) - }) - - const callback = ({ volumeActor }) => { - volumeActor.getProperty().setIndependentComponents(false) - volumeActor.getProperty().setInterpolationTypeToNearest() - } - - try { - // we don't set imageIds as we are mocking the imageVolume to - // return the volume immediately - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId, callback }]) - ctScene.render() - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) - - it('should successfully load a volume: linear', function (done) { - const canvas = createCanvas(this.renderingEngine, CORONAL) - - const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_1' - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image = canvas.toDataURL('image/png') - compareImages( - image, - volumeURI_100_100_10_1_1_1_1_color_coronal_linear, - 'volumeURI_100_100_10_1_1_1_1_color_coronal_linear' - ).then(done, done.fail) - }) - - const callback = ({ volumeActor }) => { - volumeActor.getProperty().setIndependentComponents(false) - volumeActor.getProperty().setInterpolationTypeToLinear() - } - - try { - createAndCacheVolume(volumeId, { imageIds: [] }) - .then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId, callback }]) - ctScene.render() - }) - .catch((e) => done(e)) - } catch (e) { - done.fail(e) - } - }) -}) diff --git a/packages/cornerstone-render/test/stackViewport_cpu_render_test.js b/packages/cornerstone-render/test/stackViewport_cpu_render_test.js new file mode 100644 index 0000000000..934ec94084 --- /dev/null +++ b/packages/cornerstone-render/test/stackViewport_cpu_render_test.js @@ -0,0 +1,457 @@ +import * as cornerstone3D from '../src/index'; + +import * as cpu_imageURI_64_64_20_5_1_1_0 from './groundTruth/cpu_imageURI_64_64_20_5_1_1_0.png'; +import * as cpu_imageURI_64_33_20_5_1_1_0 from './groundTruth/cpu_imageURI_64_33_20_5_1_1_0.png'; +import * as cpu_imageURI_64_64_30_10_5_5_0 from './groundTruth/cpu_imageURI_64_64_30_10_5_5_0.png'; +import * as cpu_imageURI_64_64_0_10_5_5_0 from './groundTruth/cpu_imageURI_64_64_0_10_5_5_0.png'; +import * as cpu_imageURI_64_64_54_10_5_5_0 from './groundTruth/cpu_imageURI_64_64_54_10_5_5_0.png'; +import * as cpu_imageURI_256_256_100_100_1_1_0_voi from './groundTruth/cpu_imageURI_256_256_100_100_1_1_0_voi.png'; +import * as cpu_imageURI_256_256_100_100_1_1_0 from './groundTruth/cpu_imageURI_256_256_100_100_1_1_0.png'; +import * as cpu_imageURI_256_256_50_10_1_1_0 from './groundTruth/cpu_imageURI_256_256_50_10_1_1_0.png'; +import * as cpu_imageURI_256_256_50_10_1_1_0_invert from './groundTruth/cpu_imageURI_256_256_50_10_1_1_0_invert.png'; +import * as cpu_imageURI_256_256_50_10_1_1_0_rotate from './groundTruth/cpu_imageURI_256_256_50_10_1_1_0_rotate.png'; +import * as cpu_imageURI_256_256_100_100_1_1_0_hotIron from './groundTruth/cpu_imageURI_256_256_100_100_1_1_0_hotIron.png'; + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + Utilities, + registerImageLoader, + unregisterAllImageLoaders, + metaData, + EVENTS, + init, + setUseCPURenderingOnlyForDebugOrTests, + resetCPURenderingOnlyForDebugOrTests, + cpuColormaps, +} = cornerstone3D; + +const { fakeImageLoader, fakeMetaDataProvider, compareImages } = + Utilities.testUtils; + +const renderingEngineUID = Utilities.uuidv4(); + +const viewportUID = 'VIEWPORT'; +const AXIAL = 'AXIAL'; + +const DOMElements = []; + +function createCanvas(renderingEngine, orientation, width, height) { + const canvas = document.createElement('canvas'); + + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + document.body.appendChild(canvas); + DOMElements.push(canvas); + + renderingEngine.setViewports([ + { + viewportUID: viewportUID, + type: VIEWPORT_TYPE.STACK, + canvas: canvas, + defaultOptions: { + background: [1, 0, 1], // pinkish background + }, + }, + ]); + return canvas; +} + +describe('StackViewport CPU -- ', () => { + beforeEach(() => { + setUseCPURenderingOnlyForDebugOrTests(true); + }); + + afterEach(() => { + resetCPURenderingOnlyForDebugOrTests(); + }); + + describe('Basic Rendering --- ', function () { + beforeEach(function () { + cache.purgeCache(); + + this.renderingEngine = new RenderingEngine(renderingEngineUID); + registerImageLoader('fakeImageLoader', fakeImageLoader); + metaData.addProvider(fakeMetaDataProvider, 10000); + }); + + afterEach(function () { + cache.purgeCache(); + this.renderingEngine.destroy(); + metaData.removeProvider(fakeMetaDataProvider); + unregisterAllImageLoaders(); + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + }); + + it('Should render one cpu stack viewport of square size properly', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 716, 646); + + // imageId : imageLoaderScheme: imageURI_rows_colums_barStart_barWidth_xSpacing_ySpacing_rgbFlag + const imageId = 'fakeImageLoader:imageURI_64_64_20_5_1_1_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + compareImages( + image, + cpu_imageURI_64_64_20_5_1_1_0, + 'cpu_imageURI_64_64_20_5_1_1_0' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport of rectangle size properly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256); + + const imageId = 'fakeImageLoader:imageURI_64_33_20_5_1_1_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + compareImages( + image, + cpu_imageURI_64_33_20_5_1_1_0, + 'cpu_imageURI_64_33_20_5_1_1_0' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport of square size and 5mm spacing properly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256); + + const imageId = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + cpu_imageURI_64_64_30_10_5_5_0, + 'cpu_imageURI_64_64_30_10_5_5_0' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0); + // vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should use enableElement API to render one cpu stack viewport of square size and 5mm spacing properly: nearest', function (done) { + const canvas = document.createElement('canvas'); + + canvas.style.width = `256px`; + canvas.style.height = `256px`; + document.body.appendChild(canvas); + DOMElements.push(canvas); + + // const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0'; + + this.renderingEngine.enableElement({ + viewportUID: viewportUID, + type: VIEWPORT_TYPE.STACK, + canvas: canvas, + defaultOptions: { + background: [1, 0, 1], // pinkish background + }, + }); + + const vp = this.renderingEngine.getViewport(viewportUID); + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + cpu_imageURI_64_64_30_10_5_5_0, + 'cpu_imageURI_64_64_30_10_5_5_0' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport, first slice correctly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_0_10_5_5_0'; + const imageId2 = 'fakeImageLoader:imageURI_64_64_10_20_5_5_0'; + const imageId3 = 'fakeImageLoader:imageURI_64_64_20_30_5_5_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + cpu_imageURI_64_64_0_10_5_5_0, + 'cpu_imageURI_64_64_0_10_5_5_0' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId1, imageId2, imageId3], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport, last slice correctly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_0_10_5_5_0'; + const imageId2 = 'fakeImageLoader:imageURI_64_64_10_20_5_5_0'; + const imageId3 = 'fakeImageLoader:imageURI_64_64_54_10_5_5_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + cpu_imageURI_64_64_54_10_5_5_0, + 'cpu_imageURI_64_64_54_10_5_5_0' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId1, imageId2, imageId3], 2); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('setProperties cpu', function () { + beforeEach(function () { + cache.purgeCache(); + + this.renderingEngine = new RenderingEngine(renderingEngineUID); + registerImageLoader('fakeImageLoader', fakeImageLoader); + metaData.addProvider(fakeMetaDataProvider, 10000); + }); + + afterEach(function () { + cache.purgeCache(); + this.renderingEngine.destroy(); + metaData.removeProvider(fakeMetaDataProvider); + unregisterAllImageLoaders(); + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + }); + + it('Should render one cpu stack viewport with voi presets correctly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256); + + const imageId = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + cpu_imageURI_256_256_100_100_1_1_0_voi, + 'cpu_imageURI_256_256_100_100_1_1_0_voi' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ + voiRange: { lower: 0, upper: 440 }, + }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport with multiple imageIds of different size and different spacing: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256); + + const imageId1 = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0'; + const imageId2 = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + cpu_imageURI_256_256_100_100_1_1_0, + 'cpu_imageURI_256_256_100_100_1_1_0' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId1, imageId2], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport with multiple images with linear interpolation correctly', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256); + + const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0'; + const imageId2 = 'fakeImageLoader:imageURI_256_256_50_10_1_1_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + compareImages( + image, + cpu_imageURI_256_256_50_10_1_1_0, + 'cpu_imageURI_256_256_50_10_1_1_0' + ).then(done, done.fail); + }); + try { + vp.setStack([imageId1, imageId2], 1); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport with invert', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_20_5_1_1_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + compareImages( + image, + cpu_imageURI_256_256_50_10_1_1_0_invert, + 'cpu_imageURI_256_256_50_10_1_1_0_invert' + ).then(done, done.fail); + }); + try { + vp.setStack([imageId1], 0).then(() => { + vp.setProperties({ invert: true }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport with rotation', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_20_5_1_1_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + compareImages( + image, + cpu_imageURI_256_256_50_10_1_1_0_rotate, + 'cpu_imageURI_256_256_50_10_1_1_0_rotate' + ).then(done, done.fail); + }); + try { + vp.setStack([imageId1], 0).then(() => { + vp.setProperties({ rotation: 90 }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('false colormap cpu', function () { + beforeEach(function () { + cache.purgeCache(); + + this.renderingEngine = new RenderingEngine(renderingEngineUID); + registerImageLoader('fakeImageLoader', fakeImageLoader); + metaData.addProvider(fakeMetaDataProvider, 10000); + }); + + afterEach(function () { + cache.purgeCache(); + this.renderingEngine.destroy(); + metaData.removeProvider(fakeMetaDataProvider); + unregisterAllImageLoaders(); + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + }); + + it('Should render one cpu stack viewport with presets correctly', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256); + + const imageId = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0'; + + const vp = this.renderingEngine.getViewport(viewportUID); + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + cpu_imageURI_256_256_100_100_1_1_0_hotIron, + 'cpu_imageURI_256_256_100_100_1_1_0_hotIron' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setColormap(cpuColormaps.hotIron); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + }); +}); diff --git a/packages/cornerstone-render/test/stackViewport_gpu_render_test.js b/packages/cornerstone-render/test/stackViewport_gpu_render_test.js new file mode 100644 index 0000000000..ac4fe833f0 --- /dev/null +++ b/packages/cornerstone-render/test/stackViewport_gpu_render_test.js @@ -0,0 +1,927 @@ +import * as cornerstone3D from '../src/index' +import * as csTools3d from '../../cornerstone-tools/src/index' + +// nearest neighbor interpolation +import * as imageURI_64_33_20_5_1_1_0_nearest from './groundTruth/imageURI_64_33_20_5_1_1_0_nearest.png' +import * as imageURI_64_64_20_5_1_1_0_nearest from './groundTruth/imageURI_64_64_20_5_1_1_0_nearest.png' +import * as imageURI_64_64_30_10_5_5_0_nearest from './groundTruth/imageURI_64_64_30_10_5_5_0_nearest.png' +import * as imageURI_256_256_100_100_1_1_0_nearest from './groundTruth/imageURI_256_256_100_100_1_1_0_nearest.png' +import * as imageURI_256_256_100_100_1_1_0_CT_nearest from './groundTruth/imageURI_256_256_100_100_1_1_0_CT_nearest.png' +import * as imageURI_64_64_54_10_5_5_0_nearest from './groundTruth/imageURI_64_64_54_10_5_5_0_nearest.png' +import * as imageURI_64_64_0_10_5_5_0_nearest from './groundTruth/imageURI_64_64_0_10_5_5_0_nearest.png' +import * as imageURI_100_100_0_10_1_1_1_nearest_color from './groundTruth/imageURI_100_100_0_10_1_1_1_nearest_color.png' +import * as imageURI_11_11_4_1_1_1_0_nearest_invert_90deg from './groundTruth/imageURI_11_11_4_1_1_1_0_nearest_invert_90deg.png' +import * as imageURI_64_64_20_5_1_1_0_nearestFlipH from './groundTruth/imageURI_64_64_20_5_1_1_0_nearestFlipH.png' +import * as imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90 from './groundTruth/imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90.png' + +// linear interpolation +import * as imageURI_11_11_4_1_1_1_0 from './groundTruth/imageURI_11_11_4_1_1_1_0.png' +import * as imageURI_256_256_50_10_1_1_0 from './groundTruth/imageURI_256_256_50_10_1_1_0.png' +import * as imageURI_100_100_0_10_1_1_1_linear_color from './groundTruth/imageURI_100_100_0_10_1_1_1_linear_color.png' +import * as calibrated_1_5_imageURI_11_11_4_1_1_1_0_1 from './groundTruth/calibrated_1_5_imageURI_11_11_4_1_1_1_0_1.png' + +// import { User } from ... doesn't work right now since we don't have named exports set up +const { + Utilities: { calibrateImageSpacing }, +} = csTools3d + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + INTERPOLATION_TYPE, + Utilities, + registerImageLoader, + unregisterAllImageLoaders, + metaData, + EVENTS, +} = cornerstone3D + +const { calibratedPixelSpacingMetadataProvider } = Utilities + +const { fakeImageLoader, fakeMetaDataProvider, compareImages } = + Utilities.testUtils + +const renderingEngineUID = Utilities.uuidv4() + +const scene1UID = 'SCENE_1' +const viewportUID = 'VIEWPORT' + +const AXIAL = 'AXIAL' + +const DOMElements = [] + +function createCanvas(renderingEngine, orientation, width, height) { + const canvas = document.createElement('canvas') + + canvas.style.width = `${width}px` + canvas.style.height = `${height}px` + document.body.appendChild(canvas) + DOMElements.push(canvas) + + renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: viewportUID, + type: VIEWPORT_TYPE.STACK, + canvas: canvas, + defaultOptions: { + background: [1, 0, 1], // pinkish background + }, + }, + ]) + return canvas +} + +describe('renderingCore -- Stack', () => { + beforeAll(() => { + // initialize cornerstone + cornerstone3D.init() + }) + describe('Stack Viewport Nearest Neighbor Interpolation --- ', function () { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('Should render one stack viewport of square size properly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + // imageId : imageLoaderScheme: imageURI_rows_colums_barStart_barWidth_xSpacing_ySpacing_rgbFlag + const imageId = 'fakeImageLoader:imageURI_64_64_20_5_1_1_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + imageURI_64_64_20_5_1_1_0_nearest, + 'imageURI_64_64_20_5_1_1_0_nearest' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should render one stack viewport of rectangle size properly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId = 'fakeImageLoader:imageURI_64_33_20_5_1_1_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + imageURI_64_33_20_5_1_1_0_nearest, + 'imageURI_64_33_20_5_1_1_0_nearest' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should render one stack viewport of square size and 5mm spacing properly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + + compareImages( + image, + imageURI_64_64_30_10_5_5_0_nearest, + 'imageURI_64_64_30_10_5_5_0_nearest' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should use enableElement API to render one stack viewport of square size and 5mm spacing properly: nearest', function (done) { + const canvas = document.createElement('canvas') + + canvas.style.width = `256px` + canvas.style.height = `256px` + document.body.appendChild(canvas) + DOMElements.push(canvas) + + // const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0' + + this.renderingEngine.enableElement({ + sceneUID: scene1UID, + viewportUID: viewportUID, + type: VIEWPORT_TYPE.STACK, + canvas: canvas, + defaultOptions: { + background: [1, 0, 1], // pinkish background + }, + }) + + const vp = this.renderingEngine.getViewport(viewportUID) + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + + compareImages( + image, + imageURI_64_64_30_10_5_5_0_nearest, + 'imageURI_64_64_30_10_5_5_0_nearest' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should render one stack viewport, first slice correctly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_64_64_0_10_5_5_0' + const imageId2 = 'fakeImageLoader:imageURI_64_64_10_20_5_5_0' + const imageId3 = 'fakeImageLoader:imageURI_64_64_20_30_5_5_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + + compareImages( + image, + imageURI_64_64_0_10_5_5_0_nearest, + 'imageURI_64_64_0_10_5_5_0_nearest' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId1, imageId2, imageId3], 0).then(() => { + vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should render one stack viewport, last slice correctly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_64_64_0_10_5_5_0' + const imageId2 = 'fakeImageLoader:imageURI_64_64_10_20_5_5_0' + const imageId3 = 'fakeImageLoader:imageURI_64_64_54_10_5_5_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + + compareImages( + image, + imageURI_64_64_54_10_5_5_0_nearest, + 'imageURI_64_64_54_10_5_5_0_nearest' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId1, imageId2, imageId3], 2).then(() => { + vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should render one stack viewport with CT presets correctly: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + + compareImages( + image, + imageURI_256_256_100_100_1_1_0_CT_nearest, + 'imageURI_256_256_100_100_1_1_0_CT_nearest' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ + voiRange: { lower: -160, upper: 240 }, + interpolationType: INTERPOLATION_TYPE.NEAREST, + }) + }) + + vp.render() + } catch (e) { + done.fail(e) + } + }) + + it('Should render one stack viewport with multiple imageIds of different size and different spacing: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0' + const imageId2 = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + + compareImages( + image, + imageURI_256_256_100_100_1_1_0_nearest, + 'imageURI_256_256_100_100_1_1_0_nearest' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId1, imageId2], 0).then(() => { + vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should render one stack viewport with multiple imageIds of different size and different spacing, second slice: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0' + const imageId2 = 'fakeImageLoader:imageURI_64_64_30_10_5_5_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + + compareImages( + image, + imageURI_64_64_30_10_5_5_0_nearest, + 'imageURI_64_64_30_10_5_5_0_nearest' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId1, imageId2], 1).then(() => { + vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Stack Viewport Linear Interpolation --- ', () => { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('Should render one stack viewport with linear interpolation correctly', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + imageURI_11_11_4_1_1_1_0, + 'imageURI_11_11_4_1_1_1_0' + ).then(done, done.fail) + }) + try { + vp.setStack([imageId1], 0).then(() => { + vp.setProperties({ voiRange: { lower: -160, upper: 240 } }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should render one stack viewport with multiple images with linear interpolation correctly', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0' + const imageId2 = 'fakeImageLoader:imageURI_256_256_50_10_1_1_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + // downloadURI(image, 'imageURI_256_256_50_10_1_1_0') + compareImages( + image, + imageURI_256_256_50_10_1_1_0, + 'imageURI_256_256_50_10_1_1_0' + ).then(done, done.fail) + }) + try { + vp.setStack([imageId1, imageId2], 1).then(() => { + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Color Stack Images', () => { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('Should render color images: linear', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 512, 512) + + // color image generation with 10 strips of different colors + const imageId1 = 'fakeImageLoader:imageURI_100_100_0_10_1_1_1' + const vp = this.renderingEngine.getViewport(viewportUID) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + // downloadURI(image, 'imageURI_100_100_0_10_1_1_1_linear_color') + compareImages( + image, + imageURI_100_100_0_10_1_1_1_linear_color, + 'imageURI_100_100_0_10_1_1_1_linear_color' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId1], 0).then(() => { + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should render color images: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 512, 512) + + // color image generation with 10 strips of different colors + const imageId1 = 'fakeImageLoader:imageURI_100_100_0_10_1_1_1' + const vp = this.renderingEngine.getViewport(viewportUID) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + // downloadURI(image, 'imageURI_100_100_0_10_1_1_1_nearest_color') + compareImages( + image, + imageURI_100_100_0_10_1_1_1_nearest_color, + 'imageURI_100_100_0_10_1_1_1_nearest_color' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId1], 0).then(() => { + vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Stack Viewport Calibration and Scaling --- ', () => { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + metaData.addProvider( + calibratedPixelSpacingMetadataProvider.get.bind( + calibratedPixelSpacingMetadataProvider + ), + 11000 + ) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('Should be able to render a stack viewport with PET modality scaling', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0_1' + + const vp = this.renderingEngine.getViewport(viewportUID) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + expect(vp.scaling.PET).toEqual({ + suvbwToSuvlbm: 1, + suvbwToSuvbsa: 1, + }) + done() + }) + try { + vp.setStack([imageId1], 0) + vp.render() + } catch (e) { + done.fail(e) + } + }) + + it('Should be able to calibrate the pixel spacing', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + + const imageRenderedCallback = () => { + calibratedPixelSpacingMetadataProvider.add(imageId1, [2, 2]) + + vp.calibrateSpacing(imageId1) + canvas.removeEventListener(EVENTS.IMAGE_RENDERED, imageRenderedCallback) + canvas.addEventListener( + EVENTS.IMAGE_RENDERED, + secondImageRenderedCallbackAfterCalibration + ) + } + + const secondImageRenderedCallbackAfterCalibration = () => { + done() + } + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, imageRenderedCallback) + + canvas.addEventListener(EVENTS.IMAGE_SPACING_CALIBRATED, (evt) => { + const { rowScale, columnScale } = evt.detail + expect(rowScale).toBe(2) + expect(columnScale).toBe(2) + }) + + try { + vp.setStack([imageId1], 0) + vp.render() + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Stack Viewport setProperties API --- ', () => { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + metaData.addProvider( + calibratedPixelSpacingMetadataProvider.get.bind( + calibratedPixelSpacingMetadataProvider + ), + 11000 + ) + }) + + it('Should be able to use setPropertise API', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + + const subscribeToImageRendered = () => { + canvas.addEventListener(EVENTS.IMAGE_RENDERED, (evt) => { + const image = canvas.toDataURL('image/png') + + let props = vp.getProperties() + expect(props.rotation).toBe(90) + expect(props.interpolationType).toBe(INTERPOLATION_TYPE.NEAREST) + expect(props.invert).toBe(true) + + compareImages( + image, + imageURI_11_11_4_1_1_1_0_nearest_invert_90deg, + 'imageURI_11_11_4_1_1_1_0_nearest_invert_90deg' + ).then(done, done.fail) + }) + } + + try { + vp.setStack([imageId1], 0).then(() => { + subscribeToImageRendered() + vp.setProperties({ + interpolationType: INTERPOLATION_TYPE.NEAREST, + voiRange: { lower: -260, upper: 140 }, + invert: true, + rotation: 90, + }) + + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should be able to resetProperties API', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + + const firstImageRenderedCallback = () => { + let props = vp.getProperties() + expect(props.rotation).toBe(90) + expect(props.interpolationType).toBe(INTERPOLATION_TYPE.NEAREST) + expect(props.invert).toBe(true) + + canvas.removeEventListener( + EVENTS.IMAGE_RENDERED, + firstImageRenderedCallback + ) + + setTimeout(() => { + vp.resetProperties() + }) + + canvas.addEventListener( + EVENTS.IMAGE_RENDERED, + secondImageRenderedCallback + ) + } + + const secondImageRenderedCallback = () => { + const props = vp.getProperties() + expect(props.rotation).toBe(0) + expect(props.interpolationType).toBe(INTERPOLATION_TYPE.LINEAR) + expect(props.invert).toBe(false) + done() + } + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, firstImageRenderedCallback) + + try { + vp.setStack([imageId1], 0).then(() => { + vp.setProperties({ + interpolationType: INTERPOLATION_TYPE.NEAREST, + voiRange: { lower: -260, upper: 140 }, + invert: true, + rotation: 90, + }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Calibration ', () => { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + metaData.addProvider( + calibratedPixelSpacingMetadataProvider.get.bind( + calibratedPixelSpacingMetadataProvider + ), + 11000 + ) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('Should be able to calibrate an image', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + const imageId1 = 'fakeImageLoader:imageURI_11_11_4_1_1_1_0_1' + + const vp = this.renderingEngine.getViewport(viewportUID) + + const firstCallback = () => { + canvas.removeEventListener(EVENTS.IMAGE_RENDERED, firstCallback) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, secondCallback) + const imageId = this.renderingEngine + .getViewport(viewportUID) + .getCurrentImageId() + + calibrateImageSpacing(imageId, this.renderingEngine, 1, 5) + } + const secondCallback = () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + calibrated_1_5_imageURI_11_11_4_1_1_1_0_1, + 'calibrated_1_5_imageURI_11_11_4_1_1_1_0_1' + ).then(done, done.fail) + } + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, firstCallback) + + try { + vp.setStack([imageId1], 0) + vp.render() + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Calibration ', () => { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + metaData.addProvider( + calibratedPixelSpacingMetadataProvider.get.bind( + calibratedPixelSpacingMetadataProvider + ), + 11000 + ) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('Should be able to fire imageCalibrated event with expected data', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + // Note: this should be a unique image in our tests, since we + // are basically modifying the metadata of the image to be calibrated + const imageId1 = 'fakeImageLoader:imageURI_64_46_4_1_1_1_0_1' + + const vp = this.renderingEngine.getViewport(viewportUID) + + const imageRenderedCallback = () => { + const imageId = this.renderingEngine + .getViewport(viewportUID) + .getCurrentImageId() + + calibrateImageSpacing(imageId, this.renderingEngine, 1, 5) + canvas.removeEventListener(EVENTS.IMAGE_RENDERED, imageRenderedCallback) + canvas.addEventListener( + EVENTS.IMAGE_RENDERED, + secondImageRenderedCallback + ) + } + + const secondImageRenderedCallback = () => { + done() + } + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, imageRenderedCallback) + + canvas.addEventListener(EVENTS.IMAGE_SPACING_CALIBRATED, (evt) => { + expect(evt.detail).toBeDefined() + expect(evt.detail.rowScale).toBe(1) + expect(evt.detail.columnScale).toBe(5) + expect(evt.detail.viewportUID).toBe(viewportUID) + }) + + try { + vp.setStack([imageId1], 0) + vp.render() + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Flipping', function () { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('Should be able to flip a stack viewport horizontally', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + // imageId : imageLoaderScheme: imageURI_rows_colums_barStart_barWidth_xSpacing_ySpacing_rgbFlag + const imageId = 'fakeImageLoader:imageURI_64_64_5_5_1_1_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + imageURI_64_64_20_5_1_1_0_nearestFlipH, + 'imageURI_64_64_20_5_1_1_0_nearestFlipH' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ + interpolationType: INTERPOLATION_TYPE.NEAREST, + flipHorizontal: true, + }) + + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + + it('Should be able to flip a stack viewport horizontally and rotate it', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL, 256, 256) + + // imageId : imageLoaderScheme: imageURI_rows_colums_barStart_barWidth_xSpacing_ySpacing_rgbFlag + const imageId = 'fakeImageLoader:imageURI_64_64_5_5_1_1_0' + + const vp = this.renderingEngine.getViewport(viewportUID) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90, + 'imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90' + ).then(done, done.fail) + }) + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ + interpolationType: INTERPOLATION_TYPE.NEAREST, + rotation: 90, + flipHorizontal: true, + }) + + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) + }) +}) diff --git a/packages/cornerstone-render/test/volumeViewport_gpu_render_test.js b/packages/cornerstone-render/test/volumeViewport_gpu_render_test.js new file mode 100644 index 0000000000..d80d423d90 --- /dev/null +++ b/packages/cornerstone-render/test/volumeViewport_gpu_render_test.js @@ -0,0 +1,646 @@ +import * as cornerstone3D from '../src/index' +// import { User } from ... doesn't work right now since we don't have named exports set up + +// nearest neighbor interpolation +import * as volumeURI_100_100_10_1_1_1_0_axial_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_axial_nearest.png' +import * as volumeURI_100_100_10_1_1_1_0_sagittal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_sagittal_nearest.png' +import * as volumeURI_100_100_10_1_1_1_0_coronal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_coronal_nearest.png' +import * as volumeURI_100_100_10_1_1_1_1_color_coronal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_1_color_coronal_nearest.png' + +// linear interpolation +import * as volumeURI_100_100_10_1_1_1_0_axial_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_axial_linear.png' +import * as volumeURI_100_100_10_1_1_1_0_sagittal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_sagittal_linear.png' +import * as volumeURI_100_100_10_1_1_1_0_coronal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_coronal_linear.png' +import * as volumeURI_100_100_10_1_1_1_1_color_coronal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_1_color_coronal_linear.png' + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + unregisterAllImageLoaders, + metaData, + EVENTS, + registerVolumeLoader, + createAndCacheVolume, + Utilities, +} = cornerstone3D + +const { fakeMetaDataProvider, compareImages, fakeVolumeLoader } = + Utilities.testUtils + +const renderingEngineUID = Utilities.uuidv4() + +const scene1UID = 'SCENE_1' +const viewportUID = 'VIEWPORT' + +const AXIAL = 'AXIAL' +const SAGITTAL = 'SAGITTAL' +const CORONAL = 'CORONAL' + +const DOMElements = [] + +function createCanvas(renderingEngine, orientation) { + const canvasAxial = document.createElement('canvas') + + canvasAxial.style.width = '1000px' + canvasAxial.style.height = '1000px' + document.body.appendChild(canvasAxial) + DOMElements.push(canvasAxial) + + renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: viewportUID, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvasAxial, + defaultOptions: { + orientation: ORIENTATION[orientation], + background: [1, 0, 1], // pinkish background + }, + }, + ]) + return canvasAxial +} + +describe('renderingCore -- volume', () => { + beforeAll(() => { + // initialize the library + cornerstone3D.init() + }) + + describe('Volume Viewport Axial Nearest Neighbor and Linear Interpolation --- ', function () { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + + metaData.addProvider(fakeMetaDataProvider, 10000) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should successfully load a volume: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL) + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_axial_nearest, + 'volumeURI_100_100_10_1_1_1_0_axial_nearest' + ).then(done, done.fail) + }) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId, callback }]) + ctScene.render() + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully load a volume: linear', function (done) { + const canvas = createCanvas(this.renderingEngine, AXIAL) + + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_axial_linear, + 'volumeURI_100_100_10_1_1_1_0_axial_linear' + ).then(done, done.fail) + }) + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId }]) + ctScene.render() + }) + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Volume Viewport Sagittal Nearest Neighbor and Linear Interpolation --- ', function () { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + + metaData.addProvider(fakeMetaDataProvider, 10000) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should successfully load a volume: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, SAGITTAL) + + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_sagittal_nearest, + 'volumeURI_100_100_10_1_1_1_0_sagittal_nearest' + ).then(done, done.fail) + }) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId, callback }]) + ctScene.render() + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully load a volume: linear', function (done) { + const canvas = createCanvas(this.renderingEngine, SAGITTAL) + + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_sagittal_linear, + 'volumeURI_100_100_10_1_1_1_0_sagittal_linear' + ).then(done, done.fail) + }) + + try { + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId }]) + ctScene.render() + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Volume Viewport Sagittal Coronal Neighbor and Linear Interpolation --- ', function () { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + + metaData.addProvider(fakeMetaDataProvider, 10000) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should successfully load a volume: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, CORONAL) + + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ).then(done, done.fail) + }) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + // we don't set imageIds as we are mocking the imageVolume to + // return the volume immediately + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId, callback }]) + ctScene.render() + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully load a volume: linear', function (done) { + const canvas = createCanvas(this.renderingEngine, CORONAL) + + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_linear, + 'volumeURI_100_100_10_1_1_1_0_coronal_linear' + ).then(done, done.fail) + }) + + try { + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId }]) + ctScene.render() + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Rendering Scenes API', function () { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + + metaData.addProvider(fakeMetaDataProvider, 10000) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should successfully use renderScenes API to load image', function (done) { + const canvas = createCanvas(this.renderingEngine, CORONAL) + + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ).then(done, done.fail) + }) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + // we don't set imageIds as we are mocking the imageVolume to + // return the volume immediately + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + // const scenes = this.renderingEngine.getScenes() + ctScene.setVolumes([{ volumeUID: volumeId, callback }]) + this.renderingEngine.renderScenes([scene1UID]) + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + + it('Should be able to filter viewports based on volumeUID', function (done) { + const canvas = createCanvas(this.renderingEngine, CORONAL) + + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const viewport = this.renderingEngine.getViewport(viewportUID) + const viewports = + this.renderingEngine.getViewportsContainingVolumeUID(volumeId) + + expect(viewports.length).toBe(1) + expect(viewports[0]).toBe(viewport) + + const scenes = this.renderingEngine.getScenesContainingVolume(volumeId) + const sceneViewport = scenes[0].getViewports()[0] + expect(scenes.length).toBe(1) + expect(sceneViewport).toBe(viewport) + done() + }) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + // we don't set imageIds as we are mocking the imageVolume to + // return the volume immediately + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId, callback }]) + this.renderingEngine.renderScenes([scene1UID]) + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully use renderViewports API to load image', function (done) { + const canvas = createCanvas(this.renderingEngine, CORONAL) + + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ).then(done, done.fail) + }) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + // we don't set imageIds as we are mocking the imageVolume to + // return the volume immediately + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + // const scenes = this.renderingEngine.getScenes() + ctScene.setVolumes([{ volumeUID: volumeId, callback }]) + this.renderingEngine.renderViewports([viewportUID]) + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully use renderViewport API to load image', function (done) { + const canvas = createCanvas(this.renderingEngine, CORONAL) + + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ).then(done, done.fail) + }) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + // we don't set imageIds as we are mocking the imageVolume to + // return the volume immediately + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + // const scenes = this.renderingEngine.getScenes() + ctScene.setVolumes([{ volumeUID: volumeId, callback }]) + this.renderingEngine.renderViewport(viewportUID) + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully debug the offscreen canvas', function (done) { + const canvas = createCanvas(this.renderingEngine, CORONAL) + + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + const offScreen = this.renderingEngine._debugRender() + expect(offScreen).toEqual(image) + done() + }) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + // we don't set imageIds as we are mocking the imageVolume to + // return the volume immediately + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + // const scenes = this.renderingEngine.getScenes() + ctScene.setVolumes([{ volumeUID: volumeId, callback }]) + this.renderingEngine.renderViewport(viewportUID) + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully render frameOfReference', function (done) { + const canvas = createCanvas(this.renderingEngine, CORONAL) + + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ).then(done, done.fail) + }) + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest() + + try { + // we don't set imageIds as we are mocking the imageVolume to + // return the volume immediately + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + // const scenes = this.renderingEngine.getScenes() + ctScene.setVolumes([{ volumeUID: volumeId, callback }]).then(() => { + this.renderingEngine.renderFrameOfReference( + 'Volume_Frame_Of_Reference' + ) + }) + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Volume Viewport Color images Neighbor and Linear Interpolation --- ', function () { + beforeEach(function () { + cache.purgeCache() + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + + metaData.addProvider(fakeMetaDataProvider, 10000) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + }) + + afterEach(function () { + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('should successfully load a color volume: nearest', function (done) { + const canvas = createCanvas(this.renderingEngine, CORONAL) + + // fake volume generator follows the pattern of + // volumeScheme:volumeURI_xSize_ySize_zSize_barStart_barWidth_xSpacing_ySpacing_zSpacing_rgbFlag + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_1' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_1_color_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_1_color_coronal_nearest' + ).then(done, done.fail) + }) + + const callback = ({ volumeActor }) => { + volumeActor.getProperty().setIndependentComponents(false) + volumeActor.getProperty().setInterpolationTypeToNearest() + } + + try { + // we don't set imageIds as we are mocking the imageVolume to + // return the volume immediately + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId, callback }]) + ctScene.render() + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + + it('should successfully load a volume: linear', function (done) { + const canvas = createCanvas(this.renderingEngine, CORONAL) + + const volumeId = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_1' + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png') + compareImages( + image, + volumeURI_100_100_10_1_1_1_1_color_coronal_linear, + 'volumeURI_100_100_10_1_1_1_1_color_coronal_linear' + ).then(done, done.fail) + }) + + const callback = ({ volumeActor }) => { + volumeActor.getProperty().setIndependentComponents(false) + volumeActor.getProperty().setInterpolationTypeToLinear() + } + + try { + createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId, callback }]) + ctScene.render() + }) + .catch((e) => done(e)) + } catch (e) { + done.fail(e) + } + }) + }) +}) diff --git a/packages/cornerstone-tools/src/cursors/MouseCursor.ts b/packages/cornerstone-tools/src/cursors/MouseCursor.ts index 4f607ee607..cc5d65c7ad 100644 --- a/packages/cornerstone-tools/src/cursors/MouseCursor.ts +++ b/packages/cornerstone-tools/src/cursors/MouseCursor.ts @@ -65,6 +65,7 @@ export default class MouseCursor { static getDefinedCursor(name: string): MouseCursor | undefined { const definedCursors = getDefinedCursors( + // @ts-ignore MouseCursor as Record>, DEFINED_CURSORS ) @@ -82,6 +83,7 @@ export default class MouseCursor { static setDefinedCursor(name: string, cursor: MouseCursor): boolean { if (cursor instanceof MouseCursor) { const definedCursors = getDefinedCursors( + // @ts-ignore MouseCursor as Record>, DEFINED_CURSORS ) diff --git a/packages/cornerstone-tools/src/tools/MIPJumpToClickTool.ts b/packages/cornerstone-tools/src/tools/MIPJumpToClickTool.ts index 26e3ae074c..b8570821ae 100644 --- a/packages/cornerstone-tools/src/tools/MIPJumpToClickTool.ts +++ b/packages/cornerstone-tools/src/tools/MIPJumpToClickTool.ts @@ -1,5 +1,9 @@ import { BaseTool } from './base' -import { getEnabledElement, Scene } from '@ohif/cornerstone-render' +import { + getEnabledElement, + Scene, + VolumeViewport, +} from '@ohif/cornerstone-render' import { getVoxelPositionBasedOnIntensity } from '../util/planar' import { ToolGroupManager } from '../store' import CrosshairsTool from './CrosshairsTool' @@ -54,7 +58,7 @@ export default class MIPJumpToClickTool extends BaseTool { // 4. Search for the brightest point location in the line of sight const brightestPoint = getVoxelPositionBasedOnIntensity( scene, - viewport, + viewport as VolumeViewport, targetVolumeUID, maxFn, currentPoints.world diff --git a/packages/cornerstone-tools/src/tools/PetThresholdTool.ts b/packages/cornerstone-tools/src/tools/PetThresholdTool.ts index 0d41fed44a..1bed2f6074 100644 --- a/packages/cornerstone-tools/src/tools/PetThresholdTool.ts +++ b/packages/cornerstone-tools/src/tools/PetThresholdTool.ts @@ -3,6 +3,7 @@ import { EVENTS, triggerEvent, VolumeViewport, + StackViewport, } from '@ohif/cornerstone-render' import { BaseTool } from './base' @@ -26,48 +27,27 @@ export default class PetThresholdTool extends BaseTool { const enabledElement = getEnabledElement(canvas) const { scene, sceneUID, viewportUID, viewport } = enabledElement - let volumeUID; - if (this.configuration && this.configuration.volumeUID) { - volumeUID = this.configuration.volumeUID - } else { - const defaultActor = viewport.getDefaultActor(); - volumeUID = defaultActor.uid; - } - - let volumeActor + let volumeUID, volumeActor, lower, upper, rgbTransferFunction - if (viewport instanceof VolumeViewport && volumeUID) { + if (viewport instanceof VolumeViewport) { + volumeUID = this.configuration.volumeUID volumeActor = scene.getVolumeActor(volumeUID) + rgbTransferFunction = volumeActor.getProperty().getRGBTransferFunction(0) + ;[lower, upper] = rgbTransferFunction.getRange() } else { - const volumeActors = viewport.getActors() - if (volumeActors && volumeActors.length) { - volumeActor = volumeActors[0].volumeActor - } + const properties = viewport.getProperties() + ;({ lower, upper } = properties.voiRange) } - if (!volumeActor) { - // No volume actor available. - return - } - - const rgbTransferFunction = volumeActor - .getProperty() - .getRGBTransferFunction(0) - const deltaY = deltaPoints.canvas[1] const multiplier = 5 / canvas.clientHeight const wcDelta = deltaY * multiplier - const range = rgbTransferFunction.getRange() - const lower = range[0] - let upper = range[1] upper -= wcDelta upper = Math.max(upper, 0.1) const newRange = { lower, upper } - rgbTransferFunction.setMappingRange(lower, upper) - const eventDetail = { volumeUID, viewportUID, @@ -77,16 +57,16 @@ export default class PetThresholdTool extends BaseTool { triggerEvent(canvas, EVENTS.VOI_MODIFIED, eventDetail) - if (scene || viewport instanceof VolumeViewport) { - scene.render() + if (viewport instanceof StackViewport) { + viewport.setProperties({ + voiRange: newRange, + }) + + viewport.render() return } - // store the new range for viewport to preserve it during scrolling - viewport.setProperties({ - voiRange: newRange - }) - - viewport.render() + rgbTransferFunction.setRange(newRange.lower, newRange.upper) + scene.render() } } diff --git a/packages/cornerstone-tools/src/tools/WindowLevelTool.ts b/packages/cornerstone-tools/src/tools/WindowLevelTool.ts index f7bb34b422..11e6e5d951 100644 --- a/packages/cornerstone-tools/src/tools/WindowLevelTool.ts +++ b/packages/cornerstone-tools/src/tools/WindowLevelTool.ts @@ -5,8 +5,13 @@ import { EVENTS, triggerEvent, VolumeViewport, + StackViewport, + Utilities, } from '@ohif/cornerstone-render' -import { StreamingImageVolume } from '@ohif/cornerstone-image-loader-streaming-volume' + +// Todo: should move to configuration +const DEFAULT_MULTIPLIER = 4 +const DEFAULT_IMAGE_DYNAMIC_RANGE = 1024 export default class WindowLevelTool extends BaseTool { touchDragCallback: () => void @@ -22,74 +27,40 @@ export default class WindowLevelTool extends BaseTool { this.mouseDragCallback = this._dragCallback.bind(this) } - private _toWindowLevel(low, high) { - const windowWidth = Math.abs(low - high) - const windowCenter = low + windowWidth / 2 - - return { windowWidth, windowCenter } - } - - private _toLowHighRange(windowWidth, windowCenter) { - const lower = windowCenter - windowWidth / 2.0 - const upper = windowCenter + windowWidth / 2.0 - - return { lower, upper } - } - _dragCallback(evt) { const { element: canvas, deltaPoints } = evt.detail const enabledElement = getEnabledElement(canvas) const { scene, sceneUID, viewportUID, viewport } = enabledElement - let volumeUID - if (this.configuration && this.configuration.volumeUID) { - volumeUID = this.configuration.volumeUID - } else { - const defaultActor = viewport.getDefaultActor() - volumeUID = defaultActor.uid - } - - let volumeActor + let volumeUID, volumeActor, lower, upper, rgbTransferFunction + let useDynamicRange = false - if (viewport instanceof VolumeViewport && volumeUID) { + if (viewport instanceof VolumeViewport) { + volumeUID = this.configuration.volumeUID volumeActor = scene.getVolumeActor(volumeUID) + rgbTransferFunction = volumeActor.getProperty().getRGBTransferFunction(0) + ;[lower, upper] = rgbTransferFunction.getRange() + useDynamicRange = true } else { - const volumeActors = viewport.getActors() - if (volumeActors && volumeActors.length) { - volumeActor = volumeActors[0].volumeActor - } + const properties = viewport.getProperties() + ;({ lower, upper } = properties.voiRange) } - if (!volumeActor) { - // No volume actor available. - return - } - - const rgbTransferFunction = volumeActor - .getProperty() - .getRGBTransferFunction(0) - const deltaPointsCanvas = deltaPoints.canvas // Todo: enabling a viewport twice in a row sets the imageDynamicRange to be zero for some reason // 1 was too little - let multiplier = 4 - if (viewport instanceof VolumeViewport && volumeUID) { - const imageDynamicRange = this._getImageDynamicRange(volumeUID) - - const ratio = imageDynamicRange / 1024 - - if (ratio > 1) { - multiplier = Math.round(ratio) - } - } + const multiplier = useDynamicRange + ? this._getMultiplyerFromDynamicRange(volumeUID) + : DEFAULT_MULTIPLIER const wwDelta = deltaPointsCanvas[0] * multiplier const wcDelta = deltaPointsCanvas[1] * multiplier - const [lower, upper] = rgbTransferFunction.getRange() - - let { windowWidth, windowCenter } = this._toWindowLevel(lower, upper) + let { windowWidth, windowCenter } = Utilities.windowLevel.toWindowLevel( + lower, + upper + ) windowWidth += wwDelta windowCenter += wcDelta @@ -97,9 +68,10 @@ export default class WindowLevelTool extends BaseTool { windowWidth = Math.max(windowWidth, 1) // Convert back to range - const newRange = this._toLowHighRange(windowWidth, windowCenter) - - rgbTransferFunction.setMappingRange(newRange.lower, newRange.upper) + const newRange = Utilities.windowLevel.toLowHighRange( + windowWidth, + windowCenter + ) const eventDetail = { volumeUID, @@ -110,17 +82,34 @@ export default class WindowLevelTool extends BaseTool { triggerEvent(canvas, EVENTS.VOI_MODIFIED, eventDetail) - if (scene || viewport instanceof VolumeViewport) { - scene.render() + if (viewport instanceof StackViewport) { + viewport.setProperties({ + voiRange: newRange, + }) + + viewport.render() return } - // store the new range for viewport to preserve it during scrolling - viewport.setProperties({ - voiRange: newRange, - }) + rgbTransferFunction.setRange(newRange.lower, newRange.upper) + scene.render() + } - viewport.render() + _getMultiplyerFromDynamicRange(volumeUID) { + if (!volumeUID) { + throw new Error('No volumeUID provided for the volume Viewport') + } + + let multiplier = DEFAULT_MULTIPLIER + const imageDynamicRange = this._getImageDynamicRange(volumeUID) + + const ratio = imageDynamicRange / DEFAULT_IMAGE_DYNAMIC_RANGE + + if (ratio > 1) { + multiplier = Math.round(ratio) + } + + return multiplier } _getImageDynamicRange = (volumeUID: string) => { @@ -173,5 +162,3 @@ export default class WindowLevelTool extends BaseTool { return max - min } } - -const DEFAULT_IMAGE_DYNAMIC_RANGE = 1024 diff --git a/packages/cornerstone-tools/src/tools/ZoomTool.ts b/packages/cornerstone-tools/src/tools/ZoomTool.ts index fa7c5f5768..908ed292d8 100644 --- a/packages/cornerstone-tools/src/tools/ZoomTool.ts +++ b/packages/cornerstone-tools/src/tools/ZoomTool.ts @@ -54,7 +54,10 @@ export default class ZoomTool extends BaseTool { const k = deltaY * zoomScale - viewport.setCamera({ parallelScale: (1.0 - k) * camera.parallelScale }) + const newParallelScale = (1.0 - k) * camera.parallelScale + + // viewport.setCamera({ parallelScale: newParallelScale, deltaPoints }); + viewport.setCamera({ parallelScale: newParallelScale }) } _dragPerspectiveProjection = (evt, camera) => { diff --git a/packages/cornerstone-tools/src/tools/annotation/BidirectionalTool.ts b/packages/cornerstone-tools/src/tools/annotation/BidirectionalTool.ts index b0709612d8..7a8155fc90 100644 --- a/packages/cornerstone-tools/src/tools/annotation/BidirectionalTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/BidirectionalTool.ts @@ -1174,22 +1174,17 @@ export default class BidirectionalTool extends BaseAnnotationTool { renderingEngine ) - const { vtkImageData: imageData, dimensions } = imageVolume + const { imageData, dimensions } = imageVolume const dist1 = this._calculateLength(worldPos1, worldPos2) const dist2 = this._calculateLength(worldPos3, worldPos4) const length = dist1 > dist2 ? dist1 : dist2 const width = dist1 > dist2 ? dist2 : dist1 - const index1 = [0, 0, 0] - const index2 = [0, 0, 0] - const index3 = [0, 0, 0] - const index4 = [0, 0, 0] - - imageData.worldToIndexVec3(worldPos1, index1) - imageData.worldToIndexVec3(worldPos2, index2) - imageData.worldToIndexVec3(worldPos3, index3) - imageData.worldToIndexVec3(worldPos4, index4) + const index1 = imageData.worldToIndex(worldPos1) + const index2 = imageData.worldToIndex(worldPos2) + const index3 = imageData.worldToIndex(worldPos3) + const index4 = imageData.worldToIndex(worldPos4) this._isInsideVolume(index1, index2, index3, index4, dimensions) ? (this.isHandleOutsideImage = false) diff --git a/packages/cornerstone-tools/src/tools/annotation/EllipticalRoiTool.ts b/packages/cornerstone-tools/src/tools/annotation/EllipticalRoiTool.ts index 7095e724c2..bcd492a4e4 100644 --- a/packages/cornerstone-tools/src/tools/annotation/EllipticalRoiTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/EllipticalRoiTool.ts @@ -968,23 +968,15 @@ export default class EllipticalRoiTool extends BaseAnnotationTool { renderingEngine ) - const { - dimensions, - scalarData, - vtkImageData: imageData, - direction, - metadata, - } = imageVolume - const worldPos1Index = vec3.fromValues(0, 0, 0) - const worldPos2Index = vec3.fromValues(0, 0, 0) - - imageData.worldToIndexVec3(worldPos1, worldPos1Index) + const { dimensions, scalarData, imageData, metadata } = imageVolume + + const worldPos1Index = imageData.worldToIndex(worldPos1) worldPos1Index[0] = Math.floor(worldPos1Index[0]) worldPos1Index[1] = Math.floor(worldPos1Index[1]) worldPos1Index[2] = Math.floor(worldPos1Index[2]) - imageData.worldToIndexVec3(worldPos2, worldPos2Index) + const worldPos2Index = imageData.worldToIndex(worldPos2) worldPos2Index[0] = Math.floor(worldPos2Index[0]) worldPos2Index[1] = Math.floor(worldPos2Index[1]) @@ -1024,29 +1016,25 @@ export default class EllipticalRoiTool extends BaseAnnotationTool { // So we instead work out the change in canvas position incrementing each index causes. const start = vec3.fromValues(iMin, jMin, kMin) - const worldPosStart = vec3.create() - imageData.indexToWorldVec3(start, worldPosStart) + const worldPosStart = imageData.indexToWorld(start) const canvasPosStart = viewport.worldToCanvas(worldPosStart) const startPlusI = vec3.fromValues(iMin + 1, jMin, kMin) const startPlusJ = vec3.fromValues(iMin, jMin + 1, kMin) const startPlusK = vec3.fromValues(iMin, jMin, kMin + 1) - const worldPosStartPlusI = vec3.create() const plusICanvasDelta = vec2.create() - imageData.indexToWorldVec3(startPlusI, worldPosStartPlusI) + const worldPosStartPlusI = imageData.indexToWorld(startPlusI) const canvasPosStartPlusI = viewport.worldToCanvas(worldPosStartPlusI) vec2.sub(plusICanvasDelta, canvasPosStartPlusI, canvasPosStart) - const worldPosStartPlusJ = vec3.create() const plusJCanvasDelta = vec2.create() - imageData.indexToWorldVec3(startPlusJ, worldPosStartPlusJ) + const worldPosStartPlusJ = imageData.indexToWorld(startPlusJ) const canvasPosStartPlusJ = viewport.worldToCanvas(worldPosStartPlusJ) vec2.sub(plusJCanvasDelta, canvasPosStartPlusJ, canvasPosStart) - const worldPosStartPlusK = vec3.create() const plusKCanvasDelta = vec2.create() - imageData.indexToWorldVec3(startPlusK, worldPosStartPlusK) + const worldPosStartPlusK = imageData.indexToWorld(startPlusK) const canvasPosStartPlusK = viewport.worldToCanvas(worldPosStartPlusK) vec2.sub(plusKCanvasDelta, canvasPosStartPlusK, canvasPosStart) diff --git a/packages/cornerstone-tools/src/tools/annotation/LengthTool.ts b/packages/cornerstone-tools/src/tools/annotation/LengthTool.ts index 3a8d3c4d0f..51ca36716c 100644 --- a/packages/cornerstone-tools/src/tools/annotation/LengthTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/LengthTool.ts @@ -746,15 +746,12 @@ class LengthTool extends BaseAnnotationTool { renderingEngine ) - const { vtkImageData: imageData, dimensions } = imageVolume + const { imageData, dimensions } = imageVolume const length = this._calculateLength(worldPos1, worldPos2) - const index1 = [0, 0, 0] - const index2 = [0, 0, 0] - - imageData.worldToIndexVec3(worldPos1, index1) - imageData.worldToIndexVec3(worldPos2, index2) + const index1 = imageData.worldToIndex(worldPos1) + const index2 = imageData.worldToIndex(worldPos2) this._isInsideVolume(index1, index2, dimensions) ? (this.isHandleOutsideImage = false) diff --git a/packages/cornerstone-tools/src/tools/annotation/ProbeTool.ts b/packages/cornerstone-tools/src/tools/annotation/ProbeTool.ts index e40f673331..b5f066dae0 100644 --- a/packages/cornerstone-tools/src/tools/annotation/ProbeTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/ProbeTool.ts @@ -555,18 +555,10 @@ export default class ProbeTool extends BaseAnnotationTool { renderingEngine ) - const { - dimensions, - scalarData, - vtkImageData: imageData, - metadata, - } = imageVolume + const { dimensions, scalarData, imageData, metadata } = imageVolume const modality = metadata.Modality - - const index = [0, 0, 0] - - imageData.worldToIndexVec3(worldPos, index) + const index = imageData.worldToIndex(worldPos) index[0] = Math.floor(index[0]) index[1] = Math.floor(index[1]) diff --git a/packages/cornerstone-tools/src/tools/annotation/RectangleRoiTool.ts b/packages/cornerstone-tools/src/tools/annotation/RectangleRoiTool.ts index fedc33257a..17759044df 100644 --- a/packages/cornerstone-tools/src/tools/annotation/RectangleRoiTool.ts +++ b/packages/cornerstone-tools/src/tools/annotation/RectangleRoiTool.ts @@ -916,23 +916,15 @@ export default class RectangleRoiTool extends BaseAnnotationTool { renderingEngine ) - const { - dimensions, - scalarData, - vtkImageData: imageData, - metadata, - direction, - } = imageVolume - const worldPos1Index = vec3.fromValues(0, 0, 0) - const worldPos2Index = vec3.fromValues(0, 0, 0) - - imageData.worldToIndexVec3(worldPos1, worldPos1Index) + const { dimensions, scalarData, imageData, metadata } = imageVolume + + const worldPos1Index = imageData.worldToIndex(worldPos1) worldPos1Index[0] = Math.floor(worldPos1Index[0]) worldPos1Index[1] = Math.floor(worldPos1Index[1]) worldPos1Index[2] = Math.floor(worldPos1Index[2]) - imageData.worldToIndexVec3(worldPos2, worldPos2Index) + const worldPos2Index = imageData.worldToIndex(worldPos2) worldPos2Index[0] = Math.floor(worldPos2Index[0]) worldPos2Index[1] = Math.floor(worldPos2Index[1]) diff --git a/packages/cornerstone-tools/src/util/planar/getVoxelPositionBasedOnIntensity.ts b/packages/cornerstone-tools/src/util/planar/getVoxelPositionBasedOnIntensity.ts index 332a5e70fd..b1c4066620 100644 --- a/packages/cornerstone-tools/src/util/planar/getVoxelPositionBasedOnIntensity.ts +++ b/packages/cornerstone-tools/src/util/planar/getVoxelPositionBasedOnIntensity.ts @@ -1,7 +1,7 @@ import getTargetVolume from './getTargetVolume' import vtkMath from 'vtk.js/Sources/Common/Core/Math' import { Point3 } from './../../types' -import { Scene, Viewport } from '@ohif/cornerstone-render' +import { Scene, VolumeViewport } from '@ohif/cornerstone-render' /** * Returns a point on the line between the passed canvasPoint (clicked point often) @@ -21,7 +21,7 @@ import { Scene, Viewport } from '@ohif/cornerstone-render' */ export default function getVoxelPositionBasedOnIntensity( scene: Scene, - viewport: Viewport, + viewport: VolumeViewport, targetVolumeUID: string, criteriaFunction: (intensity: number, point: Point3) => Point3, canvasPointInWorld: Point3 diff --git a/packages/cornerstone-tools/test/BidirectionalTool_test.js b/packages/cornerstone-tools/test/BidirectionalTool_test.js index ce0fb8c645..20d3b6b82e 100644 --- a/packages/cornerstone-tools/test/BidirectionalTool_test.js +++ b/packages/cornerstone-tools/test/BidirectionalTool_test.js @@ -88,6 +88,11 @@ function createCanvas(renderingEngine, viewportType, width, height) { const volumeId = `fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0` describe('Cornerstone Tools: ', () => { + beforeAll(() => { + // initialize the library + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + beforeEach(function () { csTools3d.init() csTools3d.addTool(BidirectionalTool, {}) @@ -168,7 +173,7 @@ describe('Cornerstone Tools: ', () => { const index1 = [32, 32, 0] const index2 = [10, 1, 0] - const { vtkImageData } = vp.getImageData() + const { imageData } = vp.getImageData() const { pageX: pageX1, @@ -176,7 +181,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX1, clientY: clientY1, worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) p1 = worldCoord1 const { @@ -185,7 +190,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX2, clientY: clientY2, worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) p2 = worldCoord2 // Mouse Down @@ -276,7 +281,7 @@ describe('Cornerstone Tools: ', () => { const index1 = [32, 32, 4] const index2 = [10, 1, 4] - const { vtkImageData } = vp.getImageData() + const { imageData } = vp.getImageData() const { pageX: pageX1, @@ -284,7 +289,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX1, clientY: clientY1, worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) p1 = worldCoord1 const { @@ -293,7 +298,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX2, clientY: clientY2, worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) p2 = worldCoord2 // Mouse Down @@ -393,7 +398,7 @@ describe('Cornerstone Tools: ', () => { const index2 = [5, 5, 0] const index3 = [52, 47, 0] - const { vtkImageData } = vp.getImageData() + const { imageData } = vp.getImageData() const { pageX: pageX1, @@ -401,7 +406,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX1, clientY: clientY1, worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) const { pageX: pageX2, @@ -409,7 +414,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX2, clientY: clientY2, worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) p2 = worldCoord2 const { pageX: pageX3, @@ -417,7 +422,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX3, clientY: clientY3, worldCoord: worldCoord3, - } = createNormalizedMouseEvent(vtkImageData, index3, canvas, vp) + } = createNormalizedMouseEvent(imageData, index3, canvas, vp) p3 = worldCoord3 // Mouse Down @@ -541,7 +546,7 @@ describe('Cornerstone Tools: ', () => { // grab the tool in its middle (just to make it easy) const index3 = [20, 25, 0] - const { vtkImageData } = vp.getImageData() + const { imageData } = vp.getImageData() const { pageX: pageX1, @@ -549,7 +554,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX1, clientY: clientY1, worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) p1 = worldCoord1 const { pageX: pageX2, @@ -557,7 +562,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX2, clientY: clientY2, worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) p2 = worldCoord2 const { @@ -566,7 +571,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX3, clientY: clientY3, worldCoord: worldCoord3, - } = createNormalizedMouseEvent(vtkImageData, index3, canvas, vp) + } = createNormalizedMouseEvent(imageData, index3, canvas, vp) // Mouse Down let evt = new MouseEvent('mousedown', { @@ -721,7 +726,7 @@ describe('Cornerstone Tools: ', () => { // Where to move the center of the tool const index4 = [40, 40, 0] - const { vtkImageData } = vp.getImageData() + const { imageData } = vp.getImageData() const { pageX: pageX1, @@ -729,7 +734,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX1, clientY: clientY1, worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) p1 = worldCoord1 const { pageX: pageX2, @@ -737,7 +742,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX2, clientY: clientY2, worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) p2 = worldCoord2 const { @@ -746,7 +751,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX3, clientY: clientY3, worldCoord: worldCoord3, - } = createNormalizedMouseEvent(vtkImageData, index3, canvas, vp) + } = createNormalizedMouseEvent(imageData, index3, canvas, vp) p3 = worldCoord3 const { @@ -755,7 +760,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX4, clientY: clientY4, worldCoord: worldCoord4, - } = createNormalizedMouseEvent(vtkImageData, index4, canvas, vp) + } = createNormalizedMouseEvent(imageData, index4, canvas, vp) p4 = worldCoord4 // Mouse Down @@ -843,7 +848,7 @@ describe('Cornerstone Tools: ', () => { const index1 = [32, 32, 4] const index2 = [10, 1, 4] - const { vtkImageData } = vp.getImageData() + const { imageData } = vp.getImageData() const { pageX: pageX1, @@ -851,7 +856,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX1, clientY: clientY1, worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) p1 = worldCoord1 const { @@ -860,7 +865,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX2, clientY: clientY2, worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) p2 = worldCoord2 // Mouse Down diff --git a/packages/cornerstone-tools/test/CrosshairsTool_test.js b/packages/cornerstone-tools/test/CrosshairsTool_test.js index 4461b8c82f..f0762f753f 100644 --- a/packages/cornerstone-tools/test/CrosshairsTool_test.js +++ b/packages/cornerstone-tools/test/CrosshairsTool_test.js @@ -132,6 +132,11 @@ function createCanvas(renderingEngine, viewportType, width, height) { } describe('Cornerstone Tools: ', () => { + beforeAll(() => { + // initialize the library + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + beforeEach(function () { csTools3d.init() csTools3d.addTool(CrosshairsTool, {}) @@ -183,13 +188,13 @@ describe('Cornerstone Tools: ', () => { } const vp = this.renderingEngine.getViewport(viewportUID1) - const { vtkImageData } = vp.getImageData() + const { imageData } = vp.getImageData() - const indexMiddle = vtkImageData + const indexMiddle = imageData .getDimensions() .map((s) => Math.floor(s / 2)) - const imageCenterWorld = vtkImageData.indexToWorldVec3(indexMiddle) + const imageCenterWorld = imageData.indexToWorld(indexMiddle) const { sHeight, sWidth } = vp const centerCanvas = [sWidth * 0.5, sHeight * 0.5] @@ -329,15 +334,14 @@ describe('Cornerstone Tools: ', () => { } const vp1 = this.renderingEngine.getViewport(viewportUID1) - const { vtkImageData } = vp1.getImageData() + const { imageData } = vp1.getImageData() const enabledElement = getEnabledElement(canvas1) const crosshairToolState = getToolState(enabledElement, 'Crosshairs') // First viewport is axial const currentWorldLocation = crosshairToolState[0].data.handles.toolCenter - const currentIndexLocation = - vtkImageData.worldToIndexVec3(currentWorldLocation) + const currentIndexLocation = imageData.worldToIndex(currentWorldLocation) const jumpIndexLocation = [ currentIndexLocation[0] + 20, @@ -351,12 +355,7 @@ describe('Cornerstone Tools: ', () => { clientX: clientX1, clientY: clientY1, worldCoord: worldCoord1, - } = createNormalizedMouseEvent( - vtkImageData, - jumpIndexLocation, - canvas1, - vp1 - ) + } = createNormalizedMouseEvent(imageData, jumpIndexLocation, canvas1, vp1) p1 = worldCoord1 // Mouse Down @@ -425,7 +424,7 @@ describe('Cornerstone Tools: ', () => { } const vp1 = this.renderingEngine.getViewport(viewportUID1) - const { vtkImageData } = vp1.getImageData() + const { imageData } = vp1.getImageData() setTimeout(() => { const enabledElement = getEnabledElement(canvas1) @@ -435,7 +434,7 @@ describe('Cornerstone Tools: ', () => { const currentWorldLocation = crosshairToolState[0].data.handles.toolCenter const currentIndexLocation = - vtkImageData.worldToIndexVec3(currentWorldLocation) + imageData.worldToIndex(currentWorldLocation) const jumpIndexLocation = [ currentIndexLocation[0] - 20, @@ -450,7 +449,7 @@ describe('Cornerstone Tools: ', () => { clientY: clientY1, worldCoord: worldCoord1, } = createNormalizedMouseEvent( - vtkImageData, + imageData, currentIndexLocation, canvas1, vp1 @@ -463,7 +462,7 @@ describe('Cornerstone Tools: ', () => { clientY: clientY2, worldCoord: worldCoord2, } = createNormalizedMouseEvent( - vtkImageData, + imageData, jumpIndexLocation, canvas1, vp1 diff --git a/packages/cornerstone-tools/test/EllipseROI_test.js b/packages/cornerstone-tools/test/EllipseROI_test.js index 46a7ed2a1e..ee0bb5a937 100644 --- a/packages/cornerstone-tools/test/EllipseROI_test.js +++ b/packages/cornerstone-tools/test/EllipseROI_test.js @@ -62,184 +62,400 @@ function createCanvas(renderingEngine, viewportType, width, height) { DOMElements.push(canvas) DOMElements.push(viewportPane) - renderingEngine.enableElement( - { - sceneUID: scene1UID, - viewportUID, - type: viewportType, - canvas: canvas, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION[AXIAL], - }, + renderingEngine.enableElement({ + sceneUID: scene1UID, + viewportUID, + type: viewportType, + canvas: canvas, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION[AXIAL], }, - ) + }) return canvas } const volumeId = `fakeVolumeLoader:volumeURI_100_100_4_1_1_1_0` -describe('Cornerstone Tools: ', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(EllipticalRoiTool, {}) - cache.purgeCache() - this.stackToolGroup = ToolGroupManager.createToolGroup('stack') - this.stackToolGroup.addTool('EllipticalRoi', { - configuration: { volumeUID: volumeId }, - }) - this.stackToolGroup.setToolActive('EllipticalRoi', { - bindings: [{ mouseButton: 1 }], - }) - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) +describe('EllipticalRoiTool', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) }) - afterEach(function () { - this.renderingEngine.disableElement(viewportUID) + describe('Cornerstone EllipticalRoiTool: ', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(EllipticalRoiTool, {}) + cache.purgeCache() + this.stackToolGroup = ToolGroupManager.createToolGroup('stack') + this.stackToolGroup.addTool('EllipticalRoi', { + configuration: { volumeUID: volumeId }, + }) + this.stackToolGroup.setToolActive('EllipticalRoi', { + bindings: [{ mouseButton: 1 }], + }) + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) - csTools3d.destroy() - eventTarget.reset() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') + afterEach(function () { + this.renderingEngine.disableElement(viewportUID) - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } + csTools3d.destroy() + eventTarget.reset() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('stack') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) }) - }) - it('Should successfully create a ellipse tool on a canvas with mouse drag - 512 x 128', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 512, - 128 - ) + it('Should successfully create a ellipse tool on a canvas with mouse drag - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ) - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const ellipseToolState = getToolState( + enabledElement, + 'EllipticalRoi' + ) + // Can successfully add Length tool to toolStateManager + expect(ellipseToolState).toBeDefined() + expect(ellipseToolState.length).toBe(1) + + const ellipseToolData = ellipseToolState[0] + expect(ellipseToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + + expect(ellipseToolData.metadata.toolName).toBe('EllipticalRoi') + expect(ellipseToolData.data.invalidated).toBe(false) + + const data = ellipseToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + // the rectangle is drawn on the strip + expect(data[targets[0]].mean).toBe(255) + + removeToolState(canvas, ellipseToolData) + done() + } + ) + } - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - const enabledElement = getEnabledElement(canvas) - const ellipseToolState = getToolState(enabledElement, 'EllipticalRoi') - // Can successfully add Length tool to toolStateManager - expect(ellipseToolState).toBeDefined() - expect(ellipseToolState.length).toBe(1) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + // Since ellipse draws from center to out, we are picking a very center + // point in the image (strip is 255 from 10-15 in X and from 0-64 in Y) + const index1 = [12, 30, 0] + const index2 = [14, 40, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) - const ellipseToolData = ellipseToolState[0] - expect(ellipseToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - expect(ellipseToolData.metadata.toolName).toBe('EllipticalRoi') - expect(ellipseToolData.data.invalidated).toBe(false) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } + }) - const data = ellipseToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + it('Should successfully create a ellipse tool on a canvas with mouse drag in a Volume viewport - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.ORTHOGRAPHIC, + 512, + 128 + ) - // the rectangle is drawn on the strip - expect(data[targets[0]].mean).toBe(255) + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const ellipseToolState = getToolState( + enabledElement, + 'EllipticalRoi' + ) + // Can successfully add Length tool to toolStateManager + expect(ellipseToolState).toBeDefined() + expect(ellipseToolState.length).toBe(1) + + const ellipseToolData = ellipseToolState[0] + expect(ellipseToolData.metadata.toolName).toBe('EllipticalRoi') + expect(ellipseToolData.data.invalidated).toBe(false) + + const data = ellipseToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + expect(data[targets[0]].mean).toBe(255) + expect(data[targets[0]].stdDev).toBe(0) + + removeToolState(canvas, ellipseToolData) + done() + } + ) + } - removeToolState(canvas, ellipseToolData) - done() - } + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [60, 50, 2] + const index2 = [65, 60, 2] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - // Since ellipse draws from center to out, we are picking a very center - // point in the image (strip is 255 from 10-15 in X and from 0-64 in Y) - const index1 = [12, 30, 0] - const index2 = [14, 40, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId }]) + ctScene.render() + }) + } catch (e) { + done.fail(e) + } + }) + }) + + describe('Should successfully cancel a EllipseTool', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(EllipticalRoiTool, {}) + cache.purgeCache() + this.stackToolGroup = ToolGroupManager.createToolGroup('stack') + this.stackToolGroup.addTool('EllipticalRoi', { + configuration: { volumeUID: volumeId }, }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, + this.stackToolGroup.setToolActive('EllipticalRoi', { + bindings: [{ mouseButton: 1 }], }) - document.dispatchEvent(evt) - // Mouse Up instantly after - evt = new MouseEvent('mouseup') + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + afterEach(function () { + csTools3d.destroy() + eventTarget.reset() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('stack') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) }) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) + it('Should cancel drawing of a EllipseTool annotation', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ) - it('Should successfully create a ellipse tool on a canvas with mouse drag in a Volume viewport - 512 x 128', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.ORTHOGRAPHIC, - 512, - 128 - ) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + let p1, p2 + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + // Since ellipse draws from center to out, we are picking a very center + // point in the image (strip is 255 from 10-15 in X and from 0-64 in Y) + const index1 = [12, 30, 0] + const index2 = [14, 40, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Cancel the drawing + let e = new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + key: 'Esc', + char: 'Esc', + }) + canvas.dispatchEvent(e) + + e = new KeyboardEvent('keyup', { + bubbles: true, + cancelable: true, + }) + canvas.dispatchEvent(e) + }) - const vp = this.renderingEngine.getViewport(viewportUID) + const cancelToolDrawing = () => { + const canceledDataUID = cancelActiveManipulations(canvas) + expect(canceledDataUID).toBeDefined() - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { + setTimeout(() => { const enabledElement = getEnabledElement(canvas) const ellipseToolState = getToolState(enabledElement, 'EllipticalRoi') // Can successfully add Length tool to toolStateManager @@ -247,249 +463,43 @@ describe('Cornerstone Tools: ', () => { expect(ellipseToolState.length).toBe(1) const ellipseToolData = ellipseToolState[0] + expect(ellipseToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(ellipseToolData.metadata.toolName).toBe('EllipticalRoi') expect(ellipseToolData.data.invalidated).toBe(false) + expect(ellipseToolData.data.active).toBe(false) const data = ellipseToolData.data.cachedStats const targets = Array.from(Object.keys(data)) expect(targets.length).toBe(1) + // the rectangle is drawn on the strip expect(data[targets[0]].mean).toBe(255) - expect(data[targets[0]].stdDev).toBe(0) removeToolState(canvas, ellipseToolData) done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [60, 50, 2] - const index2 = [65, 60, 2] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, - }) - document.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) - }) - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId }]) - ctScene.render() - }) - } catch (e) { - done.fail(e) - } - }) -}) + }, 100) + } -describe('Should successfully cancel a EllipseTool', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(EllipticalRoiTool, {}) - cache.purgeCache() - this.stackToolGroup = ToolGroupManager.createToolGroup('stack') - this.stackToolGroup.addTool('EllipticalRoi', { - configuration: { volumeUID: volumeId }, - }) - this.stackToolGroup.setToolActive('EllipticalRoi', { - bindings: [{ mouseButton: 1 }], - }) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) + canvas.addEventListener( + CornerstoneTools3DEvents.KEY_DOWN, + cancelToolDrawing + ) - afterEach(function () { - csTools3d.destroy() - eventTarget.reset() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') - - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) } }) }) - - it('Should cancel drawing of a EllipseTool annotation', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 512, - 128 - ) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) - - let p1, p2 - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - // Since ellipse draws from center to out, we are picking a very center - // point in the image (strip is 255 from 10-15 in X and from 0-64 in Y) - const index1 = [12, 30, 0] - const index2 = [14, 40, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, - }) - document.dispatchEvent(evt) - - // Cancel the drawing - let e = new KeyboardEvent('keydown', { - bubbles: true, - cancelable: true, - key: 'Esc', - char: 'Esc', - }) - canvas.dispatchEvent(e) - - e = new KeyboardEvent('keyup', { - bubbles: true, - cancelable: true, - }) - canvas.dispatchEvent(e) - }) - - const cancelToolDrawing = () => { - const canceledDataUID = cancelActiveManipulations(canvas) - expect(canceledDataUID).toBeDefined() - - setTimeout(() => { - const enabledElement = getEnabledElement(canvas) - const ellipseToolState = getToolState(enabledElement, 'EllipticalRoi') - // Can successfully add Length tool to toolStateManager - expect(ellipseToolState).toBeDefined() - expect(ellipseToolState.length).toBe(1) - - const ellipseToolData = ellipseToolState[0] - expect(ellipseToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - - expect(ellipseToolData.metadata.toolName).toBe('EllipticalRoi') - expect(ellipseToolData.data.invalidated).toBe(false) - expect(ellipseToolData.data.active).toBe(false) - - const data = ellipseToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) - - // the rectangle is drawn on the strip - expect(data[targets[0]].mean).toBe(255) - - removeToolState(canvas, ellipseToolData) - done() - }, 100) - } - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - canvas.addEventListener( - CornerstoneTools3DEvents.KEY_DOWN, - cancelToolDrawing - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) }) diff --git a/packages/cornerstone-tools/test/FrameOfReferenceSpecificToolStateManger_test.js b/packages/cornerstone-tools/test/FrameOfReferenceSpecificToolStateManger_test.js index a374eef9bb..6d331268b4 100644 --- a/packages/cornerstone-tools/test/FrameOfReferenceSpecificToolStateManger_test.js +++ b/packages/cornerstone-tools/test/FrameOfReferenceSpecificToolStateManger_test.js @@ -1,4 +1,5 @@ import * as csTools from '../src/index' +import * as cornerstone3D from '../../cornerstone-render/src/index' const toolStateManager = csTools.defaultFrameOfReferenceSpecificToolStateManager @@ -58,6 +59,7 @@ function addAndReturnToolName1ToolData() { describe('FrameOfReferenceSpecificToolStateManager:', () => { beforeAll(function () { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) csTools.init() }) diff --git a/packages/cornerstone-tools/test/LengthTool_test.js b/packages/cornerstone-tools/test/LengthTool_test.js index ba42c436eb..49c0d48413 100644 --- a/packages/cornerstone-tools/test/LengthTool_test.js +++ b/packages/cornerstone-tools/test/LengthTool_test.js @@ -92,556 +92,858 @@ function createCanvas(renderingEngine, viewportType, width, height) { const volumeId = `fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0` -describe('Cornerstone Tools: ', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(LengthTool, {}) - cache.purgeCache() - this.stackToolGroup = ToolGroupManager.createToolGroup('stack') - this.stackToolGroup.addTool('Length', { - configuration: { volumeUID: volumeId }, - }) - this.stackToolGroup.setToolActive('Length', { - bindings: [{ mouseButton: 1 }], - }) - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) - - afterEach(function () { - csTools3d.destroy() - eventTarget.reset() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') - - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) +describe('LengthTool:', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) }) - it('Should successfully create a length tool on a canvas with mouse drag - 512 x 128', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 512, - 128 - ) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + describe('Cornerstone Tools: -- Length ', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(LengthTool, {}) + cache.purgeCache() + this.stackToolGroup = ToolGroupManager.createToolGroup('stack') + this.stackToolGroup.addTool('Length', { + configuration: { volumeUID: volumeId }, + }) + this.stackToolGroup.setToolActive('Length', { + bindings: [{ mouseButton: 1 }], + }) - let p1, p2 + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - const enabledElement = getEnabledElement(canvas) - const lengthToolState = getToolState(enabledElement, 'Length') - // Can successfully add Length tool to toolStateManager - expect(lengthToolState).toBeDefined() - expect(lengthToolState.length).toBe(1) + afterEach(function () { + csTools3d.destroy() + eventTarget.reset() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('stack') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) - const lengthToolData = lengthToolState[0] - expect(lengthToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - expect(lengthToolData.metadata.toolName).toBe('Length') - expect(lengthToolData.data.invalidated).toBe(false) + it('Should successfully create a length tool on a canvas with mouse drag - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ) - const data = lengthToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + let p1, p2 + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const lengthToolState = getToolState(enabledElement, 'Length') + // Can successfully add Length tool to toolStateManager + expect(lengthToolState).toBeDefined() + expect(lengthToolState.length).toBe(1) + + const lengthToolData = lengthToolState[0] + expect(lengthToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(lengthToolData.metadata.toolName).toBe('Length') + expect(lengthToolData.data.invalidated).toBe(false) + + const data = lengthToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + expect(data[targets[0]].length).toBe(calculateLength(p1, p2)) + removeToolState(canvas, lengthToolData) + done() + } + ) + } - expect(data[targets[0]].length).toBe(calculateLength(p1, p2)) - removeToolState(canvas, lengthToolData) - done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [32, 32, 0] - const index2 = [10, 1, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - p1 = worldCoord1 - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - p2 = worldCoord2 - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [32, 32, 0] + const index2 = [10, 1, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + p1 = worldCoord1 + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + p2 = worldCoord2 + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + // Since there is tool rendering happening for any mouse event + // we just attach a listener before the last one -> mouse up + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) }) - document.dispatchEvent(evt) - // Mouse Up instantly after - evt = new MouseEvent('mouseup') + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - // Since there is tool rendering happening for any mouse event - // we just attach a listener before the last one -> mouse up - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } }) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) + it('Should successfully create a length tool on a canvas with mouse drag in a Volume viewport - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.ORTHOGRAPHIC, + 512, + 128 + ) - it('Should successfully create a length tool on a canvas with mouse drag in a Volume viewport - 512 x 128', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.ORTHOGRAPHIC, - 512, - 128 - ) + const vp = this.renderingEngine.getViewport(viewportUID) - const vp = this.renderingEngine.getViewport(viewportUID) + let p1, p2 - let p1, p2 + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const lengthToolState = getToolState(enabledElement, 'Length') + // Can successfully add Length tool to toolStateManager + expect(lengthToolState).toBeDefined() + expect(lengthToolState.length).toBe(1) - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - const enabledElement = getEnabledElement(canvas) - const lengthToolState = getToolState(enabledElement, 'Length') - // Can successfully add Length tool to toolStateManager - expect(lengthToolState).toBeDefined() - expect(lengthToolState.length).toBe(1) + const lengthToolData = lengthToolState[0] + expect(lengthToolData.metadata.toolName).toBe('Length') + expect(lengthToolData.data.invalidated).toBe(false) + expect(lengthToolData.data.active).toBe(false) - const lengthToolData = lengthToolState[0] - expect(lengthToolData.metadata.toolName).toBe('Length') - expect(lengthToolData.data.invalidated).toBe(false) - expect(lengthToolData.data.active).toBe(false) + const data = lengthToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) - const data = lengthToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + expect(data[targets[0]].length).toBe(calculateLength(p1, p2)) - expect(data[targets[0]].length).toBe(calculateLength(p1, p2)) + removeToolState(canvas, lengthToolData) + done() + } + ) + } - removeToolState(canvas, lengthToolData) - done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [32, 32, 4] - const index2 = [10, 1, 4] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - p1 = worldCoord1 - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - p2 = worldCoord2 - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [32, 32, 4] + const index2 = [10, 1, 4] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + p1 = worldCoord1 + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + p2 = worldCoord2 + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) }) - document.dispatchEvent(evt) - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) - }) - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId }]) - ctScene.render() - }) - } catch (e) { - done.fail(e) - } - }) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - it('Should successfully create a length tool and modify its handle', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 256, - 256 - ) + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId }]) + ctScene.render() + }) + } catch (e) { + done.fail(e) + } + }) - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + it('Should successfully create a length tool and modify its handle', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ) - let p2, p3 + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + let p2, p3 + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const lengthToolState = getToolState(enabledElement, 'Length') + // Can successfully add Length tool to toolStateManager + expect(lengthToolState).toBeDefined() + expect(lengthToolState.length).toBe(1) + + const lengthToolData = lengthToolState[0] + expect(lengthToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(lengthToolData.metadata.toolName).toBe('Length') + expect(lengthToolData.data.invalidated).toBe(false) + expect(lengthToolData.data.active).toBe(false) + + const data = lengthToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + expect(data[targets[0]].length).toBe(calculateLength(p3, p2)) + + removeToolState(canvas, lengthToolData) + done() + } + ) + } + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [50, 50, 0] + const index2 = [5, 5, 0] + const index3 = [33, 33, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: p1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + p2 = worldCoord2 + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp) + p3 = worldCoord3 + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + document.dispatchEvent(evt) + + // Select the first handle + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Drag it somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - const enabledElement = getEnabledElement(canvas) - const lengthToolState = getToolState(enabledElement, 'Length') - // Can successfully add Length tool to toolStateManager - expect(lengthToolState).toBeDefined() - expect(lengthToolState.length).toBe(1) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - const lengthToolData = lengthToolState[0] - expect(lengthToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - expect(lengthToolData.metadata.toolName).toBe('Length') - expect(lengthToolData.data.invalidated).toBe(false) - expect(lengthToolData.data.active).toBe(false) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } + }) - const data = lengthToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + it('Should successfully create a length tool and select but not move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ) - expect(data[targets[0]].length).toBe(calculateLength(p3, p2)) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + let p1, p2 + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const lengthToolState = getToolState(enabledElement, 'Length') + // Can successfully add Length tool to toolStateManager + expect(lengthToolState).toBeDefined() + expect(lengthToolState.length).toBe(1) + + const lengthToolData = lengthToolState[0] + expect(lengthToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(lengthToolData.metadata.toolName).toBe('Length') + expect(lengthToolData.data.invalidated).toBe(false) + expect(lengthToolData.data.active).toBe(false) + + const data = lengthToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + expect(data[targets[0]].length).toBe(calculateLength(p1, p2)) + + removeToolState(canvas, lengthToolData) + done() + } + ) + } - removeToolState(canvas, lengthToolData) - done() - } - ) - } - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [50, 50, 0] - const index2 = [5, 5, 0] - const index3 = [33, 33, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: p1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - p2 = worldCoord2 - - const { - pageX: pageX3, - pageY: pageY3, - clientX: clientX3, - clientY: clientY3, - worldCoord: worldCoord3, - } = createNormalizedMouseEvent(vtkImageData, index3, canvas, vp) - p3 = worldCoord3 - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [20, 20, 0] + const index2 = [20, 30, 0] + + // grab the tool in its middle (just to make it easy) + const index3 = [20, 25, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + p1 = worldCoord1 + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + p2 = worldCoord2 + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + document.dispatchEvent(evt) + + // Mouse down on the middle of the length tool, just to select + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }) + canvas.dispatchEvent(evt) + + // Just grab and don't really move it + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) }) - document.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - document.dispatchEvent(evt) - - // Select the first handle - evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Drag it somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX3, - clientY: clientY3, - pageX: pageX3, - pageY: pageY3, - }) - document.dispatchEvent(evt) - // Mouse Up instantly after - evt = new MouseEvent('mouseup') + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } }) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should successfully create a length tool and select but not move it', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 256, - 256 - ) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) - - let p1, p2 + it('Should successfully create a length tool and select AND move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ) - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - const enabledElement = getEnabledElement(canvas) - const lengthToolState = getToolState(enabledElement, 'Length') - // Can successfully add Length tool to toolStateManager - expect(lengthToolState).toBeDefined() - expect(lengthToolState.length).toBe(1) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + let p1, p2, p3, p4 + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const lengthToolState = getToolState(enabledElement, 'Length') + // Can successfully add Length tool to toolStateManager + expect(lengthToolState).toBeDefined() + expect(lengthToolState.length).toBe(1) + + const lengthToolData = lengthToolState[0] + expect(lengthToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(lengthToolData.metadata.toolName).toBe('Length') + expect(lengthToolData.data.invalidated).toBe(false) + + const data = lengthToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + // We don't expect the length to change on tool move + expect(data[targets[0]].length).toBeCloseTo( + calculateLength(p1, p2), + 6 + ) + + const handles = lengthToolData.data.handles.points + + const preMoveFirstHandle = p1 + const preMoveSecondHandle = p2 + const preMoveCenter = p3 + + const centerToHandle1 = [ + preMoveCenter[0] - preMoveFirstHandle[0], + preMoveCenter[1] - preMoveFirstHandle[1], + preMoveCenter[2] - preMoveFirstHandle[2], + ] + + const centerToHandle2 = [ + preMoveCenter[0] - preMoveSecondHandle[0], + preMoveCenter[1] - preMoveSecondHandle[1], + preMoveCenter[2] - preMoveSecondHandle[2], + ] + + const afterMoveCenter = p4 + + const afterMoveFirstHandle = [ + afterMoveCenter[0] - centerToHandle1[0], + afterMoveCenter[1] - centerToHandle1[1], + afterMoveCenter[2] - centerToHandle1[2], + ] + + const afterMoveSecondHandle = [ + afterMoveCenter[0] - centerToHandle2[0], + afterMoveCenter[1] - centerToHandle2[1], + afterMoveCenter[2] - centerToHandle2[2], + ] + + // Expect handles are moved accordingly + expect(handles[0]).toEqual(afterMoveFirstHandle) + expect(handles[1]).toEqual(afterMoveSecondHandle) + + removeToolState(canvas, lengthToolData) + done() + } + ) + } - const lengthToolData = lengthToolState[0] - expect(lengthToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - expect(lengthToolData.metadata.toolName).toBe('Length') - expect(lengthToolData.data.invalidated).toBe(false) - expect(lengthToolData.data.active).toBe(false) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [20, 20, 0] + const index2 = [20, 30, 0] + + // grab the tool in its middle (just to make it easy) + const index3 = [20, 25, 0] + + // Where to move the center of the tool + const index4 = [40, 40, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + p1 = worldCoord1 + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + p2 = worldCoord2 + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp) + p3 = worldCoord3 + + const { + pageX: pageX4, + pageY: pageY4, + clientX: clientX4, + clientY: clientY4, + worldCoord: worldCoord4, + } = createNormalizedMouseEvent(imageData, index4, canvas, vp) + p4 = worldCoord4 + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + document.dispatchEvent(evt) + + // Drag the middle of the tool + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }) + canvas.dispatchEvent(evt) + + // Move the middle of the tool to point4 + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX4, + clientY: clientY4, + pageX: pageX4, + pageY: pageY4, + }) + document.dispatchEvent(evt) + + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) - const data = lengthToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - expect(data[targets[0]].length).toBe(calculateLength(p1, p2)) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } + }) + }) - removeToolState(canvas, lengthToolData) - done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [20, 20, 0] - const index2 = [20, 30, 0] - - // grab the tool in its middle (just to make it easy) - const index3 = [20, 25, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - p1 = worldCoord1 - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - p2 = worldCoord2 - - const { - pageX: pageX3, - pageY: pageY3, - clientX: clientX3, - clientY: clientY3, - worldCoord: worldCoord3, - } = createNormalizedMouseEvent(vtkImageData, index3, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, + describe('Should successfully cancel a LengthTool', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(LengthTool, {}) + cache.purgeCache() + this.stackToolGroup = ToolGroupManager.createToolGroup('stack') + this.stackToolGroup.addTool('Length', { + configuration: { volumeUID: volumeId }, }) - document.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - document.dispatchEvent(evt) - - // Mouse down on the middle of the length tool, just to select - evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX3, - clientY: clientY3, - pageX: pageX3, - pageY: pageY3, + this.stackToolGroup.setToolActive('Length', { + bindings: [{ mouseButton: 1 }], }) - canvas.dispatchEvent(evt) - // Just grab and don't really move it - evt = new MouseEvent('mouseup') - - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) }) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) + afterEach(function () { + csTools3d.destroy() + eventTarget.reset() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('stack') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) - it('Should successfully create a length tool and select AND move it', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 256, - 256 - ) + it('Should cancel drawing of a LengthTool annotation', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ) - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + let p1, p2 + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [32, 32, 0] + const index2 = [10, 1, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + p1 = worldCoord1 + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + p2 = worldCoord2 + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Cancel the drawing + let e = new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + key: 'Esc', + char: 'Esc', + }) + canvas.dispatchEvent(e) + + e = new KeyboardEvent('keyup', { + bubbles: true, + cancelable: true, + }) + canvas.dispatchEvent(e) + }) - let p1, p2, p3, p4 + const cancelToolDrawing = () => { + const canceledDataUID = cancelActiveManipulations(canvas) + expect(canceledDataUID).toBeDefined() - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { + setTimeout(() => { const enabledElement = getEnabledElement(canvas) const lengthToolState = getToolState(enabledElement, 'Length') // Can successfully add Length tool to toolStateManager @@ -654,291 +956,93 @@ describe('Cornerstone Tools: ', () => { ) expect(lengthToolData.metadata.toolName).toBe('Length') expect(lengthToolData.data.invalidated).toBe(false) + expect(lengthToolData.data.handles.activeHandleIndex).toBe(null) + expect(lengthToolData.data.active).toBe(false) const data = lengthToolData.data.cachedStats const targets = Array.from(Object.keys(data)) expect(targets.length).toBe(1) - // We don't expect the length to change on tool move - expect(data[targets[0]].length).toBeCloseTo( - calculateLength(p1, p2), - 6 - ) - - const handles = lengthToolData.data.handles.points - - const preMoveFirstHandle = p1 - const preMoveSecondHandle = p2 - const preMoveCenter = p3 - - const centerToHandle1 = [ - preMoveCenter[0] - preMoveFirstHandle[0], - preMoveCenter[1] - preMoveFirstHandle[1], - preMoveCenter[2] - preMoveFirstHandle[2], - ] - - const centerToHandle2 = [ - preMoveCenter[0] - preMoveSecondHandle[0], - preMoveCenter[1] - preMoveSecondHandle[1], - preMoveCenter[2] - preMoveSecondHandle[2], - ] - - const afterMoveCenter = p4 - - const afterMoveFirstHandle = [ - afterMoveCenter[0] - centerToHandle1[0], - afterMoveCenter[1] - centerToHandle1[1], - afterMoveCenter[2] - centerToHandle1[2], - ] - - const afterMoveSecondHandle = [ - afterMoveCenter[0] - centerToHandle2[0], - afterMoveCenter[1] - centerToHandle2[1], - afterMoveCenter[2] - centerToHandle2[2], - ] - - // Expect handles are moved accordingly - expect(handles[0]).toEqual(afterMoveFirstHandle) - expect(handles[1]).toEqual(afterMoveSecondHandle) - + expect(data[targets[0]].length).toBe(calculateLength(p1, p2)) removeToolState(canvas, lengthToolData) done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [20, 20, 0] - const index2 = [20, 30, 0] - - // grab the tool in its middle (just to make it easy) - const index3 = [20, 25, 0] - - // Where to move the center of the tool - const index4 = [40, 40, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - p1 = worldCoord1 - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - p2 = worldCoord2 - - const { - pageX: pageX3, - pageY: pageY3, - clientX: clientX3, - clientY: clientY3, - worldCoord: worldCoord3, - } = createNormalizedMouseEvent(vtkImageData, index3, canvas, vp) - p3 = worldCoord3 - - const { - pageX: pageX4, - pageY: pageY4, - clientX: clientX4, - clientY: clientY4, - worldCoord: worldCoord4, - } = createNormalizedMouseEvent(vtkImageData, index4, canvas, vp) - p4 = worldCoord4 - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, - }) - document.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - document.dispatchEvent(evt) - - // Drag the middle of the tool - evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX3, - clientY: clientY3, - pageX: pageX3, - pageY: pageY3, - }) - canvas.dispatchEvent(evt) - - // Move the middle of the tool to point4 - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX4, - clientY: clientY4, - pageX: pageX4, - pageY: pageY4, - }) - document.dispatchEvent(evt) - - evt = new MouseEvent('mouseup') - - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) - }) - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) -}) + }, 100) + } -describe('Should successfully cancel a LengthTool', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(LengthTool, {}) - cache.purgeCache() - this.stackToolGroup = ToolGroupManager.createToolGroup('stack') - this.stackToolGroup.addTool('Length', { - configuration: { volumeUID: volumeId }, - }) - this.stackToolGroup.setToolActive('Length', { - bindings: [{ mouseButton: 1 }], - }) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) + canvas.addEventListener( + CornerstoneTools3DEvents.KEY_DOWN, + cancelToolDrawing + ) - afterEach(function () { - csTools3d.destroy() - eventTarget.reset() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') - - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) } }) }) - it('Should cancel drawing of a LengthTool annotation', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 512, - 128 - ) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) - - let p1, p2 - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [32, 32, 0] - const index2 = [10, 1, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - p1 = worldCoord1 - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - p2 = worldCoord2 - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, + describe('Calibration ', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(LengthTool, {}) + cache.purgeCache() + this.stackToolGroup = ToolGroupManager.createToolGroup('stack') + this.stackToolGroup.addTool('Length', { + configuration: {}, }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, + this.stackToolGroup.setToolActive('Length', { + bindings: [{ mouseButton: 1 }], }) - document.dispatchEvent(evt) - - // Cancel the drawing - let e = new KeyboardEvent('keydown', { - bubbles: true, - cancelable: true, - key: 'Esc', - char: 'Esc', - }) - canvas.dispatchEvent(e) - e = new KeyboardEvent('keyup', { - bubbles: true, - cancelable: true, + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + metaData.addProvider( + calibratedPixelSpacingMetadataProvider.get.bind( + calibratedPixelSpacingMetadataProvider + ), + 11000 + ) + }) + + afterEach(function () { + csTools3d.destroy() + eventTarget.reset() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('stack') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } }) - canvas.dispatchEvent(e) }) - const cancelToolDrawing = () => { - const canceledDataUID = cancelActiveManipulations(canvas) - expect(canceledDataUID).toBeDefined() + it('Should be able to calibrate an image and update the tool', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ) + + const imageId1 = 'fakeImageLoader:imageURI_64_64_4_40_1_1_0_1' + + const vp = this.renderingEngine.getViewport(viewportUID) - setTimeout(() => { + const secondCallback = () => { const enabledElement = getEnabledElement(canvas) const lengthToolState = getToolState(enabledElement, 'Length') // Can successfully add Length tool to toolStateManager @@ -946,191 +1050,93 @@ describe('Should successfully cancel a LengthTool', () => { expect(lengthToolState.length).toBe(1) const lengthToolData = lengthToolState[0] - expect(lengthToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) expect(lengthToolData.metadata.toolName).toBe('Length') expect(lengthToolData.data.invalidated).toBe(false) - expect(lengthToolData.data.handles.activeHandleIndex).toBe(null) expect(lengthToolData.data.active).toBe(false) const data = lengthToolData.data.cachedStats const targets = Array.from(Object.keys(data)) expect(targets.length).toBe(1) - expect(data[targets[0]].length).toBe(calculateLength(p1, p2)) + // Todo: add calibrated spacing length check + // expect(data[targets[0]].length).toBe(calculateLength(p1, p2)) + removeToolState(canvas, lengthToolData) done() - }, 100) - } - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - canvas.addEventListener( - CornerstoneTools3DEvents.KEY_DOWN, - cancelToolDrawing - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) -}) + } -describe('Calibration ', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(LengthTool, {}) - cache.purgeCache() - this.stackToolGroup = ToolGroupManager.createToolGroup('stack') - this.stackToolGroup.addTool('Length', { - configuration: {}, - }) - this.stackToolGroup.setToolActive('Length', { - bindings: [{ mouseButton: 1 }], - }) + const firstCallback = () => { + const index1 = [32, 32, 0] + const index2 = [10, 1, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + // Since there is tool rendering happening for any mouse event + // we just attach a listener before the last one -> mouse up + document.dispatchEvent(evt) + + const imageId = this.renderingEngine + .getViewport(viewportUID) + .getCurrentImageId() + + calibrateImageSpacing(imageId, this.renderingEngine, 1, 5) + canvas.removeEventListener(EVENTS.IMAGE_RENDERED, firstCallback) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, secondCallback) + } - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - metaData.addProvider( - calibratedPixelSpacingMetadataProvider.get.bind( - calibratedPixelSpacingMetadataProvider - ), - 11000 - ) - }) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, firstCallback) - afterEach(function () { - csTools3d.destroy() - eventTarget.reset() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') - - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) + + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) } }) }) - - it('Should be able to calibrate an image and update the tool', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 256, - 256 - ) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_4_40_1_1_0_1' - - const vp = this.renderingEngine.getViewport(viewportUID) - - const secondCallback = () => { - const enabledElement = getEnabledElement(canvas) - const lengthToolState = getToolState(enabledElement, 'Length') - // Can successfully add Length tool to toolStateManager - expect(lengthToolState).toBeDefined() - expect(lengthToolState.length).toBe(1) - - const lengthToolData = lengthToolState[0] - expect(lengthToolData.metadata.toolName).toBe('Length') - expect(lengthToolData.data.invalidated).toBe(false) - expect(lengthToolData.data.active).toBe(false) - - const data = lengthToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) - - // Todo: add calibrated spacing length check - // expect(data[targets[0]].length).toBe(calculateLength(p1, p2)) - - removeToolState(canvas, lengthToolData) - done() - } - - const firstCallback = () => { - const index1 = [32, 32, 0] - const index2 = [10, 1, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, - }) - document.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - - // Since there is tool rendering happening for any mouse event - // we just attach a listener before the last one -> mouse up - document.dispatchEvent(evt) - - const imageId = this.renderingEngine - .getViewport(viewportUID) - .getCurrentImageId() - - calibrateImageSpacing(imageId, this.renderingEngine, 1, 5) - canvas.removeEventListener(EVENTS.IMAGE_RENDERED, firstCallback) - canvas.addEventListener(EVENTS.IMAGE_RENDERED, secondCallback) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, firstCallback) - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) }) diff --git a/packages/cornerstone-tools/test/ProbeTool_test.js b/packages/cornerstone-tools/test/ProbeTool_test.js index 6d464b51c1..ba7a4e4fb0 100644 --- a/packages/cornerstone-tools/test/ProbeTool_test.js +++ b/packages/cornerstone-tools/test/ProbeTool_test.js @@ -79,527 +79,757 @@ function createCanvas(renderingEngine, viewportType, width, height) { return canvas } -describe('Cornerstone Tools: ', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(ProbeTool, {}) - cache.purgeCache() - this.stackToolGroup = ToolGroupManager.createToolGroup('stack') - this.stackToolGroup.addTool('Probe', { - configuration: { volumeUID: volumeId }, // Only for volume viewport - }) - this.stackToolGroup.setToolActive('Probe', { - bindings: [{ mouseButton: 1 }], - }) - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) +describe('ProbeTool:', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) }) - afterEach(function () { - csTools3d.destroy() - eventTarget.reset() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') - - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } + describe('Cornerstone Tools: ', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(ProbeTool, {}) + cache.purgeCache() + this.stackToolGroup = ToolGroupManager.createToolGroup('stack') + this.stackToolGroup.addTool('Probe', { + configuration: { volumeUID: volumeId }, // Only for volume viewport + }) + this.stackToolGroup.setToolActive('Probe', { + bindings: [{ mouseButton: 1 }], + }) + + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) }) - }) - it('Should successfully click to put a probe tool on a canvas - 512 x 128', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 512, - 128 - ) + afterEach(function () { + csTools3d.destroy() + eventTarget.reset() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('stack') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + it('Should successfully click to put a probe tool on a canvas - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ) - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - // Can successfully add probe tool to toolStateManager - const enabledElement = getEnabledElement(canvas) - const probeToolState = getToolState(enabledElement, 'Probe') - expect(probeToolState).toBeDefined() - expect(probeToolState.length).toBe(1) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + // Can successfully add probe tool to toolStateManager + const enabledElement = getEnabledElement(canvas) + const probeToolState = getToolState(enabledElement, 'Probe') + expect(probeToolState).toBeDefined() + expect(probeToolState.length).toBe(1) + + const probeToolData = probeToolState[0] + expect(probeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(probeToolData.metadata.toolName).toBe('Probe') + expect(probeToolData.data.invalidated).toBe(false) + + const data = probeToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(255) + + removeToolState(canvas, probeToolData) + done() + } + ) + } - const probeToolData = probeToolState[0] - expect(probeToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - expect(probeToolData.metadata.toolName).toBe('Probe') - expect(probeToolData.data.invalidated).toBe(false) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 20, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + }) + canvas.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + // Since there is tool rendering happening for any mouse event + // we just attach a listener before the last one -> mouse up + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) - const data = probeToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - // The world coordinate is on the white bar so value is 255 - expect(data[targets[0]].value).toBe(255) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } + }) - removeToolState(canvas, probeToolData) - done() - } + it('Should successfully click to put two probe tools on a canvas - 256 x 256', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [11, 20, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + // Can successfully add probe tool to toolStateManager + const enabledElement = getEnabledElement(canvas) + const probeToolState = getToolState(enabledElement, 'Probe') + expect(probeToolState).toBeDefined() + expect(probeToolState.length).toBe(2) + + const firstProbeToolData = probeToolState[0] + expect(firstProbeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(firstProbeToolData.metadata.toolName).toBe('Probe') + expect(firstProbeToolData.data.invalidated).toBe(false) + + let data = firstProbeToolData.data.cachedStats + let targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(255) + + // Second click + const secondProbeToolData = probeToolState[1] + expect(secondProbeToolData.metadata.toolName).toBe('Probe') + expect(secondProbeToolData.data.invalidated).toBe(false) + + data = secondProbeToolData.data.cachedStats + targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(0) + + // + removeToolState(canvas, firstProbeToolData) + removeToolState(canvas, secondProbeToolData) + + done() + } + ) + } + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 20, 0] // 255 + const index2 = [20, 20, 0] // 0 + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + + // Mouse Down + let evt1 = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + }) + canvas.dispatchEvent(evt1) + + // Mouse Up instantly after + evt1 = new MouseEvent('mouseup') + document.dispatchEvent(evt1) + + // Mouse Down + let evt2 = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + }) + canvas.dispatchEvent(evt2) + + // Mouse Up instantly after + evt2 = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt2) }) - canvas.dispatchEvent(evt) - // Mouse Up instantly after - evt = new MouseEvent('mouseup') + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - // Since there is tool rendering happening for any mouse event - // we just attach a listener before the last one -> mouse up - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } }) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should successfully click to put two probe tools on a canvas - 256 x 256', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 256, - 256 - ) + it('Should successfully click to put a probe tool on a canvas - 256 x 512', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 512 + ) - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + const imageId1 = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + // Can successfully add probe tool to toolStateManager + const enabledElement = getEnabledElement(canvas) + const probeToolState = getToolState(enabledElement, 'Probe') + expect(probeToolState).toBeDefined() + expect(probeToolState.length).toBe(1) + + const probeToolData = probeToolState[0] + expect(probeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(probeToolData.metadata.toolName).toBe('Probe') + expect(probeToolData.data.invalidated).toBe(false) + + const data = probeToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(255) + + removeToolState(canvas, probeToolData) + done() + } + ) + } - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - // Can successfully add probe tool to toolStateManager - const enabledElement = getEnabledElement(canvas) - const probeToolState = getToolState(enabledElement, 'Probe') - expect(probeToolState).toBeDefined() - expect(probeToolState.length).toBe(2) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [150, 100, 0] // 255 + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + }) + canvas.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) - const firstProbeToolData = probeToolState[0] - expect(firstProbeToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - expect(firstProbeToolData.metadata.toolName).toBe('Probe') - expect(firstProbeToolData.data.invalidated).toBe(false) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - let data = firstProbeToolData.data.cachedStats - let targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } + }) - // The world coordinate is on the white bar so value is 255 - expect(data[targets[0]].value).toBe(255) + it('Should successfully click to put a probe tool on a canvas - 256 x 512', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 512 + ) - // Second click - const secondProbeToolData = probeToolState[1] - expect(secondProbeToolData.metadata.toolName).toBe('Probe') - expect(secondProbeToolData.data.invalidated).toBe(false) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + // Can successfully add probe tool to toolStateManager + const enabledElement = getEnabledElement(canvas) + const probeToolState = getToolState(enabledElement, 'Probe') + expect(probeToolState).toBeDefined() + expect(probeToolState.length).toBe(1) + + const probeToolData = probeToolState[0] + expect(probeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(probeToolData.metadata.toolName).toBe('Probe') + expect(probeToolData.data.invalidated).toBe(false) + + const data = probeToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(0) + + removeToolState(canvas, probeToolData) + done() + } + ) + } - data = secondProbeToolData.data.cachedStats - targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [35, 35, 0] // 0 + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + }) + canvas.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) - // The world coordinate is on the white bar so value is 255 - expect(data[targets[0]].value).toBe(0) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - // - removeToolState(canvas, firstProbeToolData) - removeToolState(canvas, secondProbeToolData) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } + }) - done() - } + it('Should successfully create a prob tool on a canvas with mouse drag in a Volume viewport - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.ORTHOGRAPHIC, + 512, + 128 ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [11, 20, 0] // 255 - const index2 = [20, 20, 0] // 0 - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - - // Mouse Down - let evt1 = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - }) - canvas.dispatchEvent(evt1) - - // Mouse Up instantly after - evt1 = new MouseEvent('mouseup') - document.dispatchEvent(evt1) - - // Mouse Down - let evt2 = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - }) - canvas.dispatchEvent(evt2) - // Mouse Up instantly after - evt2 = new MouseEvent('mouseup') + const vp = this.renderingEngine.getViewport(viewportUID) - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt2) - }) + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const probeToolState = getToolState(enabledElement, 'Probe') + // Can successfully add Length tool to toolStateManager + expect(probeToolState).toBeDefined() + expect(probeToolState.length).toBe(1) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) + const probeToolData = probeToolState[0] + expect(probeToolData.metadata.toolName).toBe('Probe') + expect(probeToolData.data.invalidated).toBe(false) - it('Should successfully click to put a probe tool on a canvas - 256 x 512', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 256, - 512 - ) + const data = probeToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) - const imageId1 = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + expect(data[targets[0]].value).toBe(255) - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - // Can successfully add probe tool to toolStateManager - const enabledElement = getEnabledElement(canvas) - const probeToolState = getToolState(enabledElement, 'Probe') - expect(probeToolState).toBeDefined() - expect(probeToolState.length).toBe(1) + removeToolState(canvas, probeToolData) + done() + } + ) + } - const probeToolData = probeToolState[0] - expect(probeToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - expect(probeToolData.metadata.toolName).toBe('Probe') - expect(probeToolData.data.invalidated).toBe(false) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [50, 50, 4] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) - const data = probeToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - // The world coordinate is on the white bar so value is 255 - expect(data[targets[0]].value).toBe(255) + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId }]) + ctScene.render() + }) + } catch (e) { + done.fail(e) + } + }) - removeToolState(canvas, probeToolData) - done() - } + it('Should successfully create a Probe tool and select AND move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [150, 100, 0] // 255 - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - }) - canvas.dispatchEvent(evt) - // Mouse Up instantly after - evt = new MouseEvent('mouseup') + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) - }) + let p2 - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const probeToolState = getToolState(enabledElement, 'Probe') + // Can successfully add Length tool to toolStateManager + expect(probeToolState).toBeDefined() + expect(probeToolState.length).toBe(1) - it('Should successfully click to put a probe tool on a canvas - 256 x 512', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 256, - 512 - ) + const probeToolData = probeToolState[0] + expect(probeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(probeToolData.metadata.toolName).toBe('Probe') + expect(probeToolData.data.invalidated).toBe(false) - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + const data = probeToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - // Can successfully add probe tool to toolStateManager - const enabledElement = getEnabledElement(canvas) - const probeToolState = getToolState(enabledElement, 'Probe') - expect(probeToolState).toBeDefined() - expect(probeToolState.length).toBe(1) + // We expect the probeTool which was original on 255 strip should be 0 now + expect(data[targets[0]].value).toBe(0) - const probeToolData = probeToolState[0] - expect(probeToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - expect(probeToolData.metadata.toolName).toBe('Probe') - expect(probeToolData.data.invalidated).toBe(false) + const handles = probeToolData.data.handles.points - const data = probeToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + expect(handles[0]).toEqual(p2) - // The world coordinate is on the white bar so value is 255 - expect(data[targets[0]].value).toBe(0) + removeToolState(canvas, probeToolData) + done() + } + ) + } - removeToolState(canvas, probeToolData) - done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [35, 35, 0] // 0 - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 20, 0] // 255 + const index2 = [40, 40, 0] // 0 + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + p2 = worldCoord2 + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + document.dispatchEvent(evt) + + // Grab the probe tool again + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) }) - canvas.dispatchEvent(evt) - // Mouse Up instantly after - evt = new MouseEvent('mouseup') + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } }) - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } }) - it('Should successfully create a prob tool on a canvas with mouse drag in a Volume viewport - 512 x 128', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.ORTHOGRAPHIC, - 512, - 128 - ) - - const vp = this.renderingEngine.getViewport(viewportUID) - - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - const enabledElement = getEnabledElement(canvas) - const probeToolState = getToolState(enabledElement, 'Probe') - // Can successfully add Length tool to toolStateManager - expect(probeToolState).toBeDefined() - expect(probeToolState.length).toBe(1) - - const probeToolData = probeToolState[0] - expect(probeToolData.metadata.toolName).toBe('Probe') - expect(probeToolData.data.invalidated).toBe(false) - - const data = probeToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) - - expect(data[targets[0]].value).toBe(255) - - removeToolState(canvas, probeToolData) - done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [50, 50, 4] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, + describe('Should successfully cancel a ProbeTool', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(ProbeTool, {}) + cache.purgeCache() + this.stackToolGroup = ToolGroupManager.createToolGroup('stack') + this.stackToolGroup.addTool('Probe', { + configuration: { volumeUID: volumeId }, // Only for volume viewport + }) + this.stackToolGroup.setToolActive('Probe', { + bindings: [{ mouseButton: 1 }], }) - canvas.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) }) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId }]) - ctScene.render() + afterEach(function () { + csTools3d.destroy() + eventTarget.reset() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('stack') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } }) - } catch (e) { - done.fail(e) - } - }) + }) - it('Should successfully create a Probe tool and select AND move it', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 256, - 256 - ) + it('Should successfully cancel drawing of a ProbeTool', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ) - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + let p2 + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 20, 0] // 255 + const index2 = [40, 40, 0] // 0 + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + p2 = worldCoord2 + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Cancel the drawing + let e = new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + key: 'Esc', + char: 'Esc', + }) + canvas.dispatchEvent(e) + + e = new KeyboardEvent('keyup', { + bubbles: true, + cancelable: true, + }) + canvas.dispatchEvent(e) + }) - let p2 + const cancelToolDrawing = () => { + const canceledDataUID = cancelActiveManipulations(canvas) + expect(canceledDataUID).toBeDefined() - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { + setTimeout(() => { const enabledElement = getEnabledElement(canvas) const probeToolState = getToolState(enabledElement, 'Probe') // Can successfully add Length tool to toolStateManager @@ -612,6 +842,7 @@ describe('Cornerstone Tools: ', () => { ) expect(probeToolData.metadata.toolName).toBe('Probe') expect(probeToolData.data.invalidated).toBe(false) + expect(probeToolData.data.active).toBe(false) const data = probeToolData.data.cachedStats const targets = Array.from(Object.keys(data)) @@ -626,250 +857,151 @@ describe('Cornerstone Tools: ', () => { removeToolState(canvas, probeToolData) done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [11, 20, 0] // 255 - const index2 = [40, 40, 0] // 0 - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - p2 = worldCoord2 - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - document.dispatchEvent(evt) - - // Grab the probe tool again - evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, - }) - document.dispatchEvent(evt) - - evt = new MouseEvent('mouseup') - - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) - }) - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) -}) - -describe('Should successfully cancel a ProbeTool', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(ProbeTool, {}) - cache.purgeCache() - this.stackToolGroup = ToolGroupManager.createToolGroup('stack') - this.stackToolGroup.addTool('Probe', { - configuration: { volumeUID: volumeId }, // Only for volume viewport - }) - this.stackToolGroup.setToolActive('Probe', { - bindings: [{ mouseButton: 1 }], - }) + }, 100) + } - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) + canvas.addEventListener( + CornerstoneTools3DEvents.KEY_DOWN, + cancelToolDrawing + ) - afterEach(function () { - csTools3d.destroy() - eventTarget.reset() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') - - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) } }) }) - it('Should successfully cancel drawing of a ProbeTool', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 256, - 256 - ) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) - - let p2 - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [11, 20, 0] // 255 - const index2 = [40, 40, 0] // 0 - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - p2 = worldCoord2 - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, + describe('Flipped image: ', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(ProbeTool, {}) + cache.purgeCache() + this.stackToolGroup = ToolGroupManager.createToolGroup('stack') + this.stackToolGroup.addTool('Probe', { + configuration: { volumeUID: volumeId }, // Only for volume viewport }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, + this.stackToolGroup.setToolActive('Probe', { + bindings: [{ mouseButton: 1 }], }) - document.dispatchEvent(evt) - - // Cancel the drawing - let e = new KeyboardEvent('keydown', { - bubbles: true, - cancelable: true, - key: 'Esc', - char: 'Esc', - }) - canvas.dispatchEvent(e) - e = new KeyboardEvent('keyup', { - bubbles: true, - cancelable: true, - }) - canvas.dispatchEvent(e) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) }) - const cancelToolDrawing = () => { - const canceledDataUID = cancelActiveManipulations(canvas) - expect(canceledDataUID).toBeDefined() + afterEach(function () { + csTools3d.destroy() + eventTarget.reset() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('stack') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) - setTimeout(() => { - const enabledElement = getEnabledElement(canvas) - const probeToolState = getToolState(enabledElement, 'Probe') - // Can successfully add Length tool to toolStateManager - expect(probeToolState).toBeDefined() - expect(probeToolState.length).toBe(1) + it('Should successfully click retrieve information from a flipped image by probe', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ) - const probeToolData = probeToolState[0] - expect(probeToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + // Can successfully add probe tool to toolStateManager + const enabledElement = getEnabledElement(canvas) + const probeToolState = getToolState(enabledElement, 'Probe') + expect(probeToolState).toBeDefined() + expect(probeToolState.length).toBe(1) + + const probeToolData = probeToolState[0] + expect(probeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(probeToolData.metadata.toolName).toBe('Probe') + expect(probeToolData.data.invalidated).toBe(false) + + const data = probeToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(255) + + removeToolState(canvas, probeToolData) + done() + } ) - expect(probeToolData.metadata.toolName).toBe('Probe') - expect(probeToolData.data.invalidated).toBe(false) - expect(probeToolData.data.active).toBe(false) - - const data = probeToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) - - // We expect the probeTool which was original on 255 strip should be 0 now - expect(data[targets[0]].value).toBe(0) - - const handles = probeToolData.data.handles.points - - expect(handles[0]).toEqual(p2) - - removeToolState(canvas, probeToolData) - done() - }, 100) - } - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - canvas.addEventListener( - CornerstoneTools3DEvents.KEY_DOWN, - cancelToolDrawing - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } + } + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 20, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + }) + canvas.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + // Since there is tool rendering happening for any mouse event + // we just attach a listener before the last one -> mouse up + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) + + try { + vp.setStack([imageId1], 0).then(() => { + vp.setProperties({ flipHorizontal: true }) + vp.render() + }) + } catch (e) { + done.fail(e) + } + }) }) }) diff --git a/packages/cornerstone-tools/test/RectangleROI_test.js b/packages/cornerstone-tools/test/RectangleROI_test.js index cab90072e5..69b790caf1 100644 --- a/packages/cornerstone-tools/test/RectangleROI_test.js +++ b/packages/cornerstone-tools/test/RectangleROI_test.js @@ -79,558 +79,913 @@ function createCanvas(renderingEngine, viewportType, width, height) { const volumeId = `fakeVolumeLoader:volumeURI_100_100_4_1_1_1_0` -describe('Cornerstone Tools: ', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(RectangleRoiTool, {}) - cache.purgeCache() - this.stackToolGroup = ToolGroupManager.createToolGroup('stack') - this.stackToolGroup.addTool('RectangleRoi', { - configuration: { volumeUID: volumeId }, - }) - this.stackToolGroup.setToolActive('RectangleRoi', { - bindings: [{ mouseButton: 1 }], - }) - - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) - - afterEach(function () { - csTools3d.destroy() - cache.purgeCache() - eventTarget.reset() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') - - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - }) +describe('RectangleRoiTool:', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) }) - it('Should successfully create a rectangle tool on a canvas with mouse drag - 512 x 128', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 512, - 128 - ) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) - - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - const enabledElement = getEnabledElement(canvas) - const rectangleToolState = getToolState( - enabledElement, - 'RectangleRoi' - ) - // Can successfully add rectangleROI to toolStateManager - expect(rectangleToolState).toBeDefined() - expect(rectangleToolState.length).toBe(1) - - const rectangleToolData = rectangleToolState[0] - expect(rectangleToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - - expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi') - expect(rectangleToolData.data.invalidated).toBe(false) - - const data = rectangleToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) - - // the rectangle is drawn on the strip - expect(data[targets[0]].mean).toBe(255) - - removeToolState(canvas, rectangleToolData) - done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [11, 5, 0] - const index2 = [14, 10, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, + describe('Cornerstone Tools: ', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(RectangleRoiTool, {}) + cache.purgeCache() + this.stackToolGroup = ToolGroupManager.createToolGroup('stack') + this.stackToolGroup.addTool('RectangleRoi', { + configuration: { volumeUID: volumeId }, }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, + this.stackToolGroup.setToolActive('RectangleRoi', { + bindings: [{ mouseButton: 1 }], }) - document.dispatchEvent(evt) - // Mouse Up instantly after - evt = new MouseEvent('mouseup') + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + afterEach(function () { + csTools3d.destroy() + cache.purgeCache() + eventTarget.reset() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('stack') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) }) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) + it('Should successfully create a rectangle tool on a canvas with mouse drag - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ) - it('Should successfully create a rectangle tool on a canvas with mouse drag in a Volume viewport - 512 x 128', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.ORTHOGRAPHIC, - 512, - 128 - ) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const rectangleToolState = getToolState( + enabledElement, + 'RectangleRoi' + ) + // Can successfully add rectangleROI to toolStateManager + expect(rectangleToolState).toBeDefined() + expect(rectangleToolState.length).toBe(1) + + const rectangleToolData = rectangleToolState[0] + expect(rectangleToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + + expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi') + expect(rectangleToolData.data.invalidated).toBe(false) + + const data = rectangleToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + // the rectangle is drawn on the strip + expect(data[targets[0]].mean).toBe(255) + + removeToolState(canvas, rectangleToolData) + done() + } + ) + } - const vp = this.renderingEngine.getViewport(viewportUID) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 5, 0] + const index2 = [14, 10, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - const enabledElement = getEnabledElement(canvas) - const rectangleToolState = getToolState( - enabledElement, - 'RectangleRoi' - ) - // Can successfully add rectangleROI to toolStateManager - expect(rectangleToolState).toBeDefined() - expect(rectangleToolState.length).toBe(1) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - const rectangleToolData = rectangleToolState[0] - expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi') - expect(rectangleToolData.data.invalidated).toBe(false) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } + }) - const data = rectangleToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + it('Should successfully create a rectangle tool on a canvas with mouse drag in a Volume viewport - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.ORTHOGRAPHIC, + 512, + 128 + ) - expect(data[targets[0]].mean).toBe(255) - expect(data[targets[0]].stdDev).toBe(0) + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const rectangleToolState = getToolState( + enabledElement, + 'RectangleRoi' + ) + // Can successfully add rectangleROI to toolStateManager + expect(rectangleToolState).toBeDefined() + expect(rectangleToolState.length).toBe(1) + + const rectangleToolData = rectangleToolState[0] + expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi') + expect(rectangleToolData.data.invalidated).toBe(false) + + const data = rectangleToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + expect(data[targets[0]].mean).toBe(255) + expect(data[targets[0]].stdDev).toBe(0) + + removeToolState(canvas, rectangleToolData) + done() + } + ) + } - removeToolState(canvas, rectangleToolData) - done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - // Inside the strip which is from 50-75 in slice 2 - // volumeURI_100_100_4_1_1_1_0 - // The strip is from - const index1 = [55, 10, 2] - const index2 = [65, 20, 2] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + // Inside the strip which is from 50-75 in slice 2 + // volumeURI_100_100_4_1_1_1_0 + // The strip is from + const index1 = [55, 10, 2] + const index2 = [65, 20, 2] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) }) - document.dispatchEvent(evt) - // Mouse Up instantly after - evt = new MouseEvent('mouseup') + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + const ctScene = this.renderingEngine.getScene(scene1UID) + ctScene.setVolumes([{ volumeUID: volumeId }]) + ctScene.render() + }) + } catch (e) { + done.fail(e) + } }) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { - const ctScene = this.renderingEngine.getScene(scene1UID) - ctScene.setVolumes([{ volumeUID: volumeId }]) - ctScene.render() - }) - } catch (e) { - done.fail(e) - } - }) + it('Should successfully create a rectangle tool and modify its handle', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ) - it('Should successfully create a rectangle tool and modify its handle', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 256, - 256 - ) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const rectangleToolState = getToolState( + enabledElement, + 'RectangleRoi' + ) + // Can successfully add rectangleROI to toolStateManager + expect(rectangleToolState).toBeDefined() + expect(rectangleToolState.length).toBe(1) + + const rectangleToolData = rectangleToolState[0] + expect(rectangleToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi') + expect(rectangleToolData.data.invalidated).toBe(false) + + const data = rectangleToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + expect(data[targets[0]].mean).toBe(255) + expect(data[targets[0]].stdDev).toBe(0) + + removeToolState(canvas, rectangleToolData) + done() + } + ) + } - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 5, 0] + const index2 = [14, 10, 0] + const index3 = [11, 30, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + document.dispatchEvent(evt) + + // Select the first handle + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Drag it somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - const enabledElement = getEnabledElement(canvas) - const rectangleToolState = getToolState( - enabledElement, - 'RectangleRoi' - ) - // Can successfully add rectangleROI to toolStateManager - expect(rectangleToolState).toBeDefined() - expect(rectangleToolState.length).toBe(1) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - const rectangleToolData = rectangleToolState[0] - expect(rectangleToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi') - expect(rectangleToolData.data.invalidated).toBe(false) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } + }) - const data = rectangleToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + it('Should successfully create a rectangle tool and select but not move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 256 + ) - expect(data[targets[0]].mean).toBe(255) - expect(data[targets[0]].stdDev).toBe(0) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const rectangleToolState = getToolState( + enabledElement, + 'RectangleRoi' + ) + // Can successfully add rectangleROI to toolStateManager + expect(rectangleToolState).toBeDefined() + expect(rectangleToolState.length).toBe(1) + + const rectangleToolData = rectangleToolState[0] + expect(rectangleToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi') + expect(rectangleToolData.data.invalidated).toBe(false) + + const data = rectangleToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + expect(data[targets[0]].mean).toBe(255) + expect(data[targets[0]].stdDev).toBe(0) + + removeToolState(canvas, rectangleToolData) + done() + } + ) + } - removeToolState(canvas, rectangleToolData) - done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [11, 5, 0] - const index2 = [14, 10, 0] - const index3 = [11, 30, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - - const { - pageX: pageX3, - pageY: pageY3, - clientX: clientX3, - clientY: clientY3, - worldCoord: worldCoord3, - } = createNormalizedMouseEvent(vtkImageData, index3, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, - }) - document.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - document.dispatchEvent(evt) - - // Select the first handle - evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 5, 0] + const index2 = [14, 30, 0] + + // grab the tool in its middle (just to make it easy) + const index3 = [11, 20, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp) + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + document.dispatchEvent(evt) + + // Mouse down on the middle of the rectangleROI, just to select + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }) + canvas.dispatchEvent(evt) + + // Just grab and don't really move it + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) }) - canvas.dispatchEvent(evt) - - // Drag it somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX3, - clientY: clientY3, - pageX: pageX3, - pageY: pageY3, - }) - document.dispatchEvent(evt) - // Mouse Up instantly after - evt = new MouseEvent('mouseup') + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } }) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) - - it('Should successfully create a rectangle tool and select but not move it', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 512, - 256 - ) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + it('Should successfully create a rectangle tool and select AND move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ) - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { - const enabledElement = getEnabledElement(canvas) - const rectangleToolState = getToolState( - enabledElement, - 'RectangleRoi' - ) - // Can successfully add rectangleROI to toolStateManager - expect(rectangleToolState).toBeDefined() - expect(rectangleToolState.length).toBe(1) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + let p1, p2, p3, p4 + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas) + const rectangleToolState = getToolState( + enabledElement, + 'RectangleRoi' + ) + // Can successfully add rectangleROI to toolStateManager + expect(rectangleToolState).toBeDefined() + expect(rectangleToolState.length).toBe(1) + + const rectangleToolData = rectangleToolState[0] + expect(rectangleToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ) + expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi') + expect(rectangleToolData.data.invalidated).toBe(false) + + const data = rectangleToolData.data.cachedStats + const targets = Array.from(Object.keys(data)) + expect(targets.length).toBe(1) + + // We expect the mean to not be 255 as it has been moved + expect(data[targets[0]].mean).not.toBe(255) + expect(data[targets[0]].stdDev).not.toBe(0) + + const handles = rectangleToolData.data.handles.points + + const preMoveFirstHandle = p1 + const preMoveSecondHandle = p2 + const preMoveCenter = p3 + + const centerToHandle1 = [ + preMoveCenter[0] - preMoveFirstHandle[0], + preMoveCenter[1] - preMoveFirstHandle[1], + preMoveCenter[2] - preMoveFirstHandle[2], + ] + + const centerToHandle2 = [ + preMoveCenter[0] - preMoveSecondHandle[0], + preMoveCenter[1] - preMoveSecondHandle[1], + preMoveCenter[2] - preMoveSecondHandle[2], + ] + + const afterMoveCenter = p4 + + const afterMoveFirstHandle = [ + afterMoveCenter[0] - centerToHandle1[0], + afterMoveCenter[1] - centerToHandle1[1], + afterMoveCenter[2] - centerToHandle1[2], + ] + + const afterMoveSecondHandle = [ + afterMoveCenter[0] - centerToHandle2[0], + afterMoveCenter[1] - centerToHandle2[1], + afterMoveCenter[2] - centerToHandle2[2], + ] + + // Expect handles are moved accordingly + expect(handles[0]).toEqual(afterMoveFirstHandle) + expect(handles[3]).toEqual(afterMoveSecondHandle) + + removeToolState(canvas, rectangleToolData) + done() + } + ) + } - const rectangleToolData = rectangleToolState[0] - expect(rectangleToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi') - expect(rectangleToolData.data.invalidated).toBe(false) + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 5, 0] + const index2 = [14, 30, 0] + + // grab the tool on its left edge + const index3 = [11, 25, 0] + + // Where to move that grabbing point + // This will result the tool be outside of the bar + const index4 = [13, 24, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + p1 = worldCoord1 + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + p2 = worldCoord2 + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp) + p3 = worldCoord3 + + const { + pageX: pageX4, + pageY: pageY4, + clientX: clientX4, + clientY: clientY4, + worldCoord: worldCoord4, + } = createNormalizedMouseEvent(imageData, index4, canvas, vp) + p4 = worldCoord4 + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + document.dispatchEvent(evt) + + // Drag the middle of the tool + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }) + canvas.dispatchEvent(evt) + + // Move the middle of the tool to point4 + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX4, + clientY: clientY4, + pageX: pageX4, + pageY: pageY4, + }) + document.dispatchEvent(evt) + + evt = new MouseEvent('mouseup') + + addEventListenerForAnnotationRendered() + document.dispatchEvent(evt) + }) - const data = rectangleToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - expect(data[targets[0]].mean).toBe(255) - expect(data[targets[0]].stdDev).toBe(0) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) + } + }) + }) - removeToolState(canvas, rectangleToolData) - done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [11, 5, 0] - const index2 = [14, 30, 0] - - // grab the tool in its middle (just to make it easy) - const index3 = [11, 20, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - - const { - pageX: pageX3, - pageY: pageY3, - clientX: clientX3, - clientY: clientY3, - worldCoord: worldCoord3, - } = createNormalizedMouseEvent(vtkImageData, index3, canvas, vp) - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, + describe('Should successfully cancel a RectangleROI tool', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(RectangleRoiTool, {}) + cache.purgeCache() + this.stackToolGroup = ToolGroupManager.createToolGroup('stack') + this.stackToolGroup.addTool('RectangleRoi', { + configuration: { volumeUID: volumeId }, }) - document.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - document.dispatchEvent(evt) - - // Mouse down on the middle of the rectangleROI, just to select - evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX3, - clientY: clientY3, - pageX: pageX3, - pageY: pageY3, + this.stackToolGroup.setToolActive('RectangleRoi', { + bindings: [{ mouseButton: 1 }], }) - canvas.dispatchEvent(evt) - - // Just grab and don't really move it - evt = new MouseEvent('mouseup') - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerImageLoader('fakeImageLoader', fakeImageLoader) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) }) - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) + afterEach(function () { + csTools3d.destroy() + cache.purgeCache() + eventTarget.reset() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('stack') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) - it('Should successfully create a rectangle tool and select AND move it', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 512, - 128 - ) + it('Should successfully create a rectangle tool and select AND move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ) - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' + const vp = this.renderingEngine.getViewport(viewportUID) + + let p1, p2, p3, p4 + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 5, 0] + const index2 = [14, 30, 0] + + // grab the tool on its left edge + const index3 = [11, 25, 0] + + // Where to move that grabbing point + // This will result the tool be outside of the bar + const index4 = [13, 24, 0] + + const { imageData } = vp.getImageData() + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp) + p1 = worldCoord1 + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp) + p2 = worldCoord2 + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp) + p3 = worldCoord3 + + const { + pageX: pageX4, + pageY: pageY4, + clientX: clientX4, + clientY: clientY4, + worldCoord: worldCoord4, + } = createNormalizedMouseEvent(imageData, index4, canvas, vp) + p4 = worldCoord4 + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }) + canvas.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }) + document.dispatchEvent(evt) + + // Mouse Up instantly after + evt = new MouseEvent('mouseup') + document.dispatchEvent(evt) + + // Drag the middle of the tool + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }) + canvas.dispatchEvent(evt) + + // Move the middle of the tool to point4 + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX4, + clientY: clientY4, + pageX: pageX4, + pageY: pageY4, + }) + document.dispatchEvent(evt) + + // Cancel the drawing + let e = new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + key: 'Esc', + char: 'Esc', + }) + canvas.dispatchEvent(e) + + e = new KeyboardEvent('keyup', { + bubbles: true, + cancelable: true, + }) + canvas.dispatchEvent(e) + }) - let p1, p2, p3, p4 + const cancelToolDrawing = () => { + const canceledDataUID = cancelActiveManipulations(canvas) + expect(canceledDataUID).toBeDefined() - const addEventListenerForAnnotationRendered = () => { - canvas.addEventListener( - CornerstoneTools3DEvents.ANNOTATION_RENDERED, - () => { + setTimeout(() => { const enabledElement = getEnabledElement(canvas) const rectangleToolState = getToolState( enabledElement, @@ -693,372 +1048,26 @@ describe('Cornerstone Tools: ', () => { removeToolState(canvas, rectangleToolData) done() - } - ) - } - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [11, 5, 0] - const index2 = [14, 30, 0] - - // grab the tool on its left edge - const index3 = [11, 25, 0] - - // Where to move that grabbing point - // This will result the tool be outside of the bar - const index4 = [13, 24, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - p1 = worldCoord1 - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - p2 = worldCoord2 - - const { - pageX: pageX3, - pageY: pageY3, - clientX: clientX3, - clientY: clientY3, - worldCoord: worldCoord3, - } = createNormalizedMouseEvent(vtkImageData, index3, canvas, vp) - p3 = worldCoord3 - - const { - pageX: pageX4, - pageY: pageY4, - clientX: clientX4, - clientY: clientY4, - worldCoord: worldCoord4, - } = createNormalizedMouseEvent(vtkImageData, index4, canvas, vp) - p4 = worldCoord4 - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, - }) - document.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - document.dispatchEvent(evt) - - // Drag the middle of the tool - evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX3, - clientY: clientY3, - pageX: pageX3, - pageY: pageY3, - }) - canvas.dispatchEvent(evt) - - // Move the middle of the tool to point4 - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX4, - clientY: clientY4, - pageX: pageX4, - pageY: pageY4, - }) - document.dispatchEvent(evt) - - evt = new MouseEvent('mouseup') - - addEventListenerForAnnotationRendered() - document.dispatchEvent(evt) - }) - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) -}) + }, 100) + } -describe('Should successfully cancel a RectangleROI tool', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(RectangleRoiTool, {}) - cache.purgeCache() - this.stackToolGroup = ToolGroupManager.createToolGroup('stack') - this.stackToolGroup.addTool('RectangleRoi', { - configuration: { volumeUID: volumeId }, - }) - this.stackToolGroup.setToolActive('RectangleRoi', { - bindings: [{ mouseButton: 1 }], - }) + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ) - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerImageLoader('fakeImageLoader', fakeImageLoader) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) + canvas.addEventListener( + CornerstoneTools3DEvents.KEY_DOWN, + cancelToolDrawing + ) - afterEach(function () { - csTools3d.destroy() - cache.purgeCache() - eventTarget.reset() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('stack') - - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) + try { + vp.setStack([imageId1], 0) + this.renderingEngine.render() + } catch (e) { + done.fail(e) } }) }) - - it('Should successfully create a rectangle tool and select AND move it', function (done) { - const canvas = createCanvas( - this.renderingEngine, - VIEWPORT_TYPE.STACK, - 512, - 128 - ) - - const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0' - const vp = this.renderingEngine.getViewport(viewportUID) - - let p1, p2, p3, p4 - - canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const index1 = [11, 5, 0] - const index2 = [14, 30, 0] - - // grab the tool on its left edge - const index3 = [11, 25, 0] - - // Where to move that grabbing point - // This will result the tool be outside of the bar - const index4 = [13, 24, 0] - - const { vtkImageData } = vp.getImageData() - - const { - pageX: pageX1, - pageY: pageY1, - clientX: clientX1, - clientY: clientY1, - worldCoord: worldCoord1, - } = createNormalizedMouseEvent(vtkImageData, index1, canvas, vp) - p1 = worldCoord1 - - const { - pageX: pageX2, - pageY: pageY2, - clientX: clientX2, - clientY: clientY2, - worldCoord: worldCoord2, - } = createNormalizedMouseEvent(vtkImageData, index2, canvas, vp) - p2 = worldCoord2 - - const { - pageX: pageX3, - pageY: pageY3, - clientX: clientX3, - clientY: clientY3, - worldCoord: worldCoord3, - } = createNormalizedMouseEvent(vtkImageData, index3, canvas, vp) - p3 = worldCoord3 - - const { - pageX: pageX4, - pageY: pageY4, - clientX: clientX4, - clientY: clientY4, - worldCoord: worldCoord4, - } = createNormalizedMouseEvent(vtkImageData, index4, canvas, vp) - p4 = worldCoord4 - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX1, - clientY: clientY1, - pageX: pageX1, - pageY: pageY1, - }) - canvas.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX2, - clientY: clientY2, - pageX: pageX2, - pageY: pageY2, - }) - document.dispatchEvent(evt) - - // Mouse Up instantly after - evt = new MouseEvent('mouseup') - document.dispatchEvent(evt) - - // Drag the middle of the tool - evt = new MouseEvent('mousedown', { - target: canvas, - buttons: 1, - clientX: clientX3, - clientY: clientY3, - pageX: pageX3, - pageY: pageY3, - }) - canvas.dispatchEvent(evt) - - // Move the middle of the tool to point4 - evt = new MouseEvent('mousemove', { - target: canvas, - buttons: 1, - clientX: clientX4, - clientY: clientY4, - pageX: pageX4, - pageY: pageY4, - }) - document.dispatchEvent(evt) - - // Cancel the drawing - let e = new KeyboardEvent('keydown', { - bubbles: true, - cancelable: true, - key: 'Esc', - char: 'Esc', - }) - canvas.dispatchEvent(e) - - e = new KeyboardEvent('keyup', { - bubbles: true, - cancelable: true, - }) - canvas.dispatchEvent(e) - }) - - const cancelToolDrawing = () => { - const canceledDataUID = cancelActiveManipulations(canvas) - expect(canceledDataUID).toBeDefined() - - setTimeout(() => { - const enabledElement = getEnabledElement(canvas) - const rectangleToolState = getToolState(enabledElement, 'RectangleRoi') - // Can successfully add rectangleROI to toolStateManager - expect(rectangleToolState).toBeDefined() - expect(rectangleToolState.length).toBe(1) - - const rectangleToolData = rectangleToolState[0] - expect(rectangleToolData.metadata.referencedImageId).toBe( - imageId1.split(':')[1] - ) - expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi') - expect(rectangleToolData.data.invalidated).toBe(false) - - const data = rectangleToolData.data.cachedStats - const targets = Array.from(Object.keys(data)) - expect(targets.length).toBe(1) - - // We expect the mean to not be 255 as it has been moved - expect(data[targets[0]].mean).not.toBe(255) - expect(data[targets[0]].stdDev).not.toBe(0) - - const handles = rectangleToolData.data.handles.points - - const preMoveFirstHandle = p1 - const preMoveSecondHandle = p2 - const preMoveCenter = p3 - - const centerToHandle1 = [ - preMoveCenter[0] - preMoveFirstHandle[0], - preMoveCenter[1] - preMoveFirstHandle[1], - preMoveCenter[2] - preMoveFirstHandle[2], - ] - - const centerToHandle2 = [ - preMoveCenter[0] - preMoveSecondHandle[0], - preMoveCenter[1] - preMoveSecondHandle[1], - preMoveCenter[2] - preMoveSecondHandle[2], - ] - - const afterMoveCenter = p4 - - const afterMoveFirstHandle = [ - afterMoveCenter[0] - centerToHandle1[0], - afterMoveCenter[1] - centerToHandle1[1], - afterMoveCenter[2] - centerToHandle1[2], - ] - - const afterMoveSecondHandle = [ - afterMoveCenter[0] - centerToHandle2[0], - afterMoveCenter[1] - centerToHandle2[1], - afterMoveCenter[2] - centerToHandle2[2], - ] - - // Expect handles are moved accordingly - expect(handles[0]).toEqual(afterMoveFirstHandle) - expect(handles[3]).toEqual(afterMoveSecondHandle) - - removeToolState(canvas, rectangleToolData) - done() - }, 100) - } - - this.stackToolGroup.addViewports( - this.renderingEngine.uid, - undefined, - vp.uid - ) - - canvas.addEventListener( - CornerstoneTools3DEvents.KEY_DOWN, - cancelToolDrawing - ) - - try { - vp.setStack([imageId1], 0) - this.renderingEngine.render() - } catch (e) { - done.fail(e) - } - }) }) diff --git a/packages/cornerstone-tools/test/StackScrollToolMouseWheelTool_test.js b/packages/cornerstone-tools/test/StackScrollToolMouseWheelTool_test.js index 1e314cf2b6..346086d773 100644 --- a/packages/cornerstone-tools/test/StackScrollToolMouseWheelTool_test.js +++ b/packages/cornerstone-tools/test/StackScrollToolMouseWheelTool_test.js @@ -19,11 +19,7 @@ const { registerVolumeLoader, } = cornerstone3D -const { - StackScrollMouseWheelTool, - ToolGroupManager, - CornerstoneTools3DEvents, -} = csTools3d +const { StackScrollMouseWheelTool, ToolGroupManager } = csTools3d const { fakeImageLoader, @@ -78,6 +74,10 @@ function createCanvas(renderingEngine, viewportType, width, height) { } describe('Cornerstone Tools Scroll Wheel: ', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) + }) + beforeEach(function () { csTools3d.init() csTools3d.addTool(StackScrollMouseWheelTool, {}) @@ -120,10 +120,10 @@ describe('Cornerstone Tools Scroll Wheel: ', () => { const renderEventHandler = () => { const index1 = [50, 50, 4] - const { vtkImageData } = vp.getImageData() + const { imageData } = vp.getImageData() const { pageX: pageX1, pageY: pageY1 } = createNormalizedMouseEvent( - vtkImageData, + imageData, index1, canvas, vp @@ -194,10 +194,10 @@ describe('Cornerstone Tools Scroll Wheel: ', () => { // First render is the actual image render const index1 = [50, 50, 4] - const { vtkImageData } = vp.getImageData() + const { imageData } = vp.getImageData() const { pageX: pageX1, pageY: pageY1 } = createNormalizedMouseEvent( - vtkImageData, + imageData, index1, canvas, vp @@ -241,9 +241,10 @@ describe('Cornerstone Tools Scroll Wheel: ', () => { ) try { - vp.setStack([imageId1, imageId2], 0) - vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) - this.renderingEngine.render() + vp.setStack([imageId1, imageId2], 0).then(() => { + vp.setProperties({ interpolationType: INTERPOLATION_TYPE.NEAREST }) + vp.render() + }) } catch (e) { done.fail(e) } diff --git a/packages/cornerstone-tools/test/ToolGroupManager_test.js b/packages/cornerstone-tools/test/ToolGroupManager_test.js index 9e30b57c7d..a88c505f91 100644 --- a/packages/cornerstone-tools/test/ToolGroupManager_test.js +++ b/packages/cornerstone-tools/test/ToolGroupManager_test.js @@ -68,301 +68,307 @@ function createCanvas(width, height) { return [canvas1, canvas2] } -describe('Synchronizer Manager: ', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(ProbeTool, {}) - cache.purgeCache() - this.toolGroup = ToolGroupManager.createToolGroup('volume1') - this.toolGroup.addTool('Probe') - this.toolGroup.setToolActive('Probe', { - bindings: [ - { - mouseButton: ToolBindings.Mouse.Primary, - }, - ], - }) - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) +describe('ToolGroupManager', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) }) - afterEach(function () { - // Destroy synchronizer manager to test it first since csTools3D also destroy - // synchronizers - ToolGroupManager.destroy() - csTools3d.destroy() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } + describe('Synchronizer Manager1: ', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(ProbeTool, {}) + cache.purgeCache() + this.toolGroup = ToolGroupManager.createToolGroup('volume1') + this.toolGroup.addTool('Probe') + this.toolGroup.setToolActive('Probe', { + bindings: [ + { + mouseButton: ToolBindings.Mouse.Primary, + }, + ], + }) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) }) - }) - it('Should successfully creates tool groups', function () { - const [canvas1, canvas2] = createCanvas(512, 128) - - this.renderingEngine.setViewports([ - { - sceneUID: scene1UID, - viewportUID: viewportUID1, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas1, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + afterEach(function () { + // Destroy synchronizer manager to test it first since csTools3D also destroy + // synchronizers + ToolGroupManager.destroy() + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('Should successfully creates tool groups', function () { + const [canvas1, canvas2] = createCanvas(512, 128) + + this.renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: viewportUID1, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - { - sceneUID: scene2UID, - viewportUID: viewportUID2, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas2, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + { + sceneUID: scene2UID, + viewportUID: viewportUID2, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - ]) + ]) - this.toolGroup.addViewports( - this.renderingEngine.uid, - scene1UID, - viewportUID1 - ) + this.toolGroup.addViewports( + this.renderingEngine.uid, + scene1UID, + viewportUID1 + ) - const tg = ToolGroupManager.getToolGroupById('volume1') - expect(tg).toBeDefined() + const tg = ToolGroupManager.getToolGroupById('volume1') + expect(tg).toBeDefined() + }) }) -}) -describe('Synchronizer Manager: ', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(ProbeTool, {}) - cache.purgeCache() - this.toolGroup = ToolGroupManager.createToolGroup('volume1') - this.toolGroup.addTool('Probe') - this.toolGroup.setToolActive('Probe', { - bindings: [ - { - mouseButton: ToolBindings.Mouse.Primary, - }, - ], + describe('Synchronizer Manager2: ', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(ProbeTool, {}) + cache.purgeCache() + this.toolGroup = ToolGroupManager.createToolGroup('volume1') + this.toolGroup.addTool('Probe') + this.toolGroup.setToolActive('Probe', { + bindings: [ + { + mouseButton: ToolBindings.Mouse.Primary, + }, + ], + }) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) }) - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) - afterEach(function () { - // Destroy synchronizer manager to test it first since csTools3D also destroy - // synchronizers - ToolGroupManager.destroyToolGroupById('volume1') - csTools3d.destroy() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } + afterEach(function () { + // Destroy synchronizer manager to test it first since csTools3D also destroy + // synchronizers + ToolGroupManager.destroyToolGroupById('volume1') + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) }) - }) - it('Should successfully create toolGroup and get tool instances', function () { - const [canvas1, canvas2] = createCanvas(512, 128) - - this.renderingEngine.setViewports([ - { - sceneUID: scene1UID, - viewportUID: viewportUID1, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas1, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + it('Should successfully create toolGroup and get tool instances', function () { + const [canvas1, canvas2] = createCanvas(512, 128) + + this.renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: viewportUID1, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - { - sceneUID: scene2UID, - viewportUID: viewportUID2, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas2, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + { + sceneUID: scene2UID, + viewportUID: viewportUID2, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - ]) - - this.toolGroup.addViewports( - this.renderingEngine.uid, - scene1UID, - viewportUID1 - ) - - const tg = ToolGroupManager.getToolGroupById('volume1') - expect(tg).toBeDefined() - - const tg2 = ToolGroupManager.getToolGroups( - renderingEngineUID, - scene1UID, - viewportUID1 - ) - expect(tg2).toBeDefined() - expect(tg2.length).toBe(1) - expect(tg).toBe(tg2[0]) - - const tg3 = ToolGroupManager.createToolGroup('volume1') - expect(tg3).toBeUndefined() - - const instance = tg.getToolInstance('Probe') - expect(instance.name).toBe('Probe') - - const instance2 = tg.getToolInstance('probe') - expect(instance2).toBeUndefined() - }) + ]) + + this.toolGroup.addViewports( + this.renderingEngine.uid, + scene1UID, + viewportUID1 + ) + + const tg = ToolGroupManager.getToolGroupById('volume1') + expect(tg).toBeDefined() + + const tg2 = ToolGroupManager.getToolGroups( + renderingEngineUID, + scene1UID, + viewportUID1 + ) + expect(tg2).toBeDefined() + expect(tg2.length).toBe(1) + expect(tg).toBe(tg2[0]) + + const tg3 = ToolGroupManager.createToolGroup('volume1') + expect(tg3).toBeUndefined() + + const instance = tg.getToolInstance('Probe') + expect(instance.name).toBe('Probe') + + const instance2 = tg.getToolInstance('probe') + expect(instance2).toBeUndefined() + }) + + it('Should successfully Use toolGroup manager API', function () { + const [canvas1, canvas2] = createCanvas(512, 128) - it('Should successfully Use toolGroup manager API', function () { - const [canvas1, canvas2] = createCanvas(512, 128) - - this.renderingEngine.setViewports([ - { - sceneUID: scene1UID, - viewportUID: viewportUID1, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas1, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + this.renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: viewportUID1, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - { - sceneUID: scene2UID, - viewportUID: viewportUID2, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas2, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + { + sceneUID: scene2UID, + viewportUID: viewportUID2, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - ]) + ]) - // Remove viewports - let tg = ToolGroupManager.getToolGroupById('volume1') + // Remove viewports + let tg = ToolGroupManager.getToolGroupById('volume1') - tg.addViewports(this.renderingEngine.uid, scene1UID, viewportUID1) - expect(tg.viewports.length).toBe(1) + tg.addViewports(this.renderingEngine.uid, scene1UID, viewportUID1) + expect(tg.viewports.length).toBe(1) - tg.removeViewports(renderingEngineUID) + tg.removeViewports(renderingEngineUID) - tg = ToolGroupManager.getToolGroupById('volume1') - expect(tg.viewports.length).toBe(0) + tg = ToolGroupManager.getToolGroupById('volume1') + expect(tg.viewports.length).toBe(0) - // - tg.addViewports(this.renderingEngine.uid, scene1UID, viewportUID1) - tg = ToolGroupManager.getToolGroupById('volume1') - expect(tg.viewports.length).toBe(1) + // + tg.addViewports(this.renderingEngine.uid, scene1UID, viewportUID1) + tg = ToolGroupManager.getToolGroupById('volume1') + expect(tg.viewports.length).toBe(1) - tg.removeViewports(renderingEngineUID, scene2UID, viewportUID2) - expect(tg.viewports.length).toBe(1) - }) + tg.removeViewports(renderingEngineUID, scene2UID, viewportUID2) + expect(tg.viewports.length).toBe(1) + }) + + it('Should successfully make a tool enabled/disabled/active/passive', function () { + const [canvas1, canvas2] = createCanvas(512, 128) - it('Should successfully make a tool enabled/disabled/active/passive', function () { - const [canvas1, canvas2] = createCanvas(512, 128) - - this.renderingEngine.setViewports([ - { - sceneUID: scene1UID, - viewportUID: viewportUID1, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas1, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + this.renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: viewportUID1, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - { - sceneUID: scene2UID, - viewportUID: viewportUID2, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas2, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + { + sceneUID: scene2UID, + viewportUID: viewportUID2, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - ]) - - this.toolGroup.addViewports( - this.renderingEngine.uid, - scene1UID, - viewportUID1 - ) - - // Remove viewports - let tg = ToolGroupManager.getToolGroupById('volume1') - expect(tg._tools['Probe'].mode).toBe('Active') - expect(tg._tools['Length']).toBeUndefined() - - tg.setToolPassive('Probe') - expect(tg._tools['Probe'].mode).toBe('Passive') - }) + ]) + + this.toolGroup.addViewports( + this.renderingEngine.uid, + scene1UID, + viewportUID1 + ) + + // Remove viewports + let tg = ToolGroupManager.getToolGroupById('volume1') + expect(tg._tools['Probe'].mode).toBe('Active') + expect(tg._tools['Length']).toBeUndefined() + + tg.setToolPassive('Probe') + expect(tg._tools['Probe'].mode).toBe('Passive') + }) - it('Should successfully setTool status', function () { - const [canvas1, canvas2] = createCanvas(512, 128) - - this.renderingEngine.setViewports([ - { - sceneUID: scene1UID, - viewportUID: viewportUID1, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas1, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + it('Should successfully setTool status', function () { + const [canvas1, canvas2] = createCanvas(512, 128) + + this.renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: viewportUID1, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - { - sceneUID: scene2UID, - viewportUID: viewportUID2, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas2, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + { + sceneUID: scene2UID, + viewportUID: viewportUID2, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - ]) - - this.toolGroup.addViewports( - this.renderingEngine.uid, - scene1UID, - viewportUID1 - ) - - // Remove viewports - let tg = ToolGroupManager.getToolGroupById('volume1') - tg.setToolActive() - tg.setToolPassive() - tg.setToolEnabled() - tg.setToolDisabled() - - expect(tg._tools['Probe'].mode).toBe('Active') - - csTools3d.addTool(LengthTool, {}) - tg.addTool('Length') - tg.setToolEnabled('Length') - expect(tg._tools['Length'].mode).toBe('Enabled') - - tg.setToolDisabled('Length') - expect(tg._tools['Length'].mode).toBe('Disabled') + ]) + + this.toolGroup.addViewports( + this.renderingEngine.uid, + scene1UID, + viewportUID1 + ) + + // Remove viewports + let tg = ToolGroupManager.getToolGroupById('volume1') + tg.setToolActive() + tg.setToolPassive() + tg.setToolEnabled() + tg.setToolDisabled() + + expect(tg._tools['Probe'].mode).toBe('Active') + + csTools3d.addTool(LengthTool, {}) + tg.addTool('Length') + tg.setToolEnabled('Length') + expect(tg._tools['Length'].mode).toBe('Enabled') + + tg.setToolDisabled('Length') + expect(tg._tools['Length'].mode).toBe('Disabled') + }) }) }) diff --git a/packages/cornerstone-tools/test/cpu_BidirectionalTool_test.js b/packages/cornerstone-tools/test/cpu_BidirectionalTool_test.js new file mode 100644 index 0000000000..735882900a --- /dev/null +++ b/packages/cornerstone-tools/test/cpu_BidirectionalTool_test.js @@ -0,0 +1,848 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index'; +import * as csTools3d from '../src/index'; + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + EVENTS, + Utilities, + registerImageLoader, + unregisterAllImageLoaders, + metaData, + getEnabledElement, + eventTarget, + registerVolumeLoader, + setUseCPURenderingOnlyForDebugOrTests, + resetCPURenderingOnlyForDebugOrTests, +} = cornerstone3D; + +const { + BidirectionalTool, + ToolGroupManager, + getToolState, + removeToolState, + CornerstoneTools3DEvents, + cancelActiveManipulations, +} = csTools3d; + +const { + fakeImageLoader, + fakeVolumeLoader, + fakeMetaDataProvider, + createNormalizedMouseEvent, +} = Utilities.testUtils; + +const renderingEngineUID = Utilities.uuidv4(); + +const viewportUID = 'VIEWPORT'; +const AXIAL = 'AXIAL'; +const DOMElements = []; + +function calculateLength(pos1, pos2) { + const dx = pos1[0] - pos2[0]; + const dy = pos1[1] - pos2[1]; + const dz = pos1[2] - pos2[2]; + + return Math.sqrt(dx * dx + dy * dy + dz * dz); +} + +function createCanvas(renderingEngine, viewportType, width, height) { + // TODO: currently we need to have a parent div on the canvas with + // position of relative for the svg layer to be set correctly + const viewportPane = document.createElement('div'); + viewportPane.style.position = 'relative'; + viewportPane.style.width = `${width}px`; + viewportPane.style.height = `${height}px`; + + document.body.appendChild(viewportPane); + + const canvas = document.createElement('canvas'); + + canvas.style.position = 'absolute'; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + viewportPane.appendChild(canvas); + + DOMElements.push(canvas); + DOMElements.push(viewportPane); + + renderingEngine.setViewports([ + { + viewportUID: viewportUID, + type: viewportType, + canvas: canvas, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION[AXIAL], + }, + }, + ]); + return canvas; +} + +const volumeId = `fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0`; + +describe('Bidirectional Tool (CPU): ', () => { + beforeAll(() => { + setUseCPURenderingOnlyForDebugOrTests(true); + }); + + afterAll(() => { + resetCPURenderingOnlyForDebugOrTests(); + }); + + beforeEach(function () { + csTools3d.init(); + csTools3d.addTool(BidirectionalTool, {}); + cache.purgeCache(); + this.stackToolGroup = ToolGroupManager.createToolGroup('stack'); + this.stackToolGroup.addTool('Bidirectional', { + configuration: { volumeUID: volumeId }, + }); + this.stackToolGroup.setToolActive('Bidirectional', { + bindings: [{ mouseButton: 1 }], + }); + + this.renderingEngine = new RenderingEngine(renderingEngineUID); + registerImageLoader('fakeImageLoader', fakeImageLoader); + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader); + metaData.addProvider(fakeMetaDataProvider, 10000); + }); + + afterEach(function () { + csTools3d.destroy(); + cache.purgeCache(); + eventTarget.reset(); + this.renderingEngine.destroy(); + metaData.removeProvider(fakeMetaDataProvider); + unregisterAllImageLoaders(); + ToolGroupManager.destroyToolGroupById('stack'); + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + }); + + it('Should successfully create a Bidirectional tool on a cpu stack viewport with mouse drag - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p1, p2; + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const bidirectionalToolState = getToolState( + enabledElement, + 'Bidirectional' + ); + // Can successfully add Length tool to toolStateManager + expect(bidirectionalToolState).toBeDefined(); + expect(bidirectionalToolState.length).toBe(1); + + const bidirectionalToolData = bidirectionalToolState[0]; + expect(bidirectionalToolData.metadata.toolName).toBe('Bidirectional'); + expect(bidirectionalToolData.data.invalidated).toBe(false); + + const data = bidirectionalToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + expect(data[targets[0]].length).toBe(calculateLength(p1, p2)); + + removeToolState(canvas, bidirectionalToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [32, 32, 0]; + const index2 = [10, 1, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + p1 = worldCoord1; + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a bidirectional tool on a cpu stack viewoprt and modify its handle', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p2, p3; + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const bidirectionalToolState = getToolState( + enabledElement, + 'Bidirectional' + ); + // Can successfully add Length tool to toolStateManager + expect(bidirectionalToolState).toBeDefined(); + expect(bidirectionalToolState.length).toBe(1); + + const bidirectionalToolData = bidirectionalToolState[0]; + expect(bidirectionalToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(bidirectionalToolData.metadata.toolName).toBe('Bidirectional'); + expect(bidirectionalToolData.data.invalidated).toBe(false); + + const data = bidirectionalToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + expect(data[targets[0]].length).toBe(calculateLength(p3, p2)); + + removeToolState(canvas, bidirectionalToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + // Not not to move the handle too much since the length become width and it would fail + const index1 = [50, 50, 0]; + const index2 = [5, 5, 0]; + const index3 = [52, 47, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp); + p3 = worldCoord3; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Select the first handle + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Drag it somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a bidirectional tool on a cpu stack viewport and select but not move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p1, p2; + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const bidirectionalToolState = getToolState( + enabledElement, + 'Bidirectional' + ); + // Can successfully add Length tool to toolStateManager + expect(bidirectionalToolState).toBeDefined(); + expect(bidirectionalToolState.length).toBe(1); + + const bidirectionalToolData = bidirectionalToolState[0]; + expect(bidirectionalToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(bidirectionalToolData.metadata.toolName).toBe('Bidirectional'); + expect(bidirectionalToolData.data.invalidated).toBe(false); + + const data = bidirectionalToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + expect(data[targets[0]].length).toBe(calculateLength(p1, p2)); + + removeToolState(canvas, bidirectionalToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [20, 20, 0]; + const index2 = [20, 30, 0]; + + // grab the tool in its middle (just to make it easy) + const index3 = [20, 25, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + p1 = worldCoord1; + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Mouse down on the middle of the length tool, just to select + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }); + canvas.dispatchEvent(evt); + + // Just grab and don't really move it + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a bidirectional tool on a cpu stack viewoprt and select AND move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p1, p2, p3, p4; + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const bidirectionalToolState = getToolState( + enabledElement, + 'Bidirectional' + ); + // Can successfully add Length tool to toolStateManager + expect(bidirectionalToolState).toBeDefined(); + expect(bidirectionalToolState.length).toBe(1); + + const bidirectionalToolData = bidirectionalToolState[0]; + expect(bidirectionalToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(bidirectionalToolData.metadata.toolName).toBe('Bidirectional'); + expect(bidirectionalToolData.data.invalidated).toBe(false); + + const data = bidirectionalToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // We don't expect the length to change on tool move + expect(data[targets[0]].length).toBeCloseTo( + calculateLength(p1, p2), + 6 + ); + + const handles = bidirectionalToolData.data.handles.points; + + const preMoveFirstHandle = p1; + const preMoveSecondHandle = p2; + const preMoveCenter = p3; + + const centerToHandle1 = [ + preMoveCenter[0] - preMoveFirstHandle[0], + preMoveCenter[1] - preMoveFirstHandle[1], + preMoveCenter[2] - preMoveFirstHandle[2], + ]; + + const centerToHandle2 = [ + preMoveCenter[0] - preMoveSecondHandle[0], + preMoveCenter[1] - preMoveSecondHandle[1], + preMoveCenter[2] - preMoveSecondHandle[2], + ]; + + const afterMoveCenter = p4; + + const afterMoveFirstHandle = [ + afterMoveCenter[0] - centerToHandle1[0], + afterMoveCenter[1] - centerToHandle1[1], + afterMoveCenter[2] - centerToHandle1[2], + ]; + + const afterMoveSecondHandle = [ + afterMoveCenter[0] - centerToHandle2[0], + afterMoveCenter[1] - centerToHandle2[1], + afterMoveCenter[2] - centerToHandle2[2], + ]; + + // Expect handles are moved accordingly + expect(handles[0]).toEqual(afterMoveFirstHandle); + expect(handles[1]).toEqual(afterMoveSecondHandle); + + removeToolState(canvas, bidirectionalToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [20, 20, 0]; + const index2 = [20, 30, 0]; + + // grab the tool in its middle (just to make it easy) + const index3 = [20, 25, 0]; + + // Where to move the center of the tool + const index4 = [40, 40, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + p1 = worldCoord1; + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp); + p3 = worldCoord3; + + const { + pageX: pageX4, + pageY: pageY4, + clientX: clientX4, + clientY: clientY4, + worldCoord: worldCoord4, + } = createNormalizedMouseEvent(imageData, index4, canvas, vp); + p4 = worldCoord4; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Drag the middle of the tool + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }); + canvas.dispatchEvent(evt); + + // Move the middle of the tool to point4 + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX4, + clientY: clientY4, + pageX: pageX4, + pageY: pageY4, + }); + document.dispatchEvent(evt); + + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully cancel drawing of a bidirectional on a cpu stack viewport', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p1, p2; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [32, 32, 4]; + const index2 = [10, 1, 4]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + p1 = worldCoord1; + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Cancel the drawing + let e = new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + key: 'Esc', + char: 'Esc', + }); + canvas.dispatchEvent(e); + + e = new KeyboardEvent('keyup', { + bubbles: true, + cancelable: true, + }); + canvas.dispatchEvent(e); + }); + + const cancelToolDrawing = () => { + const canceledDataUID = cancelActiveManipulations(canvas); + expect(canceledDataUID).toBeDefined(); + + setTimeout(() => { + const enabledElement = getEnabledElement(canvas); + const bidirectionalToolState = getToolState( + enabledElement, + 'Bidirectional' + ); + // Can successfully add Length tool to toolStateManager + expect(bidirectionalToolState).toBeDefined(); + expect(bidirectionalToolState.length).toBe(1); + + const bidirectionalToolData = bidirectionalToolState[0]; + expect(bidirectionalToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(bidirectionalToolData.metadata.toolName).toBe('Bidirectional'); + expect(bidirectionalToolData.data.invalidated).toBe(false); + expect(bidirectionalToolData.data.active).toBe(false); + + const data = bidirectionalToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + expect(data[targets[0]].length).toBe(calculateLength(p1, p2)); + + removeToolState(canvas, bidirectionalToolData); + done(); + }, 100); + }; + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + canvas.addEventListener( + CornerstoneTools3DEvents.KEY_DOWN, + cancelToolDrawing + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); +}); diff --git a/packages/cornerstone-tools/test/cpu_EllipseROI_test.js b/packages/cornerstone-tools/test/cpu_EllipseROI_test.js new file mode 100644 index 0000000000..29e3d48715 --- /dev/null +++ b/packages/cornerstone-tools/test/cpu_EllipseROI_test.js @@ -0,0 +1,363 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index'; +import * as csTools3d from '../src/index'; + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + EVENTS, + Utilities, + registerImageLoader, + unregisterAllImageLoaders, + metaData, + getEnabledElement, + eventTarget, + createAndCacheVolume, + registerVolumeLoader, + setUseCPURenderingOnlyForDebugOrTests, + resetCPURenderingOnlyForDebugOrTests, +} = cornerstone3D; + +const { + EllipticalRoiTool, + ToolGroupManager, + getToolState, + removeToolState, + CornerstoneTools3DEvents, + cancelActiveManipulations, +} = csTools3d; + +const { + fakeImageLoader, + fakeVolumeLoader, + fakeMetaDataProvider, + createNormalizedMouseEvent, +} = Utilities.testUtils; + +const renderingEngineUID = Utilities.uuidv4(); + +const viewportUID = 'VIEWPORT'; + +const AXIAL = 'AXIAL'; + +const DOMElements = []; + +function createCanvas(renderingEngine, viewportType, width, height) { + // TODO: currently we need to have a parent div on the canvas with + // position of relative for the svg layer to be set correctly + const viewportPane = document.createElement('div'); + viewportPane.style.position = 'relative'; + viewportPane.style.width = `${width}px`; + viewportPane.style.height = `${height}px`; + + document.body.appendChild(viewportPane); + + const canvas = document.createElement('canvas'); + + canvas.style.position = 'absolute'; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + viewportPane.appendChild(canvas); + + DOMElements.push(canvas); + DOMElements.push(viewportPane); + + renderingEngine.enableElement({ + viewportUID, + type: viewportType, + canvas: canvas, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION[AXIAL], + }, + }); + + return canvas; +} + +const volumeId = `fakeVolumeLoader:volumeURI_100_100_4_1_1_1_0`; + +describe('EllipticalRoiTool (CPU):', () => { + beforeAll(() => { + setUseCPURenderingOnlyForDebugOrTests(true); + }); + + afterAll(() => { + resetCPURenderingOnlyForDebugOrTests(); + }); + + beforeEach(function () { + csTools3d.init(); + csTools3d.addTool(EllipticalRoiTool, {}); + cache.purgeCache(); + this.stackToolGroup = ToolGroupManager.createToolGroup('stack'); + this.stackToolGroup.addTool('EllipticalRoi', { + configuration: { volumeUID: volumeId }, + }); + this.stackToolGroup.setToolActive('EllipticalRoi', { + bindings: [{ mouseButton: 1 }], + }); + + this.renderingEngine = new RenderingEngine(renderingEngineUID); + registerImageLoader('fakeImageLoader', fakeImageLoader); + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader); + metaData.addProvider(fakeMetaDataProvider, 10000); + }); + + afterEach(function () { + this.renderingEngine.disableElement(viewportUID); + + csTools3d.destroy(); + eventTarget.reset(); + cache.purgeCache(); + this.renderingEngine.destroy(); + metaData.removeProvider(fakeMetaDataProvider); + unregisterAllImageLoaders(); + ToolGroupManager.destroyToolGroupById('stack'); + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + }); + + it('Should successfully create a ellipse tool on a cpu stack viewport with mouse drag - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const ellipseToolState = getToolState( + enabledElement, + 'EllipticalRoi' + ); + // Can successfully add Length tool to toolStateManager + expect(ellipseToolState).toBeDefined(); + expect(ellipseToolState.length).toBe(1); + + const ellipseToolData = ellipseToolState[0]; + expect(ellipseToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + + expect(ellipseToolData.metadata.toolName).toBe('EllipticalRoi'); + expect(ellipseToolData.data.invalidated).toBe(false); + + const data = ellipseToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // the rectangle is drawn on the strip + expect(data[targets[0]].mean).toBe(255); + + removeToolState(canvas, ellipseToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + // Since ellipse draws from center to out, we are picking a very center + // point in the image (strip is 255 from 10-15 in X and from 0-64 in Y) + const index1 = [12, 30, 0]; + const index2 = [14, 40, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should cancel drawing of a EllipseTool annotation on a cpu stack viewport', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p1, p2; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + // Since ellipse draws from center to out, we are picking a very center + // point in the image (strip is 255 from 10-15 in X and from 0-64 in Y) + const index1 = [12, 30, 0]; + const index2 = [14, 40, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Cancel the drawing + let e = new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + key: 'Esc', + char: 'Esc', + }); + canvas.dispatchEvent(e); + + e = new KeyboardEvent('keyup', { + bubbles: true, + cancelable: true, + }); + canvas.dispatchEvent(e); + }); + + const cancelToolDrawing = () => { + const canceledDataUID = cancelActiveManipulations(canvas); + expect(canceledDataUID).toBeDefined(); + + setTimeout(() => { + const enabledElement = getEnabledElement(canvas); + const ellipseToolState = getToolState(enabledElement, 'EllipticalRoi'); + // Can successfully add Length tool to toolStateManager + expect(ellipseToolState).toBeDefined(); + expect(ellipseToolState.length).toBe(1); + + const ellipseToolData = ellipseToolState[0]; + expect(ellipseToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + + expect(ellipseToolData.metadata.toolName).toBe('EllipticalRoi'); + expect(ellipseToolData.data.invalidated).toBe(false); + expect(ellipseToolData.data.active).toBe(false); + + const data = ellipseToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // the rectangle is drawn on the strip + expect(data[targets[0]].mean).toBe(255); + + removeToolState(canvas, ellipseToolData); + done(); + }, 100); + }; + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + canvas.addEventListener( + CornerstoneTools3DEvents.KEY_DOWN, + cancelToolDrawing + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); +}); diff --git a/packages/cornerstone-tools/test/cpu_LengthTool_test.js b/packages/cornerstone-tools/test/cpu_LengthTool_test.js new file mode 100644 index 0000000000..86c13886c6 --- /dev/null +++ b/packages/cornerstone-tools/test/cpu_LengthTool_test.js @@ -0,0 +1,718 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index'; +import * as csTools3d from '../src/index'; + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + EVENTS, + eventTarget, + Utilities, + registerImageLoader, + unregisterAllImageLoaders, + metaData, + getEnabledElement, + registerVolumeLoader, + setUseCPURenderingOnlyForDebugOrTests, + resetCPURenderingOnlyForDebugOrTests, +} = cornerstone3D; + +const { + LengthTool, + ToolGroupManager, + getToolState, + removeToolState, + CornerstoneTools3DEvents, +} = csTools3d; + +const { + fakeImageLoader, + fakeVolumeLoader, + fakeMetaDataProvider, + createNormalizedMouseEvent, +} = Utilities.testUtils; + +const renderingEngineUID = Utilities.uuidv4(); + +const viewportUID = 'VIEWPORT'; + +const AXIAL = 'AXIAL'; + +const DOMElements = []; + +function calculateLength(pos1, pos2) { + const dx = pos1[0] - pos2[0]; + const dy = pos1[1] - pos2[1]; + const dz = pos1[2] - pos2[2]; + + return Math.sqrt(dx * dx + dy * dy + dz * dz); +} + +function createCanvas(renderingEngine, viewportType, width, height) { + // TODO: currently we need to have a parent div on the canvas with + // position of relative for the svg layer to be set correctly + const viewportPane = document.createElement('div'); + viewportPane.style.position = 'relative'; + viewportPane.style.width = `${width}px`; + viewportPane.style.height = `${height}px`; + + document.body.appendChild(viewportPane); + + const canvas = document.createElement('canvas'); + + canvas.style.position = 'absolute'; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + viewportPane.appendChild(canvas); + + DOMElements.push(canvas); + DOMElements.push(viewportPane); + + renderingEngine.setViewports([ + { + viewportUID: viewportUID, + type: viewportType, + canvas: canvas, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION[AXIAL], + }, + }, + ]); + return canvas; +} + +const volumeId = `fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0`; + +describe('Length Tool (CPU):', () => { + beforeAll(() => { + setUseCPURenderingOnlyForDebugOrTests(true); + }); + + afterAll(() => { + resetCPURenderingOnlyForDebugOrTests(); + }); + + beforeEach(function () { + csTools3d.init(); + csTools3d.addTool(LengthTool, {}); + cache.purgeCache(); + this.stackToolGroup = ToolGroupManager.createToolGroup('stack'); + this.stackToolGroup.addTool('Length', { + configuration: { volumeUID: volumeId }, + }); + this.stackToolGroup.setToolActive('Length', { + bindings: [{ mouseButton: 1 }], + }); + + this.renderingEngine = new RenderingEngine(renderingEngineUID); + registerImageLoader('fakeImageLoader', fakeImageLoader); + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader); + metaData.addProvider(fakeMetaDataProvider, 10000); + }); + + afterEach(function () { + csTools3d.destroy(); + eventTarget.reset(); + cache.purgeCache(); + this.renderingEngine.destroy(); + metaData.removeProvider(fakeMetaDataProvider); + unregisterAllImageLoaders(); + ToolGroupManager.destroyToolGroupById('stack'); + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + }); + + it('Should successfully create a length tool on a cpu stack viewport with mouse drag - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p1, p2; + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const lengthToolState = getToolState(enabledElement, 'Length'); + // Can successfully add Length tool to toolStateManager + expect(lengthToolState).toBeDefined(); + expect(lengthToolState.length).toBe(1); + + const lengthToolData = lengthToolState[0]; + expect(lengthToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(lengthToolData.metadata.toolName).toBe('Length'); + expect(lengthToolData.data.invalidated).toBe(false); + + const data = lengthToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + expect(data[targets[0]].length).toBe(calculateLength(p1, p2)); + removeToolState(canvas, lengthToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [30, 30, 0]; + const index2 = [60, 60, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + p1 = worldCoord1; + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + // Since there is tool rendering happening for any mouse event + // we just attach a listener before the last one -> mouse up + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a length tool on a cpu stack viewport and modify its handle', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p2, p3; + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const lengthToolState = getToolState(enabledElement, 'Length'); + // Can successfully add Length tool to toolStateManager + expect(lengthToolState).toBeDefined(); + expect(lengthToolState.length).toBe(1); + + const lengthToolData = lengthToolState[0]; + expect(lengthToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(lengthToolData.metadata.toolName).toBe('Length'); + expect(lengthToolData.data.invalidated).toBe(false); + expect(lengthToolData.data.active).toBe(false); + + const data = lengthToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + expect(data[targets[0]].length).toBe(calculateLength(p3, p2)); + + removeToolState(canvas, lengthToolData); + done(); + } + ); + }; + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [50, 50, 0]; + const index2 = [5, 5, 0]; + const index3 = [33, 33, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: p1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp); + p3 = worldCoord3; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Select the first handle + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Drag it somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a length tool on a cpu stack viewport and select but not move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p1, p2; + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const lengthToolState = getToolState(enabledElement, 'Length'); + // Can successfully add Length tool to toolStateManager + expect(lengthToolState).toBeDefined(); + expect(lengthToolState.length).toBe(1); + + const lengthToolData = lengthToolState[0]; + expect(lengthToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(lengthToolData.metadata.toolName).toBe('Length'); + expect(lengthToolData.data.invalidated).toBe(false); + expect(lengthToolData.data.active).toBe(false); + + const data = lengthToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + expect(data[targets[0]].length).toBe(calculateLength(p1, p2)); + + removeToolState(canvas, lengthToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [20, 20, 0]; + const index2 = [20, 30, 0]; + + // grab the tool in its middle (just to make it easy) + const index3 = [20, 25, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + p1 = worldCoord1; + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Mouse down on the middle of the length tool, just to select + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }); + canvas.dispatchEvent(evt); + + // Just grab and don't really move it + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a length tool on a cpu stack viewport and select AND move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p1, p2, p3, p4; + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const lengthToolState = getToolState(enabledElement, 'Length'); + // Can successfully add Length tool to toolStateManager + expect(lengthToolState).toBeDefined(); + expect(lengthToolState.length).toBe(1); + + const lengthToolData = lengthToolState[0]; + expect(lengthToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(lengthToolData.metadata.toolName).toBe('Length'); + expect(lengthToolData.data.invalidated).toBe(false); + + const data = lengthToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // We don't expect the length to change on tool move + expect(data[targets[0]].length).toBeCloseTo( + calculateLength(p1, p2), + 6 + ); + + const handles = lengthToolData.data.handles.points; + + const preMoveFirstHandle = p1; + const preMoveSecondHandle = p2; + const preMoveCenter = p3; + + const centerToHandle1 = [ + preMoveCenter[0] - preMoveFirstHandle[0], + preMoveCenter[1] - preMoveFirstHandle[1], + preMoveCenter[2] - preMoveFirstHandle[2], + ]; + + const centerToHandle2 = [ + preMoveCenter[0] - preMoveSecondHandle[0], + preMoveCenter[1] - preMoveSecondHandle[1], + preMoveCenter[2] - preMoveSecondHandle[2], + ]; + + const afterMoveCenter = p4; + + const afterMoveFirstHandle = [ + afterMoveCenter[0] - centerToHandle1[0], + afterMoveCenter[1] - centerToHandle1[1], + afterMoveCenter[2] - centerToHandle1[2], + ]; + + const afterMoveSecondHandle = [ + afterMoveCenter[0] - centerToHandle2[0], + afterMoveCenter[1] - centerToHandle2[1], + afterMoveCenter[2] - centerToHandle2[2], + ]; + + // Expect handles are moved accordingly + expect(handles[0]).toEqual(afterMoveFirstHandle); + expect(handles[1]).toEqual(afterMoveSecondHandle); + + removeToolState(canvas, lengthToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [20, 20, 0]; + const index2 = [20, 30, 0]; + + // grab the tool in its middle (just to make it easy) + const index3 = [20, 25, 0]; + + // Where to move the center of the tool + const index4 = [40, 40, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + p1 = worldCoord1; + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp); + p3 = worldCoord3; + + const { + pageX: pageX4, + pageY: pageY4, + clientX: clientX4, + clientY: clientY4, + worldCoord: worldCoord4, + } = createNormalizedMouseEvent(imageData, index4, canvas, vp); + p4 = worldCoord4; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Drag the middle of the tool + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }); + canvas.dispatchEvent(evt); + + // Move the middle of the tool to point4 + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX4, + clientY: clientY4, + pageX: pageX4, + pageY: pageY4, + }); + document.dispatchEvent(evt); + + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); +}); diff --git a/packages/cornerstone-tools/test/cpu_ProbeTool_test.js b/packages/cornerstone-tools/test/cpu_ProbeTool_test.js new file mode 100644 index 0000000000..59afc623f4 --- /dev/null +++ b/packages/cornerstone-tools/test/cpu_ProbeTool_test.js @@ -0,0 +1,766 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index'; +import * as csTools3d from '../src/index'; + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + Utilities, + eventTarget, + registerImageLoader, + unregisterAllImageLoaders, + metaData, + EVENTS, + getEnabledElement, + registerVolumeLoader, + setUseCPURenderingOnlyForDebugOrTests, + resetCPURenderingOnlyForDebugOrTests, +} = cornerstone3D; + +const { + ProbeTool, + ToolGroupManager, + getToolState, + removeToolState, + CornerstoneTools3DEvents, + cancelActiveManipulations, +} = csTools3d; + +const { + fakeImageLoader, + fakeMetaDataProvider, + fakeVolumeLoader, + createNormalizedMouseEvent, +} = Utilities.testUtils; + +const renderingEngineUID = Utilities.uuidv4(); + +const viewportUID = 'VIEWPORT'; + +const AXIAL = 'AXIAL'; + +const DOMElements = []; + +const volumeId = `fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0`; + +function createCanvas(renderingEngine, viewportType, width, height) { + // TODO: currently we need to have a parent div on the canvas with + // position of relative for the svg layer to be set correctly + const viewportPane = document.createElement('div'); + viewportPane.style.position = 'relative'; + viewportPane.style.width = `${width}px`; + viewportPane.style.height = `${height}px`; + + document.body.appendChild(viewportPane); + + const canvas = document.createElement('canvas'); + + canvas.style.position = 'absolute'; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + viewportPane.appendChild(canvas); + + DOMElements.push(canvas); + DOMElements.push(viewportPane); + + renderingEngine.setViewports([ + { + viewportUID: viewportUID, + type: viewportType, + canvas: canvas, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION[AXIAL], + }, + }, + ]); + return canvas; +} + +describe('ProbeTool (CPU):', () => { + beforeAll(() => { + setUseCPURenderingOnlyForDebugOrTests(true); + }); + + afterAll(() => { + resetCPURenderingOnlyForDebugOrTests(); + }); + + beforeEach(function () { + csTools3d.init(); + csTools3d.addTool(ProbeTool, {}); + cache.purgeCache(); + this.stackToolGroup = ToolGroupManager.createToolGroup('stack'); + this.stackToolGroup.addTool('Probe', { + configuration: { volumeUID: volumeId }, // Only for volume viewport + }); + this.stackToolGroup.setToolActive('Probe', { + bindings: [{ mouseButton: 1 }], + }); + + this.renderingEngine = new RenderingEngine(renderingEngineUID); + registerImageLoader('fakeImageLoader', fakeImageLoader); + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader); + metaData.addProvider(fakeMetaDataProvider, 10000); + }); + + afterEach(function () { + csTools3d.destroy(); + eventTarget.reset(); + cache.purgeCache(); + this.renderingEngine.destroy(); + metaData.removeProvider(fakeMetaDataProvider); + unregisterAllImageLoaders(); + ToolGroupManager.destroyToolGroupById('stack'); + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + }); + + it('Should successfully click to put a probe tool on a cpu stack viewport - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + // Can successfully add probe tool to toolStateManager + const enabledElement = getEnabledElement(canvas); + const probeToolState = getToolState(enabledElement, 'Probe'); + expect(probeToolState).toBeDefined(); + expect(probeToolState.length).toBe(1); + + const probeToolData = probeToolState[0]; + expect(probeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(probeToolData.metadata.toolName).toBe('Probe'); + expect(probeToolData.data.invalidated).toBe(false); + + const data = probeToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(255); + + removeToolState(canvas, probeToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 20, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + }); + canvas.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + // Since there is tool rendering happening for any mouse event + // we just attach a listener before the last one -> mouse up + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully click to put two probe tools on a cpu stack viewport - 256 x 256', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + // Can successfully add probe tool to toolStateManager + const enabledElement = getEnabledElement(canvas); + const probeToolState = getToolState(enabledElement, 'Probe'); + expect(probeToolState).toBeDefined(); + expect(probeToolState.length).toBe(2); + + const firstProbeToolData = probeToolState[0]; + expect(firstProbeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(firstProbeToolData.metadata.toolName).toBe('Probe'); + expect(firstProbeToolData.data.invalidated).toBe(false); + + let data = firstProbeToolData.data.cachedStats; + let targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(255); + + // Second click + const secondProbeToolData = probeToolState[1]; + expect(secondProbeToolData.metadata.toolName).toBe('Probe'); + expect(secondProbeToolData.data.invalidated).toBe(false); + + data = secondProbeToolData.data.cachedStats; + targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(0); + + // + removeToolState(canvas, firstProbeToolData); + removeToolState(canvas, secondProbeToolData); + + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 20, 0]; // 255 + const index2 = [20, 20, 0]; // 0 + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + + // Mouse Down + let evt1 = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + }); + canvas.dispatchEvent(evt1); + + // Mouse Up instantly after + evt1 = new MouseEvent('mouseup'); + document.dispatchEvent(evt1); + + // Mouse Down + let evt2 = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + }); + canvas.dispatchEvent(evt2); + + // Mouse Up instantly after + evt2 = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt2); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully click to put a probe tool on a cpu stack viewport - 256 x 512', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 512 + ); + + const imageId1 = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + // Can successfully add probe tool to toolStateManager + const enabledElement = getEnabledElement(canvas); + const probeToolState = getToolState(enabledElement, 'Probe'); + expect(probeToolState).toBeDefined(); + expect(probeToolState.length).toBe(1); + + const probeToolData = probeToolState[0]; + expect(probeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(probeToolData.metadata.toolName).toBe('Probe'); + expect(probeToolData.data.invalidated).toBe(false); + + const data = probeToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(255); + + removeToolState(canvas, probeToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [150, 100, 0]; // 255 + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + }); + canvas.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully click to put a probe tool on a cpu stack viewport - 256 x 512', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 512 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + // Can successfully add probe tool to toolStateManager + const enabledElement = getEnabledElement(canvas); + const probeToolState = getToolState(enabledElement, 'Probe'); + expect(probeToolState).toBeDefined(); + expect(probeToolState.length).toBe(1); + + const probeToolData = probeToolState[0]; + expect(probeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(probeToolData.metadata.toolName).toBe('Probe'); + expect(probeToolData.data.invalidated).toBe(false); + + const data = probeToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // The world coordinate is on the white bar so value is 255 + expect(data[targets[0]].value).toBe(0); + + removeToolState(canvas, probeToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [35, 35, 0]; // 0 + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + }); + canvas.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a Probe tool on a cpu stack viewport and select AND move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p2; + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const probeToolState = getToolState(enabledElement, 'Probe'); + // Can successfully add Length tool to toolStateManager + expect(probeToolState).toBeDefined(); + expect(probeToolState.length).toBe(1); + + const probeToolData = probeToolState[0]; + expect(probeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(probeToolData.metadata.toolName).toBe('Probe'); + expect(probeToolData.data.invalidated).toBe(false); + + const data = probeToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // We expect the probeTool which was original on 255 strip should be 0 now + expect(data[targets[0]].value).toBe(0); + + const handles = probeToolData.data.handles.points; + + expect(handles[0][0]).toEqual(p2[0]); + expect(handles[0][1]).toEqual(p2[1]); + expect(handles[0][2]).toEqual(p2[2]); + + removeToolState(canvas, probeToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 20, 0]; // 255 + const index2 = [40, 40, 0]; // 0 + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Grab the probe tool again + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully cancel drawing of a ProbeTool on a cpu stack viewport', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p2; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 20, 0]; // 255 + const index2 = [40, 40, 0]; // 0 + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Cancel the drawing + let e = new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + key: 'Esc', + char: 'Esc', + }); + canvas.dispatchEvent(e); + + e = new KeyboardEvent('keyup', { + bubbles: true, + cancelable: true, + }); + canvas.dispatchEvent(e); + }); + + const cancelToolDrawing = () => { + const canceledDataUID = cancelActiveManipulations(canvas); + expect(canceledDataUID).toBeDefined(); + + setTimeout(() => { + const enabledElement = getEnabledElement(canvas); + const probeToolState = getToolState(enabledElement, 'Probe'); + // Can successfully add Length tool to toolStateManager + expect(probeToolState).toBeDefined(); + expect(probeToolState.length).toBe(1); + + const probeToolData = probeToolState[0]; + expect(probeToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(probeToolData.metadata.toolName).toBe('Probe'); + expect(probeToolData.data.invalidated).toBe(false); + expect(probeToolData.data.active).toBe(false); + + const data = probeToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // We expect the probeTool which was original on 255 strip should be 0 now + expect(data[targets[0]].value).toBe(0); + + const handles = probeToolData.data.handles.points; + + expect(handles[0][0]).toEqual(p2[0]); + expect(handles[0][1]).toEqual(p2[1]); + expect(handles[0][2]).toEqual(p2[2]); + + removeToolState(canvas, probeToolData); + done(); + }, 100); + }; + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + canvas.addEventListener( + CornerstoneTools3DEvents.KEY_DOWN, + cancelToolDrawing + ); + + try { + vp.setStack([imageId1], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); +}); diff --git a/packages/cornerstone-tools/test/cpu_RectangleROI_test.js b/packages/cornerstone-tools/test/cpu_RectangleROI_test.js new file mode 100644 index 0000000000..a1dffa5a38 --- /dev/null +++ b/packages/cornerstone-tools/test/cpu_RectangleROI_test.js @@ -0,0 +1,927 @@ +import * as cornerstone3D from '../../cornerstone-render/src/index'; +import * as csTools3d from '../src/index'; + +const { + cache, + RenderingEngine, + VIEWPORT_TYPE, + ORIENTATION, + EVENTS, + Utilities, + registerImageLoader, + unregisterAllImageLoaders, + eventTarget, + metaData, + getEnabledElement, + createAndCacheVolume, + registerVolumeLoader, + setUseCPURenderingOnlyForDebugOrTests, + resetCPURenderingOnlyForDebugOrTests, +} = cornerstone3D; + +const { + RectangleRoiTool, + ToolGroupManager, + getToolState, + removeToolState, + CornerstoneTools3DEvents, + cancelActiveManipulations, +} = csTools3d; + +const { + fakeImageLoader, + fakeVolumeLoader, + fakeMetaDataProvider, + createNormalizedMouseEvent, +} = Utilities.testUtils; + +const renderingEngineUID = Utilities.uuidv4(); + +const scene1UID = 'SCENE_1'; +const viewportUID = 'VIEWPORT'; + +const AXIAL = 'AXIAL'; + +const DOMElements = []; + +function createCanvas(renderingEngine, viewportType, width, height) { + // TODO: currently we need to have a parent div on the canvas with + // position of relative for the svg layer to be set correctly + const viewportPane = document.createElement('div'); + viewportPane.style.position = 'relative'; + viewportPane.style.width = `${width}px`; + viewportPane.style.height = `${height}px`; + + document.body.appendChild(viewportPane); + + const canvas = document.createElement('canvas'); + + canvas.style.position = 'absolute'; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + viewportPane.appendChild(canvas); + + DOMElements.push(canvas); + DOMElements.push(viewportPane); + + renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: viewportUID, + type: viewportType, + canvas: canvas, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION[AXIAL], + }, + }, + ]); + return canvas; +} + +const volumeId = `fakeVolumeLoader:volumeURI_100_100_4_1_1_1_0`; + +describe('RectangleRoiTool (CPU):', () => { + beforeAll(() => { + setUseCPURenderingOnlyForDebugOrTests(true); + }); + + afterAll(() => { + resetCPURenderingOnlyForDebugOrTests(); + }); + + beforeEach(function () { + csTools3d.init(); + csTools3d.addTool(RectangleRoiTool, {}); + cache.purgeCache(); + this.stackToolGroup = ToolGroupManager.createToolGroup('stack'); + this.stackToolGroup.addTool('RectangleRoi', { + configuration: { volumeUID: volumeId }, + }); + this.stackToolGroup.setToolActive('RectangleRoi', { + bindings: [{ mouseButton: 1 }], + }); + + this.renderingEngine = new RenderingEngine(renderingEngineUID); + registerImageLoader('fakeImageLoader', fakeImageLoader); + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader); + metaData.addProvider(fakeMetaDataProvider, 10000); + }); + + afterEach(function () { + csTools3d.destroy(); + cache.purgeCache(); + eventTarget.reset(); + this.renderingEngine.destroy(); + metaData.removeProvider(fakeMetaDataProvider); + unregisterAllImageLoaders(); + ToolGroupManager.destroyToolGroupById('stack'); + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + }); + + it('Should successfully create a rectangle tool on a cpu stack viewport with mouse drag - 512 x 128', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const rectangleToolState = getToolState( + enabledElement, + 'RectangleRoi' + ); + // Can successfully add rectangleROI to toolStateManager + expect(rectangleToolState).toBeDefined(); + expect(rectangleToolState.length).toBe(1); + + const rectangleToolData = rectangleToolState[0]; + expect(rectangleToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + + expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi'); + expect(rectangleToolData.data.invalidated).toBe(false); + + const data = rectangleToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // the rectangle is drawn on the strip + expect(data[targets[0]].mean).toBe(255); + + removeToolState(canvas, rectangleToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 5, 0]; + const index2 = [14, 10, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a rectangle tool on a cpu stack viewport and modify its handle', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 256, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const rectangleToolState = getToolState( + enabledElement, + 'RectangleRoi' + ); + // Can successfully add rectangleROI to toolStateManager + expect(rectangleToolState).toBeDefined(); + expect(rectangleToolState.length).toBe(1); + + const rectangleToolData = rectangleToolState[0]; + expect(rectangleToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi'); + expect(rectangleToolData.data.invalidated).toBe(false); + + const data = rectangleToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + expect(data[targets[0]].mean).toBe(255); + expect(data[targets[0]].stdDev).toBe(0); + + removeToolState(canvas, rectangleToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 5, 0]; + const index2 = [14, 10, 0]; + const index3 = [11, 30, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Select the first handle + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Drag it somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a rectangle tool on a cpu stack viewport and select but not move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 256 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const rectangleToolState = getToolState( + enabledElement, + 'RectangleRoi' + ); + // Can successfully add rectangleROI to toolStateManager + expect(rectangleToolState).toBeDefined(); + expect(rectangleToolState.length).toBe(1); + + const rectangleToolData = rectangleToolState[0]; + expect(rectangleToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi'); + expect(rectangleToolData.data.invalidated).toBe(false); + + const data = rectangleToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + expect(data[targets[0]].mean).toBe(255); + expect(data[targets[0]].stdDev).toBe(0); + + removeToolState(canvas, rectangleToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 5, 0]; + const index2 = [14, 30, 0]; + + // grab the tool in its middle (just to make it easy) + const index3 = [11, 20, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Mouse down on the middle of the rectangleROI, just to select + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }); + canvas.dispatchEvent(evt); + + // Just grab and don't really move it + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a rectangle tool on a cpu stack viewport and select AND move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p1, p2, p3, p4; + + const addEventListenerForAnnotationRendered = () => { + canvas.addEventListener( + CornerstoneTools3DEvents.ANNOTATION_RENDERED, + () => { + const enabledElement = getEnabledElement(canvas); + const rectangleToolState = getToolState( + enabledElement, + 'RectangleRoi' + ); + // Can successfully add rectangleROI to toolStateManager + expect(rectangleToolState).toBeDefined(); + expect(rectangleToolState.length).toBe(1); + + const rectangleToolData = rectangleToolState[0]; + expect(rectangleToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi'); + expect(rectangleToolData.data.invalidated).toBe(false); + + const data = rectangleToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // We expect the mean to not be 255 as it has been moved + expect(data[targets[0]].mean).not.toBe(255); + expect(data[targets[0]].stdDev).not.toBe(0); + + const handles = rectangleToolData.data.handles.points; + + const preMoveFirstHandle = p1; + const preMoveSecondHandle = p2; + const preMoveCenter = p3; + + const centerToHandle1 = [ + preMoveCenter[0] - preMoveFirstHandle[0], + preMoveCenter[1] - preMoveFirstHandle[1], + preMoveCenter[2] - preMoveFirstHandle[2], + ]; + + const centerToHandle2 = [ + preMoveCenter[0] - preMoveSecondHandle[0], + preMoveCenter[1] - preMoveSecondHandle[1], + preMoveCenter[2] - preMoveSecondHandle[2], + ]; + + const afterMoveCenter = p4; + + const afterMoveFirstHandle = [ + afterMoveCenter[0] - centerToHandle1[0], + afterMoveCenter[1] - centerToHandle1[1], + afterMoveCenter[2] - centerToHandle1[2], + ]; + + const afterMoveSecondHandle = [ + afterMoveCenter[0] - centerToHandle2[0], + afterMoveCenter[1] - centerToHandle2[1], + afterMoveCenter[2] - centerToHandle2[2], + ]; + + // Expect handles are moved accordingly + expect(handles[0]).toEqual(afterMoveFirstHandle); + expect(handles[3]).toEqual(afterMoveSecondHandle); + + removeToolState(canvas, rectangleToolData); + done(); + } + ); + }; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 5, 0]; + const index2 = [14, 30, 0]; + + // grab the tool on its left edge + const index3 = [11, 25, 0]; + + // Where to move that grabbing point + // This will result the tool be outside of the bar + const index4 = [13, 24, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + p1 = worldCoord1; + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp); + p3 = worldCoord3; + + const { + pageX: pageX4, + pageY: pageY4, + clientX: clientX4, + clientY: clientY4, + worldCoord: worldCoord4, + } = createNormalizedMouseEvent(imageData, index4, canvas, vp); + p4 = worldCoord4; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Drag the middle of the tool + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }); + canvas.dispatchEvent(evt); + + // Move the middle of the tool to point4 + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX4, + clientY: clientY4, + pageX: pageX4, + pageY: pageY4, + }); + document.dispatchEvent(evt); + + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should successfully create a rectangle tool on a cpu stack viewport and select AND move it', function (done) { + const canvas = createCanvas( + this.renderingEngine, + VIEWPORT_TYPE.STACK, + 512, + 128 + ); + + const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0'; + const vp = this.renderingEngine.getViewport(viewportUID); + + let p1, p2, p3, p4; + + canvas.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const index1 = [11, 5, 0]; + const index2 = [14, 30, 0]; + + // grab the tool on its left edge + const index3 = [11, 25, 0]; + + // Where to move that grabbing point + // This will result the tool be outside of the bar + const index4 = [13, 24, 0]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, canvas, vp); + p1 = worldCoord1; + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, canvas, vp); + p2 = worldCoord2; + + const { + pageX: pageX3, + pageY: pageY3, + clientX: clientX3, + clientY: clientY3, + worldCoord: worldCoord3, + } = createNormalizedMouseEvent(imageData, index3, canvas, vp); + p3 = worldCoord3; + + const { + pageX: pageX4, + pageY: pageY4, + clientX: clientX4, + clientY: clientY4, + worldCoord: worldCoord4, + } = createNormalizedMouseEvent(imageData, index4, canvas, vp); + p4 = worldCoord4; + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + canvas.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + document.dispatchEvent(evt); + + // Drag the middle of the tool + evt = new MouseEvent('mousedown', { + target: canvas, + buttons: 1, + clientX: clientX3, + clientY: clientY3, + pageX: pageX3, + pageY: pageY3, + }); + canvas.dispatchEvent(evt); + + // Move the middle of the tool to point4 + evt = new MouseEvent('mousemove', { + target: canvas, + buttons: 1, + clientX: clientX4, + clientY: clientY4, + pageX: pageX4, + pageY: pageY4, + }); + document.dispatchEvent(evt); + + // Cancel the drawing + let e = new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + key: 'Esc', + char: 'Esc', + }); + canvas.dispatchEvent(e); + + e = new KeyboardEvent('keyup', { + bubbles: true, + cancelable: true, + }); + canvas.dispatchEvent(e); + }); + + const cancelToolDrawing = () => { + const canceledDataUID = cancelActiveManipulations(canvas); + expect(canceledDataUID).toBeDefined(); + + setTimeout(() => { + const enabledElement = getEnabledElement(canvas); + const rectangleToolState = getToolState(enabledElement, 'RectangleRoi'); + // Can successfully add rectangleROI to toolStateManager + expect(rectangleToolState).toBeDefined(); + expect(rectangleToolState.length).toBe(1); + + const rectangleToolData = rectangleToolState[0]; + expect(rectangleToolData.metadata.referencedImageId).toBe( + imageId1.split(':')[1] + ); + expect(rectangleToolData.metadata.toolName).toBe('RectangleRoi'); + expect(rectangleToolData.data.invalidated).toBe(false); + + const data = rectangleToolData.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(1); + + // We expect the mean to not be 255 as it has been moved + expect(data[targets[0]].mean).not.toBe(255); + expect(data[targets[0]].stdDev).not.toBe(0); + + const handles = rectangleToolData.data.handles.points; + + const preMoveFirstHandle = p1; + const preMoveSecondHandle = p2; + const preMoveCenter = p3; + + const centerToHandle1 = [ + preMoveCenter[0] - preMoveFirstHandle[0], + preMoveCenter[1] - preMoveFirstHandle[1], + preMoveCenter[2] - preMoveFirstHandle[2], + ]; + + const centerToHandle2 = [ + preMoveCenter[0] - preMoveSecondHandle[0], + preMoveCenter[1] - preMoveSecondHandle[1], + preMoveCenter[2] - preMoveSecondHandle[2], + ]; + + const afterMoveCenter = p4; + + const afterMoveFirstHandle = [ + afterMoveCenter[0] - centerToHandle1[0], + afterMoveCenter[1] - centerToHandle1[1], + afterMoveCenter[2] - centerToHandle1[2], + ]; + + const afterMoveSecondHandle = [ + afterMoveCenter[0] - centerToHandle2[0], + afterMoveCenter[1] - centerToHandle2[1], + afterMoveCenter[2] - centerToHandle2[2], + ]; + + // Expect handles are moved accordingly + expect(handles[0]).toEqual(afterMoveFirstHandle); + expect(handles[3]).toEqual(afterMoveSecondHandle); + + removeToolState(canvas, rectangleToolData); + done(); + }, 100); + }; + + this.stackToolGroup.addViewports( + this.renderingEngine.uid, + undefined, + vp.uid + ); + + canvas.addEventListener( + CornerstoneTools3DEvents.KEY_DOWN, + cancelToolDrawing + ); + + try { + vp.setStack([imageId1], 0); + this.renderingEngine.render(); + } catch (e) { + done.fail(e); + } + }); +}); diff --git a/packages/cornerstone-tools/test/synchronizerManager_test.js b/packages/cornerstone-tools/test/synchronizerManager_test.js index ee5fd34ce2..318174e7d0 100644 --- a/packages/cornerstone-tools/test/synchronizerManager_test.js +++ b/packages/cornerstone-tools/test/synchronizerManager_test.js @@ -88,296 +88,303 @@ function createCanvas(width, height) { return [canvas1, canvas2] } -describe('Synchronizer Manager: ', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(StackScrollMouseWheelTool, {}) - cache.purgeCache() - this.firstToolGroup = ToolGroupManager.createToolGroup('volume1') - this.firstToolGroup.addTool('StackScrollMouseWheel') - this.firstToolGroup.setToolActive('StackScrollMouseWheel') - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) +describe('SynchronizerManager: ', () => { + beforeAll(() => { + cornerstone3D.setUseCPURenderingOnlyForDebugOrTests(false) }) - afterEach(function () { - // Destroy synchronizer manager to test it first since csTools3D also destroy - // synchronizers - SynchronizerManager.destroySynchronizerById(synchronizerId) - csTools3d.destroy() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('volume1') - - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) - } + describe('Synchronizer Manager1: ', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(StackScrollMouseWheelTool, {}) + cache.purgeCache() + this.firstToolGroup = ToolGroupManager.createToolGroup('volume1') + this.firstToolGroup.addTool('StackScrollMouseWheel') + this.firstToolGroup.setToolActive('StackScrollMouseWheel') + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) }) - }) - it('Should successfully synchronizes viewports for Camera', function (done) { - const [canvas1, canvas2] = createCanvas(512, 128) - - this.renderingEngine.setViewports([ - { - sceneUID: scene1UID, - viewportUID: viewportUID1, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas1, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + afterEach(function () { + // Destroy synchronizer manager to test it first since csTools3D also destroy + // synchronizers + SynchronizerManager.destroySynchronizerById(synchronizerId) + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('volume1') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + }) + + it('Should successfully synchronizes viewports for Camera', function (done) { + const [canvas1, canvas2] = createCanvas(512, 128) + + this.renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: viewportUID1, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - { - sceneUID: scene2UID, - viewportUID: viewportUID2, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas2, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, + { + sceneUID: scene2UID, + viewportUID: viewportUID2, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, }, - }, - ]) + ]) - let canvasesRendered = 0 + let canvasesRendered = 0 - const eventHandler = () => { - canvasesRendered += 1 + const eventHandler = () => { + canvasesRendered += 1 - if (canvasesRendered !== 2) { - return - } + if (canvasesRendered !== 2) { + return + } - const synchronizers = SynchronizerManager.getSynchronizers({ - renderingEngineUID, - viewportUID: viewportUID1, - }) + const synchronizers = SynchronizerManager.getSynchronizers({ + renderingEngineUID, + viewportUID: viewportUID1, + }) - expect(synchronizers.length).toBe(1) + expect(synchronizers.length).toBe(1) - const synchronizerById = - SynchronizerManager.getSynchronizerById(synchronizerId) + const synchronizerById = + SynchronizerManager.getSynchronizerById(synchronizerId) - expect(synchronizerById).toBe(synchronizers[0]) + expect(synchronizerById).toBe(synchronizers[0]) - const allSynchronizers = SynchronizerManager.getAllSynchronizers() + const allSynchronizers = SynchronizerManager.getAllSynchronizers() - expect(allSynchronizers.length).toBe(1) - expect(allSynchronizers[0]).toBe(synchronizerById) + expect(allSynchronizers.length).toBe(1) + expect(allSynchronizers[0]).toBe(synchronizerById) - const createAnotherSynchronizer = () => { - createCameraPositionSynchronizer('axialSync') - } + const createAnotherSynchronizer = () => { + createCameraPositionSynchronizer('axialSync') + } - expect(createAnotherSynchronizer).toThrow() - done() - } - - canvas1.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) - canvas2.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) - - this.firstToolGroup.addViewports( - this.renderingEngine.uid, - scene1UID, - viewportUID1 - ) - this.firstToolGroup.addViewports( - this.renderingEngine.uid, - scene2UID, - viewportUID2 - ) - - try { - const axialSync = createCameraPositionSynchronizer('axialSync') - synchronizerId = axialSync.id - const ctScene = this.renderingEngine.getScene(scene1UID) - const ptScene = this.renderingEngine.getScene(scene2UID) - - axialSync.add({ - renderingEngineUID: ctScene.renderingEngineUID, - sceneUID: ctScene.uid, - viewportUID: ctScene.getViewport(viewportUID1).uid, - }) - axialSync.add({ - renderingEngineUID: ptScene.renderingEngineUID, - sceneUID: ptScene.uid, - viewportUID: ptScene.getViewport(viewportUID2).uid, - }) - - const immediateRender = true - createAndCacheVolume(ctVolumeId, { imageIds: [] }).then(() => { - ctScene.setVolumes([{ volumeUID: ctVolumeId }], immediateRender) - }) - createAndCacheVolume(ptVolumeId, { imageIds: [] }).then(() => { - ptScene.setVolumes([{ volumeUID: ptVolumeId }], immediateRender) - }) - } catch (e) { - done.fail(e) - } - }) -}) - -describe('Synchronizer Manager: ', () => { - beforeEach(function () { - csTools3d.init() - csTools3d.addTool(WindowLevelTool, {}) - cache.purgeCache() - this.firstToolGroup = ToolGroupManager.createToolGroup('volume1') - this.firstToolGroup.addTool('WindowLevel', { - configuration: { volumeUID: ctVolumeId }, - }) - this.firstToolGroup.setToolActive('WindowLevel', { - bindings: [ - { - mouseButton: ToolBindings.Mouse.Primary, - }, - ], - }) - this.renderingEngine = new RenderingEngine(renderingEngineUID) - registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) - metaData.addProvider(fakeMetaDataProvider, 10000) - }) + expect(createAnotherSynchronizer).toThrow() + done() + } - afterEach(function () { - // Destroy synchronizer manager to test it first since csTools3D also destroy - // synchronizers - SynchronizerManager.destroy() - csTools3d.destroy() - cache.purgeCache() - this.renderingEngine.destroy() - metaData.removeProvider(fakeMetaDataProvider) - unregisterAllImageLoaders() - ToolGroupManager.destroyToolGroupById('volume1') - - DOMElements.forEach((el) => { - if (el.parentNode) { - el.parentNode.removeChild(el) + canvas1.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) + canvas2.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) + + this.firstToolGroup.addViewports( + this.renderingEngine.uid, + scene1UID, + viewportUID1 + ) + this.firstToolGroup.addViewports( + this.renderingEngine.uid, + scene2UID, + viewportUID2 + ) + + try { + const axialSync = createCameraPositionSynchronizer('axialSync') + synchronizerId = axialSync.id + const ctScene = this.renderingEngine.getScene(scene1UID) + const ptScene = this.renderingEngine.getScene(scene2UID) + + axialSync.add({ + renderingEngineUID: ctScene.renderingEngineUID, + sceneUID: ctScene.uid, + viewportUID: ctScene.getViewport(viewportUID1).uid, + }) + axialSync.add({ + renderingEngineUID: ptScene.renderingEngineUID, + sceneUID: ptScene.uid, + viewportUID: ptScene.getViewport(viewportUID2).uid, + }) + + const immediateRender = true + createAndCacheVolume(ctVolumeId, { imageIds: [] }).then(() => { + ctScene.setVolumes([{ volumeUID: ctVolumeId }], immediateRender) + }) + createAndCacheVolume(ptVolumeId, { imageIds: [] }).then(() => { + ptScene.setVolumes([{ volumeUID: ptVolumeId }], immediateRender) + }) + } catch (e) { + done.fail(e) } }) }) - it('Should successfully synchronizes viewports for Camera', function (done) { - const [canvas1, canvas2] = createCanvas(512, 128) - - this.renderingEngine.setViewports([ - { - sceneUID: scene1UID, - viewportUID: viewportUID1, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas1, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.AXIAL, - }, - }, - { - sceneUID: scene1UID, - viewportUID: viewportUID2, - type: VIEWPORT_TYPE.ORTHOGRAPHIC, - canvas: canvas2, - defaultOptions: { - background: [1, 0, 1], // pinkish background - orientation: ORIENTATION.CORONAL, - }, - }, - ]) - - let canvasesRendered = 0 - const [pageX1, pageY1] = [316, 125] - const [pageX2, pageY2] = [211, 20] - - const addEventListenerForVOI = () => { - canvas2.addEventListener(EVENTS.IMAGE_RENDERED, () => { - const image2 = canvas2.toDataURL('image/png') - compareImages(image2, windowLevel_canvas2, 'windowLevel_canvas2').then( - done, - done.fail - ) + describe('Synchronizer Manager2: ', () => { + beforeEach(function () { + csTools3d.init() + csTools3d.addTool(WindowLevelTool, {}) + cache.purgeCache() + this.firstToolGroup = ToolGroupManager.createToolGroup('volume1') + this.firstToolGroup.addTool('WindowLevel', { + configuration: { volumeUID: ctVolumeId }, }) - } - - const eventHandler = () => { - canvasesRendered += 1 - - if (canvasesRendered !== 2) { - return - } - - // Mouse Down - let evt = new MouseEvent('mousedown', { - target: canvas1, - buttons: 1, - clientX: pageX1, - clientY: pageY1, - pageX: pageX1, - pageY: pageY1, + this.firstToolGroup.setToolActive('WindowLevel', { + bindings: [ + { + mouseButton: ToolBindings.Mouse.Primary, + }, + ], }) + this.renderingEngine = new RenderingEngine(renderingEngineUID) + registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) + metaData.addProvider(fakeMetaDataProvider, 10000) + }) - canvas1.dispatchEvent(evt) - - // Mouse move to put the end somewhere else - const evt1 = new MouseEvent('mousemove', { - target: canvas1, - buttons: 1, - clientX: pageX2, - clientY: pageY2, - pageX: pageX2, - pageY: pageY2, + afterEach(function () { + // Destroy synchronizer manager to test it first since csTools3D also destroy + // synchronizers + SynchronizerManager.destroy() + csTools3d.destroy() + cache.purgeCache() + this.renderingEngine.destroy() + metaData.removeProvider(fakeMetaDataProvider) + unregisterAllImageLoaders() + ToolGroupManager.destroyToolGroupById('volume1') + + DOMElements.forEach((el) => { + if (el.parentNode) { + el.parentNode.removeChild(el) + } }) + }) - addEventListenerForVOI() - document.dispatchEvent(evt1) + it('Should successfully synchronizes viewports for Camera', function (done) { + const [canvas1, canvas2] = createCanvas(512, 128) - const evt3 = new MouseEvent('mouseup', { - bubbles: true, - cancelable: true, - }) + this.renderingEngine.setViewports([ + { + sceneUID: scene1UID, + viewportUID: viewportUID1, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas1, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.AXIAL, + }, + }, + { + sceneUID: scene1UID, + viewportUID: viewportUID2, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + canvas: canvas2, + defaultOptions: { + background: [1, 0, 1], // pinkish background + orientation: ORIENTATION.CORONAL, + }, + }, + ]) + + let canvasesRendered = 0 + const [pageX1, pageY1] = [316, 125] + const [pageX2, pageY2] = [211, 20] + + const addEventListenerForVOI = () => { + canvas2.addEventListener(EVENTS.IMAGE_RENDERED, () => { + const image2 = canvas2.toDataURL('image/png') + compareImages( + image2, + windowLevel_canvas2, + 'windowLevel_canvas2' + ).then(done, done.fail) + }) + } - document.dispatchEvent(evt3) - } - - canvas1.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) - canvas2.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) - - this.firstToolGroup.addViewports( - this.renderingEngine.uid, - scene1UID, - viewportUID1 - ) - this.firstToolGroup.addViewports( - this.renderingEngine.uid, - scene1UID, - viewportUID2 - ) - - try { - const voiSync = createVOISynchronizer('ctWLSync') - const ctScene = this.renderingEngine.getScene(scene1UID) - - voiSync.addSource({ - renderingEngineUID: ctScene.renderingEngineUID, - sceneUID: ctScene.uid, - viewportUID: ctScene.getViewport(viewportUID1).uid, - }) - voiSync.addTarget({ - renderingEngineUID: ctScene.renderingEngineUID, - sceneUID: ctScene.uid, - viewportUID: ctScene.getViewport(viewportUID2).uid, - }) + const eventHandler = () => { + canvasesRendered += 1 + + if (canvasesRendered !== 2) { + return + } + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: canvas1, + buttons: 1, + clientX: pageX1, + clientY: pageY1, + pageX: pageX1, + pageY: pageY1, + }) + + canvas1.dispatchEvent(evt) + + // Mouse move to put the end somewhere else + const evt1 = new MouseEvent('mousemove', { + target: canvas1, + buttons: 1, + clientX: pageX2, + clientY: pageY2, + pageX: pageX2, + pageY: pageY2, + }) + + addEventListenerForVOI() + document.dispatchEvent(evt1) + + const evt3 = new MouseEvent('mouseup', { + bubbles: true, + cancelable: true, + }) + + document.dispatchEvent(evt3) + } - const immediateRender = true - createAndCacheVolume(ctVolumeId, { imageIds: [] }).then(() => { - ctScene.setVolumes([{ volumeUID: ctVolumeId }], immediateRender) - ctScene.render() - }) - } catch (e) { - done.fail(e) - } + canvas1.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) + canvas2.addEventListener(EVENTS.IMAGE_RENDERED, eventHandler) + + this.firstToolGroup.addViewports( + this.renderingEngine.uid, + scene1UID, + viewportUID1 + ) + this.firstToolGroup.addViewports( + this.renderingEngine.uid, + scene1UID, + viewportUID2 + ) + + try { + const voiSync = createVOISynchronizer('ctWLSync') + const ctScene = this.renderingEngine.getScene(scene1UID) + + voiSync.addSource({ + renderingEngineUID: ctScene.renderingEngineUID, + sceneUID: ctScene.uid, + viewportUID: ctScene.getViewport(viewportUID1).uid, + }) + voiSync.addTarget({ + renderingEngineUID: ctScene.renderingEngineUID, + sceneUID: ctScene.uid, + viewportUID: ctScene.getViewport(viewportUID2).uid, + }) + + const immediateRender = true + createAndCacheVolume(ctVolumeId, { imageIds: [] }).then(() => { + ctScene.setVolumes([{ volumeUID: ctVolumeId }], immediateRender) + ctScene.render() + }) + } catch (e) { + done.fail(e) + } + }) }) }) diff --git a/packages/demo/package.json b/packages/demo/package.json index 72ae5266af..b0f5f99b97 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -17,20 +17,21 @@ "@ohif/cornerstone-render": "^0.0.2", "@ohif/cornerstone-tools": "^0.0.1", "cornerstone-wado-image-loader": "^3.3.2", - "dcmjs": "^0.18.6", - "dicom-parser": "^1.8.7", - "dicomweb-client": "^0.7.0", - "gl-matrix": "^3.3.0", + "dcmjs": "0.19.2", + "dicom-parser": "^1.8.11", + "dicomweb-client": "^0.8.4", + "gl-matrix": "^3.4.3", "hammerjs": "^2.0.8", "react": "17.0.2", "react-dom": "17.0.2", - "react-resize-detector": "6.6.4", - "react-router-dom": "5.2.0", - "vtk.js": "git+https://github.com/jadh4v/vtk-js.git#image-volume-half-voxel" + "react-resize-detector": "6.7.8", + "react-router-dom": "5.3.0", + "vtk.js": "git+https://github.com/jadh4v/vtk-js.git#image-volume-half-voxel", + "detect-gpu": "^4.0.7" }, "devDependencies": { "eslint-config-react-app": "^6.0.0", - "eslint-plugin-react": "^7.23.2", - "eslint-plugin-react-hooks": "^4.2.0" + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" } } diff --git a/packages/demo/src/App.tsx b/packages/demo/src/App.tsx index c26d01088d..0e16d0d496 100644 --- a/packages/demo/src/App.tsx +++ b/packages/demo/src/App.tsx @@ -13,11 +13,13 @@ import ToolDisplayConfigurationExample from './ExampleToolDisplayConfiguration' import OneVolumeExample from './ExampleOneVolume' import PriorityLoadExample from './ExamplePriorityLoad' import OneStackExample from './ExampleOneStack' +import OneStackExampleCPU from './ExampleOneStackCPU' import FlipViewportExample from './ExampleFlipViewport' import ModifierKeysExample from './ExampleModifierKeys' import TestUtils from './ExampleTestUtils' import TestUtilsVolume from './ExampleTestUtilsVolume' import CalibrationExample from './ExampleCalibration' +import { resetCPURenderingOnlyForDebugOrTests } from '@ohif/cornerstone-render' function LinkOut({ href, text }) { return ( @@ -50,6 +52,10 @@ function Index() { height: '512px', } + // Reset the CPU rendering to whatever it should be (might've navigated from + // A CPU demo). + resetCPURenderingOnlyForDebugOrTests() + const examples = [ { title: 'MPR', @@ -67,9 +73,9 @@ function Index() { text: 'Example one Stack', }, { - title: 'Flip Viewport', - url: '/flip', - text: 'Example for flipping viewport horizontally or vertically volume', + title: 'One Stack CPU', + url: '/oneStackCPU', + text: 'Example one Stack with CPU fallback (even if your environment supports GPU)', }, { title: 'Canvas Resize', @@ -141,10 +147,13 @@ function Index() { url: '/calibratedImages', text: 'Example that shows support for calibrated images', }, + { + title: 'Flip Volume Viewport', + url: '/flip', + text: 'Example for flipping viewport horizontally or vertically volume', + }, ] - - const exampleComponents = examples.map((e) => { return }) @@ -267,6 +276,11 @@ function AppRouter() { children: , }) + const OneStackCPU = () => + Example({ + children: , + }) + const Flip = () => Example({ children: , @@ -305,6 +319,7 @@ function AppRouter() { + diff --git a/packages/demo/src/ExampleCacheDecache.tsx b/packages/demo/src/ExampleCacheDecache.tsx index 2b7e3699cb..5f822d0424 100644 --- a/packages/demo/src/ExampleCacheDecache.tsx +++ b/packages/demo/src/ExampleCacheDecache.tsx @@ -6,10 +6,10 @@ import { loadAndCacheImages, ORIENTATION, VIEWPORT_TYPE, + init as csRenderInit, } from '@ohif/cornerstone-render' import * as csTools3d from '@ohif/cornerstone-tools' - import getImageIds from './helpers/getImageIds' import ViewportGrid from './components/ViewportGrid' import { initToolGroups } from './initToolGroups' @@ -58,7 +58,6 @@ class CacheDecacheExample extends Component { constructor(props) { super(props) - csTools3d.init() registerWebImageLoader(cs) this._canvasNodes = new Map() this._viewportGridRef = React.createRef() @@ -82,6 +81,9 @@ class CacheDecacheExample extends Component { * LIFECYCLE */ async componentDidMount() { + await csRenderInit() + csTools3d.init() + ;({ ctSceneToolGroup, stackCTViewportToolGroup } = initToolGroups()) this.ctVolumeUID = ctVolumeUID diff --git a/packages/demo/src/ExampleCalibration.tsx b/packages/demo/src/ExampleCalibration.tsx index 319d97ece6..cc952667b9 100644 --- a/packages/demo/src/ExampleCalibration.tsx +++ b/packages/demo/src/ExampleCalibration.tsx @@ -3,11 +3,9 @@ import { cache, RenderingEngine, VIEWPORT_TYPE, + init as csRenderInit, } from '@ohif/cornerstone-render' -import { - ToolBindings, - Utilities, -} from '@ohif/cornerstone-tools' +import { ToolBindings, Utilities } from '@ohif/cornerstone-tools' import * as csTools3d from '@ohif/cornerstone-tools' import { setCTWWWC } from './helpers/transferFunctionHelpers' @@ -16,11 +14,7 @@ import getImageIds from './helpers/getImageIds' import ViewportGrid from './components/ViewportGrid' import { initToolGroups, addToolsToToolGroups } from './initToolGroups' import './ExampleVTKMPR.css' -import { - renderingEngineUID, - VIEWPORT_IDS, - ANNOTATION_TOOLS, -} from './constants' +import { renderingEngineUID, VIEWPORT_IDS, ANNOTATION_TOOLS } from './constants' const STACK = 'stack' @@ -56,7 +50,6 @@ class CalibrationExample extends Component { constructor(props) { super(props) - csTools3d.init() this._canvasNodes = new Map() this._offScreenRef = React.createRef() @@ -82,6 +75,8 @@ class CalibrationExample extends Component { * LIFECYCLE */ async componentDidMount() { + await csRenderInit() + csTools3d.init() ;({ stackDXViewportToolGroup } = initToolGroups()) const DXStackImageIds = await this.DXStackImageIdsPromise @@ -169,7 +164,6 @@ class CalibrationExample extends Component { }) } - swapTools = (evt) => { const toolName = evt.target.value diff --git a/packages/demo/src/ExampleCanvasResize.tsx b/packages/demo/src/ExampleCanvasResize.tsx index af3ad38a0b..48c4b45310 100644 --- a/packages/demo/src/ExampleCanvasResize.tsx +++ b/packages/demo/src/ExampleCanvasResize.tsx @@ -1,6 +1,11 @@ -import React, { Component } from 'react'; +import React, { Component } from 'react' // ~~ -import { RenderingEngine, ORIENTATION, VIEWPORT_TYPE } from '@ohif/cornerstone-render'; +import { + RenderingEngine, + ORIENTATION, + VIEWPORT_TYPE, + init as csRenderInit, +} from '@ohif/cornerstone-render' class CanvasResizeExample extends Component { state = { @@ -9,20 +14,20 @@ class CanvasResizeExample extends Component { [128, 128], [128, 256], ], - }; + } constructor(props) { - super(props); + super(props) - this.axialCTContainer = React.createRef(); - this.sagittalCTContainer = React.createRef(); - this.coronalCTContainer = React.createRef(); + this.axialCTContainer = React.createRef() + this.sagittalCTContainer = React.createRef() + this.coronalCTContainer = React.createRef() - this.resize = this.resize.bind(this); + this.resize = this.resize.bind(this) } componentWillUnmount() { - this.renderingEngine.destroy(); + this.renderingEngine.destroy() } resize() { @@ -30,40 +35,41 @@ class CanvasResizeExample extends Component { [Math.floor(Math.random() * 512), Math.floor(Math.random() * 512)], [Math.floor(Math.random() * 512), Math.floor(Math.random() * 512)], [Math.floor(Math.random() * 512), Math.floor(Math.random() * 512)], - ]; + ] - this.setState({ viewportSizes }); + this.setState({ viewportSizes }) } componentDidUpdate() { - const t0 = new Date().getTime(); - this.renderingEngine.resize(); + const t0 = new Date().getTime() + this.renderingEngine.resize() - const t1 = new Date().getTime(); + const t1 = new Date().getTime() - this.renderingEngine.render(); + this.renderingEngine.render() - const t2 = new Date().getTime(); + const t2 = new Date().getTime() - console.log(`Resize time: ${t1 - t0}, Re-render time: ${t2 - t1} ms`); + console.log(`Resize time: ${t1 - t0}, Re-render time: ${t2 - t1} ms`) } - componentDidMount() { - const renderingEngineUID = 'ExampleRenderingEngineID'; + async componentDidMount() { + await csRenderInit() + const renderingEngineUID = 'ExampleRenderingEngineID' - const renderingEngine = new RenderingEngine(renderingEngineUID); + const renderingEngine = new RenderingEngine(renderingEngineUID) - this.renderingEngine = renderingEngine; + this.renderingEngine = renderingEngine - const axialCTViewportID = 'AXIAL_CT'; - const sagittalCTViewportID = 'SAGITTAL_CT'; - const coronalCTViewportID = 'CORONAL_CT'; + const axialCTViewportID = 'AXIAL_CT' + const sagittalCTViewportID = 'SAGITTAL_CT' + const coronalCTViewportID = 'CORONAL_CT' - this.axialCTViewportID = axialCTViewportID; - this.sagittalCTViewportID = sagittalCTViewportID; - this.coronalCTViewportID = coronalCTViewportID; + this.axialCTViewportID = axialCTViewportID + this.sagittalCTViewportID = sagittalCTViewportID + this.coronalCTViewportID = coronalCTViewportID - const ctSceneID = 'SCENE_CT'; + const ctSceneID = 'SCENE_CT' renderingEngine.setViewports([ { @@ -96,28 +102,28 @@ class CanvasResizeExample extends Component { background: [0, 0, 1], }, }, - ]); + ]) - renderingEngine.render(); + renderingEngine.render() } render() { - const { viewportSizes } = this.state; + const { viewportSizes } = this.state const style0 = { width: `${viewportSizes[0][0]}px`, height: `${viewportSizes[0][1]}px`, - }; + } const style1 = { width: `${viewportSizes[1][0]}px`, height: `${viewportSizes[1][1]}px`, - }; + } const style2 = { width: `${viewportSizes[2][0]}px`, height: `${viewportSizes[2][1]}px`, - }; + } return (
@@ -166,8 +172,8 @@ class CanvasResizeExample extends Component {
- ); + ) } } -export default CanvasResizeExample; +export default CanvasResizeExample diff --git a/packages/demo/src/ExampleColor.tsx b/packages/demo/src/ExampleColor.tsx index aca543eecd..243df039df 100644 --- a/packages/demo/src/ExampleColor.tsx +++ b/packages/demo/src/ExampleColor.tsx @@ -1,19 +1,20 @@ -import React, { Component } from "react"; +import React, { Component } from 'react' // ~~ -import * as cs from "@ohif/cornerstone-render"; +import * as cs from '@ohif/cornerstone-render' import { RenderingEngine, ORIENTATION, VIEWPORT_TYPE, metaData, createAndCacheVolume, -} from "@ohif/cornerstone-render"; -import { ToolBindings } from "@ohif/cornerstone-tools"; + init as csRenderInit, +} from '@ohif/cornerstone-render' +import { ToolBindings } from '@ohif/cornerstone-tools' import * as csTools3d from '@ohif/cornerstone-tools' -import { registerWebImageLoader } from "@ohif/cornerstone-image-loader-streaming-volume"; -import config from "./config/default"; -import { hardcodedMetaDataProvider } from "./helpers/initCornerstone"; +import { registerWebImageLoader } from '@ohif/cornerstone-image-loader-streaming-volume' +import config from './config/default' +import { hardcodedMetaDataProvider } from './helpers/initCornerstone' import { initToolGroups } from './initToolGroups' let colorSceneToolGroup @@ -24,25 +25,24 @@ class ColorExample extends Component { [512, 512], [512, 512], ], - }; + } constructor(props) { - super(props); + super(props) - csTools3d.init() - this.axialContainer = React.createRef(); - this.sagittalContainer = React.createRef(); - this.coronalContainer = React.createRef(); + this.axialContainer = React.createRef() + this.sagittalContainer = React.createRef() + this.coronalContainer = React.createRef() } componentWillUnmount() { - csTools3d.destroy() - this.renderingEngine.destroy(); - + this.renderingEngine.destroy() } async componentDidMount() { + await csRenderInit() + csTools3d.init() registerWebImageLoader(cs) const renderingEngineUID = 'ExampleRenderingEngineID' const { imageIds } = config.colorImages @@ -152,22 +152,22 @@ class ColorExample extends Component { } render() { - const { viewportSizes } = this.state; + const { viewportSizes } = this.state const style0 = { width: `${viewportSizes[0][0]}px`, height: `${viewportSizes[0][1]}px`, - }; + } const style1 = { width: `${viewportSizes[1][0]}px`, height: `${viewportSizes[1][1]}px`, - }; + } const style2 = { width: `${viewportSizes[2][0]}px`, height: `${viewportSizes[2][1]}px`, - }; + } return (
@@ -211,4 +211,4 @@ class ColorExample extends Component { } } -export default ColorExample; +export default ColorExample diff --git a/packages/demo/src/ExampleEnableDisableAPI.tsx b/packages/demo/src/ExampleEnableDisableAPI.tsx index 48e0ef83ea..5695622e75 100644 --- a/packages/demo/src/ExampleEnableDisableAPI.tsx +++ b/packages/demo/src/ExampleEnableDisableAPI.tsx @@ -6,6 +6,7 @@ import { metaData, ORIENTATION, VIEWPORT_TYPE, + init as csRenderInit, } from '@ohif/cornerstone-render' import { ToolBindings } from '@ohif/cornerstone-tools' import * as cs from '@ohif/cornerstone-render' @@ -32,7 +33,6 @@ import sortImageIdsByIPP from './helpers/sortImageIdsByIPP' const VOLUME = 'volume' - window.cache = cache let ctSceneToolGroup, @@ -44,7 +44,6 @@ let ctSceneToolGroup, const toolsToUse = ANNOTATION_TOOLS const ctLayoutTools = ['Levels'].concat(toolsToUse) - class EnableDisableViewportExample extends Component { state = { progressText: 'fetching metadata...', @@ -69,7 +68,6 @@ class EnableDisableViewportExample extends Component { constructor(props) { super(props) - csTools3d.init() this._canvasNodes = new Map() this._viewportGridRef = React.createRef() this._offScreenRef = React.createRef() @@ -123,6 +121,9 @@ class EnableDisableViewportExample extends Component { * LIFECYCLE */ async componentDidMount() { + await csRenderInit() + csTools3d.init() + ;({ ctSceneToolGroup, stackCTViewportToolGroup, @@ -361,14 +362,9 @@ class EnableDisableViewportExample extends Component { this.renderingEngine.enableElement(viewportInput) + const { toolGroup, sceneUID, viewportUID, type, canvas } = viewportInput - const { toolGroup, sceneUID, viewportUID, type, canvas} = viewportInput - - toolGroup.addViewports( - renderingEngineUID, - sceneUID, - viewportUID - ) + toolGroup.addViewports(renderingEngineUID, sceneUID, viewportUID) // load if (viewportUID === VIEWPORT_IDS.STACK.CT) { @@ -390,13 +386,12 @@ class EnableDisableViewportExample extends Component { })) } - swapTools = (evt) => { const toolName = evt.target.value const isAnnotationToolOn = toolName !== 'Levels' ? true : false const options = { - bindings: [ { mouseButton: ToolBindings.Mouse.Primary } ], + bindings: [{ mouseButton: ToolBindings.Mouse.Primary }], } if (isAnnotationToolOn) { // Set tool active @@ -439,7 +434,6 @@ class EnableDisableViewportExample extends Component { this.setState({ leftClickTool: toolName }) } - showOffScreenCanvas = () => { // remove all children this._offScreenRef.current.innerHTML = '' diff --git a/packages/demo/src/ExampleFlipViewport.tsx b/packages/demo/src/ExampleFlipViewport.tsx index ae808758e0..7a3f564db5 100644 --- a/packages/demo/src/ExampleFlipViewport.tsx +++ b/packages/demo/src/ExampleFlipViewport.tsx @@ -1,4 +1,3 @@ - import React, { Component } from 'react' import { cache, @@ -6,17 +5,13 @@ import { createAndCacheVolume, ORIENTATION, VIEWPORT_TYPE, - FLIP_DIRECTION, + init as csRenderInit, } from '@ohif/cornerstone-render' -import { - synchronizers, - ToolBindings, -} from '@ohif/cornerstone-tools' +import { synchronizers, ToolBindings } from '@ohif/cornerstone-tools' import * as csTools3d from '@ohif/cornerstone-tools' import vtkConstants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants' - import getImageIds from './helpers/getImageIds' import ViewportGrid from './components/ViewportGrid' import { initToolGroups, addToolsToToolGroups } from './initToolGroups' @@ -31,15 +26,12 @@ import { } from './constants' import sortImageIdsByIPP from './helpers/sortImageIdsByIPP' import * as cs from '@ohif/cornerstone-render' -import config from './config/default' -import { hardcodedMetaDataProvider } from './helpers/initCornerstone' import { registerWebImageLoader } from '@ohif/cornerstone-image-loader-streaming-volume' import { setCTWWWC, setPetTransferFunction, } from './helpers/transferFunctionHelpers' -import getToolDetailForDisplay from './helpers/getToolDetailForDisplay' const VOLUME = 'volume' const STACK = 'stack' @@ -84,7 +76,6 @@ class FlipViewportExample extends Component { constructor(props) { super(props) - csTools3d.init() registerWebImageLoader(cs) this._canvasNodes = new Map() this._viewportGridRef = React.createRef() @@ -116,6 +107,8 @@ class FlipViewportExample extends Component { * LIFECYCLE */ async componentDidMount() { + await csRenderInit() + csTools3d.init() ;({ ctSceneToolGroup, stackCTViewportToolGroup, @@ -208,28 +201,43 @@ class FlipViewportExample extends Component { ptSceneToolGroup, }) - this.axialSync.add({ renderingEngineUID, viewportUID: VIEWPORT_IDS.CT.AXIAL }); - this.axialSync.add({ renderingEngineUID, viewportUID: VIEWPORT_IDS.STACK.CT }); + this.axialSync.add({ + renderingEngineUID, + viewportUID: VIEWPORT_IDS.CT.AXIAL, + }) + this.axialSync.add({ + renderingEngineUID, + viewportUID: VIEWPORT_IDS.STACK.CT, + }) - this.ctWLSync.add({ renderingEngineUID, viewportUID: VIEWPORT_IDS.CT.AXIAL }); - this.ctWLSync.add({ renderingEngineUID, viewportUID: VIEWPORT_IDS.CT.CORONAL }); - this.ctWLSync.add({ renderingEngineUID, viewportUID: VIEWPORT_IDS.CT.SAGITTAL }); - this.ctWLSync.add({ renderingEngineUID, viewportUID: VIEWPORT_IDS.STACK.CT }); + this.ctWLSync.add({ + renderingEngineUID, + viewportUID: VIEWPORT_IDS.CT.AXIAL, + }) + this.ctWLSync.add({ + renderingEngineUID, + viewportUID: VIEWPORT_IDS.CT.CORONAL, + }) + this.ctWLSync.add({ + renderingEngineUID, + viewportUID: VIEWPORT_IDS.CT.SAGITTAL, + }) + this.ctWLSync.add({ + renderingEngineUID, + viewportUID: VIEWPORT_IDS.STACK.CT, + }) renderingEngine.render() const ctStackViewport = renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT) - const ctMiddleSlice = Math.floor(ctStackImageIds.length / 2) await ctStackViewport.setStack( sortImageIdsByIPP(ctStackImageIds), - ctMiddleSlice, - + ctMiddleSlice ) ctStackViewport.setProperties({ voiRange: { lower: -160, upper: 240 } }) - // This only creates the volumes, it does not actually load all // of the pixel data (yet) const ctVolume = await createAndCacheVolume(ctVolumeUID, { @@ -305,7 +313,7 @@ class FlipViewportExample extends Component { const isAnnotationToolOn = toolName !== 'Levels' ? true : false const options = { - bindings: [ { mouseButton: ToolBindings.Mouse.Primary } ], + bindings: [{ mouseButton: ToolBindings.Mouse.Primary }], } if (isAnnotationToolOn) { // Set tool active @@ -338,10 +346,18 @@ class FlipViewportExample extends Component { this.setState({ ptCtLeftClickTool: toolName }) } - flip = (direction) => { + flipHorizontal = () => { + const { viewportUID } = viewportInput[this.state.selectedViewport] + const viewport = this.renderingEngine.getViewport(viewportUID) + const { flipHorizontal } = viewport.getProperties() + viewport.flip({ flipHorizontal: !flipHorizontal }) + } + + flipVertical = () => { const { viewportUID } = viewportInput[this.state.selectedViewport] const viewport = this.renderingEngine.getViewport(viewportUID) - viewport.flip(direction) + const { flipVertical } = viewport.getProperties() + viewport.flip({ flipVertical: !flipVertical }) } render() { @@ -356,9 +372,9 @@ class FlipViewportExample extends Component { ) : null}

- This is a demo for flipping viewports: viewports 1,2,3 are volume - viewports and viewport 4 (bottom right) is stack viewport of the - same volume + This is a demo for flipping volume viewports: viewports 1,2,3 are + volume viewports and viewport 4 (bottom right) is stack viewport + (with only one image) of the same volume

@@ -374,14 +390,14 @@ class FlipViewportExample extends Component { - + tool !== 'Crosshairs' +); + +const availableStacks = ['ct', 'pt', 'dx', 'color']; + +class OneStackExampleCPU extends Component { + state = { + progressText: 'fetching metadata...', + metadataLoaded: false, + petColorMapIndex: 0, + layoutIndex: 0, + destroyed: false, + // + viewportGrid: { + numCols: 1, + numRows: 1, + viewports: [{}], + }, + ptCtLeftClickTool: 'WindowLevel', + currentStack: 'ct', + falseColor: false, + activeToolGroup: null, + }; + + constructor(props) { + super(props); + + setUseCPURenderingOnlyForDebugOrTests(true); + this._canvasNodes = new Map(); + this._offScreenRef = React.createRef(); + + this._viewportGridRef = React.createRef(); + + this.ctStackImageIdsPromise = getImageIds('ct1', STACK); + this.ptStackImageIdsPromise = getImageIds('pt1', STACK); + this.dxStackImageIdsPromise = getImageIds('dx', STACK); + + Promise.all([ + this.ctStackImageIdsPromise, + this.ptStackImageIdsPromise, + this.dxStackImageIdsPromise, + ]).then(() => this.setState({ progressText: 'Loading data...' })); + + this.viewportGridResizeObserver = new ResizeObserver((entries) => { + // ThrottleFn? May not be needed. This is lightning fast. + // Set in mount + if (this.renderingEngine) { + this.renderingEngine.resize(); + this.renderingEngine.render(); + } + }); + } + + /** + * LIFECYCLE + */ + async componentDidMount() { + await csRenderInit(); + csTools3d.init(); + registerWebImageLoader(cs); + ({ stackCTViewportToolGroup, stackPTViewportToolGroup } = initToolGroups()); + + const ctStackImageIds = await this.ctStackImageIdsPromise; + const ptStackImageIds = await this.ptStackImageIdsPromise; + const dxStackImageIds = await this.dxStackImageIdsPromise; + + const renderingEngine = new RenderingEngine(renderingEngineUID); + + const colorImageIds = config.colorImages.imageIds; + + metaData.addProvider( + (type, imageId) => + hardcodedMetaDataProvider(type, imageId, colorImageIds), + 10000 + ); + + this.renderingEngine = renderingEngine; + window.renderingEngine = renderingEngine; + + const viewportInput = [ + { + viewportUID: VIEWPORT_IDS.STACK.CT, + type: VIEWPORT_TYPE.STACK, + canvas: this._canvasNodes.get(0), + defaultOptions: { + background: [0.2, 0, 0.2], + }, + }, + ]; + + renderingEngine.setViewports(viewportInput); + + stackCTViewportToolGroup.addViewports( + renderingEngineUID, + undefined, + VIEWPORT_IDS.STACK.CT + ); + + addToolsToToolGroups({ + stackCTViewportToolGroup, + stackPTViewportToolGroup, + }); + + // This will initialise volumes in GPU memory + renderingEngine.render(); + + const ctStackViewport = renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT); + this.ctStackViewport = ctStackViewport; + + const ctMiddleSlice = Math.floor(ctStackImageIds.length / 2); + const ptMiddleSlice = Math.floor(ptStackImageIds.length / 2); + const colorMiddleSlice = Math.floor(colorImageIds.length / 2); + + this.dxStackImageIds = dxStackImageIds; + this.ctStackImageIds = ctStackImageIds; + this.ptStackImageIds = ptStackImageIds; + + const stacks = { + ct: [ + ctStackImageIds[ctMiddleSlice], + ctStackImageIds[ctMiddleSlice + 1], + ctStackImageIds[ctMiddleSlice + 2], + ], + pt: [ + ptStackImageIds[ptMiddleSlice], + ptStackImageIds[ptMiddleSlice + 1], + ptStackImageIds[ptMiddleSlice + 2], + ], + dx: [dxStackImageIds[0], dxStackImageIds[1]], + color: [ + colorImageIds[colorMiddleSlice], + colorImageIds[colorMiddleSlice + 1], + ], + }; + + this.stacks = stacks; + + await ctStackViewport.setStack(stacks.ct, 0); + ctStackViewport.setProperties({ + voiRange: { lower: -160, upper: 240 }, + // interpolationType: INTERPOLATION_TYPE.NEAREST, + }); + + // Start listening for resize + this.viewportGridResizeObserver.observe(this._viewportGridRef.current); + this.setState({ activeToolGroup: stackCTViewportToolGroup }); + } + + componentDidUpdate(prevProps, prevState) { + const { layoutIndex } = this.state; + const { renderingEngine } = this; + const onLoad = () => this.setState({ progressText: 'Loaded.' }); + } + + componentWillUnmount() { + // Stop listening for resize + if (this.viewportGridResizeObserver) { + this.viewportGridResizeObserver.disconnect(); + } + + cache.purgeCache(); + csTools3d.destroy(); + + this.renderingEngine.destroy(); + } + + destroyAndDecacheAllVolumes = () => { + if (!this.state.metadataLoaded || this.state.destroyed) { + return; + } + this.renderingEngine.destroy(); + + cache.purgeCache(); + }; + + resetToolModes = (toolGroup, stackName) => { + ANNOTATION_TOOLS.forEach((toolName) => { + toolGroup.setToolPassive(toolName); + }); + + const levelTool = stackName === 'pt' ? 'PetThreshold' : 'WindowLevel'; + + toolGroup.setToolActive(levelTool, { + bindings: [{ mouseButton: ToolBindings.Mouse.Primary }], + }); + toolGroup.setToolActive('Pan', { + bindings: [{ mouseButton: ToolBindings.Mouse.Auxiliary }], + }); + toolGroup.setToolActive('Zoom', { + bindings: [{ mouseButton: ToolBindings.Mouse.Secondary }], + }); + }; + + swapTools = (evt) => { + let toolName = evt.target.value; + const { activeToolGroup, currentStack } = this.state; + + if (currentStack === 'pt' && toolName === 'WindowLevel') { + toolName = 'PetThreshold'; + } + + this.resetToolModes(activeToolGroup, this.state.currentStack); + + const tools = Object.entries(activeToolGroup.tools); + + // Disabling any tool that is active on mouse primary + const [activeTool] = tools.find( + ([tool, { bindings, mode }]) => + mode === 'Active' && + bindings.length && + bindings.some( + (binding) => binding.mouseButton === ToolBindings.Mouse.Primary + ) + ); + + activeToolGroup.setToolPassive(activeTool); + + // Using mouse primary for the selected tool + const currentBindings = activeToolGroup.tools[toolName].bindings; + + activeToolGroup.setToolActive(toolName, { + bindings: [ + ...currentBindings, + { mouseButton: ToolBindings.Mouse.Primary }, + ], + }); + + this.renderingEngine.render(); + this.setState({ ptCtLeftClickTool: toolName }); + }; + + rotateViewport = (rotateDeg) => { + const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT); + const { rotation } = vp.getProperties(); + + vp.setProperties({ rotation: rotation + rotateDeg }); + vp.render(); + }; + + flipViewportHorizontal = () => { + const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT); + + const { flipHorizontal } = vp.getProperties(); + vp.setProperties({ flipHorizontal: !flipHorizontal }); + vp.render(); + }; + + flipViewportVertical = () => { + const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT); + const { flipVertical } = vp.getProperties(); + vp.setProperties({ flipVertical: !flipVertical }); + vp.render(); + }; + + resetCamera = () => { + const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT); + vp.resetCamera(); + vp.render(); + }; + + invertColors = () => { + const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT); + const invert = vp.invert; + vp.setProperties({ invert: !invert }); + vp.render(); + }; + + applyPreset = () => { + const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT); + + if (this.currentStack === 'pt') { + vp.setProperties({ voiRange: { lower: 0, upper: 5 } }); + } else { + vp.setProperties({ voiRange: { lower: 100, upper: 500 } }); + } + + vp.render(); + }; + + switchStack = (evt) => { + const stackName = evt.target.value; + + stackCTViewportToolGroup.removeViewports(renderingEngineUID); + stackPTViewportToolGroup.removeViewports(renderingEngineUID); + + let activeToolGroup; + if (stackName === 'pt') { + activeToolGroup = stackPTViewportToolGroup; + activeToolGroup.addViewports( + renderingEngineUID, + undefined, + VIEWPORT_IDS.STACK.CT + ); + } else { + activeToolGroup = stackCTViewportToolGroup; + activeToolGroup.addViewports( + renderingEngineUID, + undefined, + VIEWPORT_IDS.STACK.CT + ); + } + + const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT); + + vp.setStack(this.stacks[stackName], 0).then(() => { + vp.resetProperties(); + if (stackName === 'pt') { + vp.setProperties({ + voiRange: { lower: 0, upper: 5 }, + invert: true, + }); + } + }); + + this.resetToolModes(activeToolGroup, stackName); + + this.setState({ + currentStack: stackName, + falseColor: false, + activeToolGroup, + ptCtLeftClickTool: 'WindowLevel', // reset ui + }); + }; + + resetViewportProperties = () => { + const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT); + vp.resetProperties(); + vp.resetCamera(); + vp.render(); + }; + + toggleFalseColor = () => { + const falseColor = !this.state.falseColor; + + // TODO toggle vp state + + const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT); + + if (falseColor) { + vp.setColormap(cpuColormaps.hotIron); + } else { + vp.unsetColormap(); + } + + this.setState({ falseColor }); + }; + + render() { + return ( +
+
+
+

+ One Stack CPU Fallback Viewport Example ({this.state.progressText} + ) +

+
+
+ {/* Hide until we update react in a better way {fusionWLDisplay} */} +
+
+ + + + {this.state.currentStack !== 'color' && ( + + )} + + + + + + + + + + + {this.state.viewportGrid.viewports.map((vp, i) => ( +
+ this._canvasNodes.set(i, c)} /> +
+ ))} +
+
+ ); + } +} + +export default OneStackExampleCPU; diff --git a/packages/demo/src/ExampleOneVolume.tsx b/packages/demo/src/ExampleOneVolume.tsx index 358264e3b4..4d3a22db48 100644 --- a/packages/demo/src/ExampleOneVolume.tsx +++ b/packages/demo/src/ExampleOneVolume.tsx @@ -5,19 +5,14 @@ import { createAndCacheVolume, ORIENTATION, VIEWPORT_TYPE, + init as csRenderInit, } from '@ohif/cornerstone-render' -import { - ToolBindings, -} from '@ohif/cornerstone-tools' +import { ToolBindings } from '@ohif/cornerstone-tools' import * as csTools3d from '@ohif/cornerstone-tools' import vtkConstants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants' - -import { - setCTWWWC, -} from './helpers/transferFunctionHelpers' - +import { setCTWWWC } from './helpers/transferFunctionHelpers' import getImageIds from './helpers/getImageIds' import ViewportGrid from './components/ViewportGrid' @@ -61,7 +56,6 @@ class OneVolumeExample extends Component { constructor(props) { super(props) - csTools3d.init() this._canvasNodes = new Map() this._offScreenRef = React.createRef() @@ -87,6 +81,8 @@ class OneVolumeExample extends Component { * LIFECYCLE */ async componentDidMount() { + await csRenderInit() + csTools3d.init() ;({ ctSceneToolGroup } = initToolGroups()) const volumeImageIds = await this.volumeImageIds @@ -149,7 +145,7 @@ class OneVolumeExample extends Component { VIEWPORT_IDS.CT.CORONAL ) - addToolsToToolGroups({ctSceneToolGroup}) + addToolsToToolGroups({ ctSceneToolGroup }) renderingEngine.render() @@ -227,13 +223,13 @@ class OneVolumeExample extends Component { toolGroup.setToolPassive(toolName) }) toolGroup.setToolActive('WindowLevel', { - bindings: [ { mouseButton: ToolBindings.Mouse.Primary } ], + bindings: [{ mouseButton: ToolBindings.Mouse.Primary }], }) toolGroup.setToolActive('Pan', { - bindings: [ { mouseButton: ToolBindings.Mouse.Auxiliary } ], + bindings: [{ mouseButton: ToolBindings.Mouse.Auxiliary }], }) toolGroup.setToolActive('Zoom', { - bindings: [ { mouseButton: ToolBindings.Mouse.Secondary } ], + bindings: [{ mouseButton: ToolBindings.Mouse.Secondary }], }) } @@ -249,7 +245,9 @@ class OneVolumeExample extends Component { ([tool, { bindings, mode }]) => mode === 'Active' && bindings.length && - bindings.some(binding => binding.mouseButton === ToolBindings.Mouse.Primary) + bindings.some( + (binding) => binding.mouseButton === ToolBindings.Mouse.Primary + ) ) ctSceneToolGroup.setToolPassive(activeTool) @@ -258,15 +256,16 @@ class OneVolumeExample extends Component { const currentBindings = ctSceneToolGroup.tools[toolName].bindings ctSceneToolGroup.setToolActive(toolName, { - bindings: [...currentBindings, { mouseButton: ToolBindings.Mouse.Primary } ], + bindings: [ + ...currentBindings, + { mouseButton: ToolBindings.Mouse.Primary }, + ], }) this.renderingEngine.render() this.setState({ ptCtLeftClickTool: toolName }) } - - showOffScreenCanvas = () => { // remove all children this._offScreenRef.current.innerHTML = '' diff --git a/packages/demo/src/ExamplePriorityLoad.tsx b/packages/demo/src/ExamplePriorityLoad.tsx index 96b9780d62..465d1f5044 100644 --- a/packages/demo/src/ExamplePriorityLoad.tsx +++ b/packages/demo/src/ExamplePriorityLoad.tsx @@ -4,10 +4,9 @@ import { RenderingEngine, createAndCacheVolume, requestPoolManager, + init as csRenderInit, } from '@ohif/cornerstone-render' -import { - synchronizers, -} from '@ohif/cornerstone-tools' +import { synchronizers } from '@ohif/cornerstone-tools' import * as csTools3d from '@ohif/cornerstone-tools' import _ from 'lodash' @@ -87,7 +86,6 @@ class PriorityLoadExample extends Component { constructor(props) { super(props) - csTools3d.init() ptCtLayoutTools = ['Levels'].concat(ANNOTATION_TOOLS) this._canvasNodes = new Map() @@ -125,6 +123,8 @@ class PriorityLoadExample extends Component { * LIFECYCLE */ async componentDidMount() { + await csRenderInit() + csTools3d.init() this.axialSync = createCameraPositionSynchronizer('axialSync') this.sagittalSync = createCameraPositionSynchronizer('sagittalSync') this.coronalSync = createCameraPositionSynchronizer('coronalSync') diff --git a/packages/demo/src/ExampleSetVolumes.tsx b/packages/demo/src/ExampleSetVolumes.tsx index 925eec6708..38f6a0575a 100644 --- a/packages/demo/src/ExampleSetVolumes.tsx +++ b/packages/demo/src/ExampleSetVolumes.tsx @@ -3,10 +3,9 @@ import { cache, RenderingEngine, createAndCacheVolume, + init as csRenderInit, } from '@ohif/cornerstone-render' -import { - synchronizers, -} from '@ohif/cornerstone-tools' +import { synchronizers } from '@ohif/cornerstone-tools' import * as csTools3d from '@ohif/cornerstone-tools' import getImageIds from './helpers/getImageIds' @@ -83,7 +82,6 @@ class VTKSetVolumesExample extends Component { constructor(props) { super(props) - csTools3d.init() ptCtLayoutTools = ['Levels'].concat(ANNOTATION_TOOLS) this._canvasNodes = new Map() @@ -124,6 +122,8 @@ class VTKSetVolumesExample extends Component { * LIFECYCLE */ async componentDidMount() { + await csRenderInit() + csTools3d.init() this.axialSync = createCameraPositionSynchronizer('axialSync') this.sagittalSync = createCameraPositionSynchronizer('sagittalSync') this.coronalSync = createCameraPositionSynchronizer('coronalSync') diff --git a/packages/demo/src/ExampleStackViewport.tsx b/packages/demo/src/ExampleStackViewport.tsx index 2e05672c19..9e43ef9f6a 100644 --- a/packages/demo/src/ExampleStackViewport.tsx +++ b/packages/demo/src/ExampleStackViewport.tsx @@ -9,6 +9,7 @@ import { VIEWPORT_TYPE, INTERPOLATION_TYPE, EVENTS as RENDERING_EVENTS, + init as csRenderInit, } from '@ohif/cornerstone-render' import { SynchronizerManager, @@ -57,9 +58,7 @@ let ctSceneToolGroup, stackDXViewportToolGroup, ptSceneToolGroup -const toolsToUse = ANNOTATION_TOOLS.filter( - (tool) => tool !== 'Crosshairs' -) +const toolsToUse = ANNOTATION_TOOLS.filter((tool) => tool !== 'Crosshairs') const ctLayoutTools = ['Levels'].concat(toolsToUse) class StackViewportExample extends Component { @@ -89,7 +88,6 @@ class StackViewportExample extends Component { constructor(props) { super(props) - csTools3d.init() registerWebImageLoader(cs) this._canvasNodes = new Map() this._viewportGridRef = React.createRef() @@ -134,6 +132,8 @@ class StackViewportExample extends Component { * LIFECYCLE */ async componentDidMount() { + await csRenderInit() + csTools3d.init() ;({ ctSceneToolGroup, stackCTViewportToolGroup, @@ -303,7 +303,10 @@ class StackViewportExample extends Component { ptMiddleSlice ) - ptStackViewport.setProperties({ invert: true, voiRange: { lower: 0, upper: 5 } }) + ptStackViewport.setProperties({ + invert: true, + voiRange: { lower: 0, upper: 5 }, + }) // ct + dx + color // const dxColorViewport = renderingEngine.getViewport(VIEWPORT_IDS.STACK.DX) @@ -487,7 +490,7 @@ class StackViewportExample extends Component { const isAnnotationToolOn = toolName !== 'Levels' ? true : false const options = { - bindings: [ { mouseButton: ToolBindings.Mouse.Primary } ], + bindings: [{ mouseButton: ToolBindings.Mouse.Primary }], } if (isAnnotationToolOn) { // Set tool active diff --git a/packages/demo/src/ExampleTestUtils.tsx b/packages/demo/src/ExampleTestUtils.tsx index 65f9db9083..a785976b1b 100644 --- a/packages/demo/src/ExampleTestUtils.tsx +++ b/packages/demo/src/ExampleTestUtils.tsx @@ -6,10 +6,9 @@ import { metaData, VIEWPORT_TYPE, Utilities, + init as csRenderInit, } from '@ohif/cornerstone-render' -import { - ToolBindings, -} from '@ohif/cornerstone-tools' +import { ToolBindings } from '@ohif/cornerstone-tools' import * as csTools3d from '@ohif/cornerstone-tools' import { @@ -34,14 +33,11 @@ const STACK = 'stack' window.cache = cache - const { fakeImageLoader, fakeMetaDataProvider } = Utilities.testUtils let stackCTViewportToolGroup -const toolsToUse = ANNOTATION_TOOLS.filter( - (tool) => tool !== 'Crosshairs' -) +const toolsToUse = ANNOTATION_TOOLS.filter((tool) => tool !== 'Crosshairs') const ctLayoutTools = ['Levels'].concat(toolsToUse) class testUtil extends Component { @@ -65,7 +61,6 @@ class testUtil extends Component { constructor(props) { super(props) - csTools3d.init() this._canvasNodes = new Map() this._offScreenRef = React.createRef() this._viewportGridRef = React.createRef() @@ -73,7 +68,6 @@ class testUtil extends Component { registerImageLoader('fakeImageLoader', fakeImageLoader) metaData.addProvider(fakeMetaDataProvider, 10000) - this.ctStackImageIdsPromise = ['fakeImageLoader:imageURI_64_64_10_5_1_1_0'] this.viewportGridResizeObserver = new ResizeObserver((entries) => { @@ -90,8 +84,9 @@ class testUtil extends Component { * LIFECYCLE */ async componentDidMount() { - - ({ stackCTViewportToolGroup } = initToolGroups()) + await csRenderInit() + csTools3d.init() + ;({ stackCTViewportToolGroup } = initToolGroups()) const ctStackImageIds = await this.ctStackImageIdsPromise @@ -128,14 +123,14 @@ class testUtil extends Component { const ctMiddleSlice = Math.floor(ctStackImageIds.length / 2) await ctStackViewport.setStack( sortImageIdsByIPP(ctStackImageIds), - ctMiddleSlice, + ctMiddleSlice ) // Start listening for resize this.viewportGridResizeObserver.observe(this._viewportGridRef.current) } - componentWillUnmount() { + componentWillUnmount() { // Stop listening for resize if (this.viewportGridResizeObserver) { this.viewportGridResizeObserver.disconnect() @@ -161,7 +156,7 @@ class testUtil extends Component { const isAnnotationToolOn = toolName !== 'Levels' ? true : false const options = { - bindings: [ { mouseButton: ToolBindings.Mouse.Primary } ], + bindings: [{ mouseButton: ToolBindings.Mouse.Primary }], } if (isAnnotationToolOn) { // Set tool active @@ -210,13 +205,15 @@ class testUtil extends Component {

Test Stack Render ({this.state.progressText})

-

The purpose of this demo is to render the stack test data that we utilize in testing

+

+ The purpose of this demo is to render the stack test data that we + utilize in testing +

-
+ >