import { vtkInteractorStyleMPRCrosshairs, vtkInteractorStyleMPRWindowLevel, vtkInteractorStyleMPRSlice, vtkInteractorStyleMPRRotate, vtkSVGCrosshairsWidget, vtkSVGWidgetManager, } from 'react-vtkjs-viewport'; import setMPRLayout from './utils/setMPRLayout.js'; import setViewportToVTK from './utils/setViewportToVTK.js'; import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js'; import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate'; import vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder'; import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js'; const { BlendMode } = Constants; // TODO: Put this somewhere else let apis = {}; let currentSlabThickness = 0.1; let defaultSlabThickness = 0.1; function getCrosshairCallbackForIndex(index) { return ({ worldPos }) => { // Set camera focal point to world coordinate for linked views apis.forEach((api, viewportIndex) => { if (viewportIndex !== index) { // We are basically doing the same as getSlice but with the world coordinate // that we want to jump to instead of the camera focal point. // I would rather do the camera adjustment directly but I keep // doing it wrong and so this is good enough for now. const renderWindow = api.genericRenderWindow.getRenderWindow(); const istyle = renderWindow.getInteractor().getInteractorStyle(); const sliceNormal = istyle.getSliceNormal(); const transform = vtkMatrixBuilder .buildFromDegree() .identity() .rotateFromDirections(sliceNormal, [1, 0, 0]); const mutatedWorldPos = worldPos.slice(); transform.apply(mutatedWorldPos); const slice = mutatedWorldPos[0]; istyle.setSlice(slice); renderWindow.render(); } const renderer = api.genericRenderWindow.getRenderer(); const wPos = vtkCoordinate.newInstance(); wPos.setCoordinateSystemToWorld(); wPos.setValue(worldPos); const displayPosition = wPos.getComputedDisplayValue(renderer); const { svgWidgetManager } = api; api.svgWidgets.crosshairsWidget.setPoint( displayPosition[0], displayPosition[1] ); svgWidgetManager.render(); }); }; } async function _getActiveViewportVTKApi(viewports) { const { layout, viewportSpecificData, activeViewportIndex } = viewports; const currentData = layout.viewports[activeViewportIndex]; if (currentData && currentData.plugin === 'vtk') { // TODO: I was storing/pulling this from Redux but ran into weird issues if (apis[activeViewportIndex]) { return apis[activeViewportIndex]; } } const displaySet = viewportSpecificData[activeViewportIndex]; let api; if (!api) { try { api = await setViewportToVTK( displaySet, activeViewportIndex, layout, viewportSpecificData ); } catch (error) { throw new Error(error); } } return api; } function _setView(api, sliceNormal, viewUp) { const renderWindow = api.genericRenderWindow.getRenderWindow(); const renderer = api.genericRenderWindow.getRenderer(); const camera = renderer.getActiveCamera(); const istyle = renderWindow.getInteractor().getInteractorStyle(); istyle.setSliceNormal(...sliceNormal); istyle.setViewUp(...viewUp); renderWindow.render(); } function switchMPRInteractors(api, istyle) { const renderWindow = api.genericRenderWindow.getRenderWindow(); const renderer = api.genericRenderWindow.getRenderer(); const camera = renderer.getActiveCamera(); const currentIStyle = renderWindow.getInteractor().getInteractorStyle(); let currentViewport; if (currentIStyle.getViewport && istyle.getViewport) { currentViewport = currentIStyle.getViewport(); } let currentSlabThickness; if (currentIStyle.getSlabThickness && istyle.getSlabThickness) { currentSlabThickness = currentIStyle.getSlabThickness(); defaultSlabThickness = currentSlabThickness; } renderWindow.getInteractor().setInteractorStyle(istyle); // TODO: Not sure why this is required the second time this function is called istyle.setInteractor(renderWindow.getInteractor()); if (currentViewport) { istyle.setViewport(currentViewport); } if (istyle.getVolumeMapper() !== api.volumes[0]) { if (currentSlabThickness) { istyle.setSlabThickness(currentSlabThickness); } istyle.setVolumeMapper(api.volumes[0]); } } const actions = { axial: async ({ viewports }) => { const api = await _getActiveViewportVTKApi(viewports); apis[viewports.activeViewportIndex] = api; _setView(api, [0, 0, 1], [0, -1, 0]); }, sagittal: async ({ viewports }) => { const api = await _getActiveViewportVTKApi(viewports); apis[viewports.activeViewportIndex] = api; _setView(api, [1, 0, 0], [0, 0, 1]); }, coronal: async ({ viewports }) => { const api = await _getActiveViewportVTKApi(viewports); apis[viewports.activeViewportIndex] = api; _setView(api, [0, 1, 0], [0, 0, 1]); }, enableRotateTool: () => { apis.forEach(api => { const istyle = vtkInteractorStyleMPRRotate.newInstance(); switchMPRInteractors(api, istyle); }); }, enableCrosshairsTool: () => { apis.forEach((api, index) => { const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); switchMPRInteractors(api, istyle); istyle.setCallback(getCrosshairCallbackForIndex(index)); }); }, enableLevelTool: () => { apis.forEach(api => { const istyle = vtkInteractorStyleMPRWindowLevel.newInstance(); switchMPRInteractors(api, istyle); }); }, setSlabThickness: ({ slabThickness }) => { currentSlabThickness = slabThickness; apis.forEach(api => { const renderWindow = api.genericRenderWindow.getRenderWindow(); const istyle = renderWindow.getInteractor().getInteractorStyle(); if (istyle.setSlabThickness) { istyle.setSlabThickness(currentSlabThickness); } renderWindow.render(); }); }, changeSlabThickness: ({ change }) => { currentSlabThickness += change; currentSlabThickness = Math.max(currentSlabThickness, 0.1); apis.forEach(api => { const renderWindow = api.genericRenderWindow.getRenderWindow(); const istyle = renderWindow.getInteractor().getInteractorStyle(); if (istyle.setSlabThickness) { istyle.setSlabThickness(currentSlabThickness); } renderWindow.render(); }); }, setBlendModeToComposite: () => { apis.forEach(api => { const renderWindow = api.genericRenderWindow.getRenderWindow(); const istyle = renderWindow.getInteractor().getInteractorStyle(); const mapper = api.volumes[0].getMapper(); if (mapper.setBlendModeToComposite) { mapper.setBlendModeToComposite(); } if (istyle.setSlabThickness) { istyle.setSlabThickness(defaultSlabThickness); } renderWindow.render(); }); }, setBlendModeToMaximumIntensity: () => { apis.forEach(api => { const renderWindow = api.genericRenderWindow.getRenderWindow(); const mapper = api.volumes[0].getMapper(); if (mapper.setBlendModeToMaximumIntensity) { mapper.setBlendModeToMaximumIntensity(); } renderWindow.render(); }); }, setBlendMode: ({ blendMode }) => { apis.forEach(api => { const renderWindow = api.genericRenderWindow.getRenderWindow(); const istyle = renderWindow.getInteractor().getInteractorStyle(); api.volumes[0].getMapper().setBlendMode(blendMode); renderWindow.render(); }); }, mpr2d: async ({ viewports }) => { const displaySet = viewports.viewportSpecificData[viewports.activeViewportIndex]; let apiByViewport; try { apiByViewport = await setMPRLayout(displaySet); } catch (error) { throw new Error(error); } apis = apiByViewport; const rgbTransferFunction = apiByViewport[0].volumes[0] .getProperty() .getRGBTransferFunction(0); const onModifiedSubscription = rgbTransferFunction.onModified(() => { apiByViewport.forEach(a => { const renderWindow = a.genericRenderWindow.getRenderWindow(); renderWindow.render(); }); }); vtkViewportSubscriptionManager.pushSubscription(0, onModifiedSubscription); apiByViewport.forEach((api, index) => { const renderWindow = api.genericRenderWindow.getRenderWindow(); const renderer = api.genericRenderWindow.getRenderer(); const camera = renderer.getActiveCamera(); const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); renderWindow.getInteractor().setInteractorStyle(istyle); istyle.setVolumeMapper(api.volumes[0]); istyle.setCallback(getCrosshairCallbackForIndex(index)); const svgWidgetManager = vtkSVGWidgetManager.newInstance(); svgWidgetManager.setRenderer(renderer); svgWidgetManager.setScale(1); const crosshairsWidget = vtkSVGCrosshairsWidget.newInstance(); svgWidgetManager.addWidget(crosshairsWidget); svgWidgetManager.render(); api.svgWidgetManager = svgWidgetManager; api.svgWidgets = { crosshairsWidget, }; switch (index) { default: case 0: //Axial istyle.setSliceNormal(0, 0, 1); istyle.setViewUp(0, -1, 0); break; case 1: // sagittal istyle.setSliceNormal(1, 0, 0); istyle.setViewUp(0, 0, 1); break; case 2: // Coronal istyle.setSliceNormal(0, 1, 0); istyle.setViewUp(0, 0, 1); break; } renderWindow.render(); }); }, }; window.vtkActions = actions; const definitions = { axial: { commandFn: actions.axial, storeContexts: ['viewports'], options: {}, }, coronal: { commandFn: actions.coronal, storeContexts: ['viewports'], options: {}, }, sagittal: { commandFn: actions.sagittal, storeContexts: ['viewports'], options: {}, }, enableRotateTool: { commandFn: actions.enableRotateTool, storeContexts: [], options: {}, }, enableCrosshairsTool: { commandFn: actions.enableCrosshairsTool, storeContexts: [], options: {}, }, enableLevelTool: { commandFn: actions.enableLevelTool, storeContexts: [], options: {}, }, setBlendModeToComposite: { commandFn: actions.setBlendModeToComposite, storeContexts: [], options: { blendMode: BlendMode.COMPOSITE_BLEND }, }, setBlendModeToMaximumIntensity: { commandFn: actions.setBlendModeToMaximumIntensity, storeContexts: [], options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND }, }, setBlendModeToMinimumIntensity: { commandFn: actions.setBlendMode, storeContexts: [], options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND }, }, setBlendModeToAverageIntensity: { commandFn: actions.setBlendMode, storeContexts: [], options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND }, }, setSlabThickness: { // TODO: How do we pass in a function argument? commandFn: actions.setSlabThickness, storeContexts: [], options: {}, }, increaseSlabThickness: { commandFn: actions.changeSlabThickness, storeContexts: [], options: { change: 3, }, }, decreaseSlabThickness: { commandFn: actions.changeSlabThickness, storeContexts: [], options: { change: -3, }, }, mpr2d: { commandFn: actions.mpr2d, storeContexts: ['viewports'], options: {}, context: 'VIEWER', }, }; export default { definitions, defaultContext: 'ACTIVE_VIEWPORT::VTK', };