refactor(@ohif/extension-vtk): Update VTK extension to add basic MPR functionality
This commit is contained in:
parent
04e00fad20
commit
9573f67132
@ -1,71 +1,81 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "dist/index.es.js",
|
||||
"jsnext:main": "dist/index.es.js",
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"prepublishOnly": "npm run build",
|
||||
"start": "rollup -c -w",
|
||||
"build": "webpack --progress --colors --mode development",
|
||||
"build:release": "webpack --progress --colors --mode production",
|
||||
"start": "webpack --watch --progress --colors --mode development",
|
||||
"prepare": "yarn run build:release",
|
||||
"predeploy": "cd example && yarn install && yarn run build:release",
|
||||
"prepublishOnly": "yarn run build:release",
|
||||
"lint": "eslint -c .eslintrc --fix src && prettier --single-quote --write src/**/*.{js,jsx,json,css}"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/i18n": "0.0.4",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"react-i18next": "^10.11.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-wado-image-loader": "^2.2.3",
|
||||
"dcmjs": "^0.3.6",
|
||||
"dcmjs": "^0.4.7",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"ohif-core": "^0.3.3",
|
||||
"@ohif/i18n": "0.0.4",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^15.0.0 || ^16.0.0",
|
||||
"react-dom": "^15.0.0 || ^16.0.0",
|
||||
"react-redux": "^6.0.0",
|
||||
"react-resize-detector": "^3.4.0",
|
||||
"react-viewerbase": "^0.6.0",
|
||||
"ohif-core": "^0.5.9",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-redux": "^7.1.0",
|
||||
"react-resize-detector": "^4.2.0",
|
||||
"react-viewerbase": "^0.8.1",
|
||||
"redux": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/plugin-external-helpers": "^7.2.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.2.3",
|
||||
"@babel/plugin-transform-runtime": "^7.2.0",
|
||||
"@babel/preset-env": "^7.2.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.4",
|
||||
"@babel/plugin-transform-runtime": "^7.4.4",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"react-i18next": "^10.11.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.0.6",
|
||||
"cornerstone-tools": "^3.13.0",
|
||||
"cornerstone-wado-image-loader": "^2.2.3",
|
||||
"cross-env": "^5.2.0",
|
||||
"eslint": "5.13.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-node": "^8.0.0",
|
||||
"eslint-plugin-promise": "^4.0.1",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"dcmjs": "^0.4.7",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-plugin-import": "^2.17.3",
|
||||
"eslint-plugin-node": "^9.1.0",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-react": "^7.13.0",
|
||||
"gh-pages": "^2.0.1",
|
||||
"husky": "^1.3.1",
|
||||
"lint-staged": "^8.1.0",
|
||||
"prettier": "^1.15.3",
|
||||
"husky": "^2.4.1",
|
||||
"lint-staged": "^8.2.0",
|
||||
"ohif-core": "^0.5.9",
|
||||
"prettier": "^1.18.2",
|
||||
"react": "^16.6.3",
|
||||
"react-dom": "^16.6.3",
|
||||
"rollup": "^1.1.2",
|
||||
"rollup-plugin-babel": "^4.2.0",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-copy": "^2.0.1",
|
||||
"rollup-plugin-node-builtins": "^2.1.2",
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.0",
|
||||
"rollup-plugin-postcss": "^2.0.3",
|
||||
"rollup-plugin-url": "^2.1.0",
|
||||
"stylelint": "^9.9.0",
|
||||
"stylelint-config-recommended": "^2.1.0",
|
||||
"stylus": "^0.54.5"
|
||||
"react-redux": "^7.1.0",
|
||||
"react-viewerbase": "^0.8.1",
|
||||
"redux": "^4.0.1",
|
||||
"shader-loader": "^1.3.1",
|
||||
"stylelint": "^10.1.0",
|
||||
"stylelint-config-recommended": "^2.2.0",
|
||||
"stylus": "^0.54.5",
|
||||
"webpack": "^4.33.0",
|
||||
"webpack-cli": "^3.3.4",
|
||||
"worker-loader": "^2.0.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@ -92,8 +102,8 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.2.0",
|
||||
"classnames": "^2.2.6",
|
||||
"react-vtkjs-viewport": "^0.0.7"
|
||||
"@babel/runtime": "^7.4.5",
|
||||
"@ohif/i18n": "0.0.4",
|
||||
"react-vtkjs-viewport": "0.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,33 +5,50 @@ import OHIF from 'ohif-core';
|
||||
const { setToolActive } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const activeButton = state.tools.buttons.find(tool => tool.active === true);
|
||||
|
||||
return {
|
||||
buttons: [
|
||||
{
|
||||
command: 'Crosshairs',
|
||||
type: 'tool',
|
||||
text: 'Crosshairs',
|
||||
icon: 'crosshairs',
|
||||
active: true,
|
||||
onClick: () => {
|
||||
// TODO: Make these use setToolActive instead
|
||||
window.commandsManager.runCommand('enableCrosshairsTool', {}, 'vtk');
|
||||
}
|
||||
},
|
||||
{
|
||||
command: 'WWWC',
|
||||
type: 'tool',
|
||||
text: 'WWWC',
|
||||
icon: 'level',
|
||||
active: true,
|
||||
onClick: () => {
|
||||
// TODO: Make these use setToolActive instead
|
||||
window.commandsManager.runCommand('enableLevelTool', {}, 'vtk');
|
||||
}
|
||||
}
|
||||
/* Disabled temporarily due to issues with the interactor style
|
||||
{
|
||||
command: 'Rotate',
|
||||
type: 'tool',
|
||||
text: 'Rotate',
|
||||
icon: '3d-rotate',
|
||||
active: true
|
||||
}
|
||||
active: false,
|
||||
onClick: () => {
|
||||
// TODO: Make these use setToolActive instead
|
||||
window.commandsManager.runCommand('enableRotateTool', {}, 'vtk');
|
||||
}
|
||||
},*/
|
||||
],
|
||||
activeCommand: 'Rotate'
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setToolActive: tool => {
|
||||
//dispatch(setToolActive(tool.command))
|
||||
}
|
||||
activeCommand: 'Crosshairs'
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedToolbarSection = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
null
|
||||
)(ToolbarSection);
|
||||
|
||||
export default ConnectedToolbarSection;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { VTKMPRViewport } from 'react-vtkjs-viewport';
|
||||
import { View2D } from 'react-vtkjs-viewport';
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
const {
|
||||
@ -17,7 +17,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
}
|
||||
|
||||
// If this is the active viewport, enable prefetching.
|
||||
const { viewportIndex } = ownProps; //.viewportData;
|
||||
const { viewportIndex } = ownProps;
|
||||
const isActive = viewportIndex === state.viewports.activeViewportIndex;
|
||||
const viewportSpecificData =
|
||||
state.viewports.viewportSpecificData[viewportIndex] || {};
|
||||
@ -31,7 +31,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
...pluginDetails,
|
||||
activeTool: activeButton && activeButton.command,
|
||||
...dataFromStore,
|
||||
enableStackPrefetch: isActive,
|
||||
enableStackPrefetch: isActive
|
||||
};
|
||||
};
|
||||
|
||||
@ -45,17 +45,44 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
|
||||
setViewportSpecificData: data => {
|
||||
dispatch(setViewportSpecificData(viewportIndex, data));
|
||||
},
|
||||
|
||||
clearViewportSpecificData: () => {
|
||||
dispatch(clearViewportSpecificData(viewportIndex));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
||||
const { afterCreation } = propsFromState;
|
||||
const { setViewportSpecificData } = propsFromDispatch;
|
||||
|
||||
const props = {
|
||||
...propsFromState,
|
||||
...propsFromDispatch,
|
||||
...ownProps,
|
||||
/**
|
||||
* Our component sets up the underlying dom element on "componentDidMount"
|
||||
* for use with VTK.
|
||||
*
|
||||
* The onCreated prop passes back an Object containing many of the internal
|
||||
* components of the VTK scene. We can grab a reference to these here, to
|
||||
* make playing with VTK's native methods easier.
|
||||
*
|
||||
* A similar approach is taken with the Cornerstone extension.
|
||||
*/
|
||||
onCreated: api => {
|
||||
// Store the API details for later
|
||||
//setViewportSpecificData({ vtkApi: api });
|
||||
|
||||
if (afterCreation && typeof afterCreation === 'function') {
|
||||
afterCreation(api);
|
||||
}
|
||||
}
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
const ConnectedVTKViewport = connect(
|
||||
mapStateToProps,
|
||||
//mapDispatchToProps
|
||||
)(VTKMPRViewport);
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(View2D);
|
||||
|
||||
export default ConnectedVTKViewport;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import OHIFVTKViewport from './OHIFVTKViewport.js';
|
||||
import ToolbarModule from './ToolbarModule.js';
|
||||
import { definitions } from './commands';
|
||||
|
||||
/**
|
||||
* Pass in 'children' to a React component. The purpose of this is to
|
||||
@ -13,7 +14,7 @@ import ToolbarModule from './ToolbarModule.js';
|
||||
*/
|
||||
function withChildren(WrappedComponent, children) {
|
||||
return function(props) {
|
||||
return <WrappedComponent children={children} { ...props } />;
|
||||
return <WrappedComponent children={children} {...props} />;
|
||||
};
|
||||
}
|
||||
|
||||
@ -21,8 +22,10 @@ function withChildren(WrappedComponent, children) {
|
||||
// https://github.com/whitecolor/yalc
|
||||
|
||||
export default class OHIFVTKExtension {
|
||||
constructor(children) {
|
||||
constructor({ children, commandsManager }) {
|
||||
this.children = children;
|
||||
|
||||
_registerCommands(commandsManager, definitions, this.getExtensionId());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,3 +55,21 @@ export default class OHIFVTKExtension {
|
||||
return ToolbarModule;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all Viewer commands
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function _registerCommands(commandsManager, definitions, commandContext) {
|
||||
commandsManager.createContext(commandContext);
|
||||
Object.keys(definitions).forEach(commandName => {
|
||||
const commandDefinition = definitions[commandName];
|
||||
|
||||
commandsManager.registerCommand(
|
||||
commandContext,
|
||||
commandName,
|
||||
commandDefinition
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF from 'ohif-core';
|
||||
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
|
||||
|
||||
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
|
||||
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
|
||||
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
|
||||
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
|
||||
|
||||
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
||||
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||
import LoadingIndicator from './LoadingIndicator.js';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
@ -27,9 +33,36 @@ specialCaseHandlers[
|
||||
SOP_CLASSES.SEGMENTATION_STORAGE
|
||||
] = handleSegmentationStorage;
|
||||
|
||||
// TODO: Figure out where we plan to put this long term
|
||||
const volumeCache = {};
|
||||
|
||||
/**
|
||||
* Create a labelmap image with the same dimensions as our background volume.
|
||||
*
|
||||
* @param backgroundImageData vtkImageData
|
||||
*/
|
||||
function createLabelMapImageData(backgroundImageData) {
|
||||
const labelMapData = vtkImageData.newInstance(
|
||||
backgroundImageData.get('spacing', 'origin', 'direction')
|
||||
);
|
||||
labelMapData.setDimensions(backgroundImageData.getDimensions());
|
||||
labelMapData.computeTransforms();
|
||||
|
||||
const values = new Uint8Array(backgroundImageData.getNumberOfPoints());
|
||||
const dataArray = vtkDataArray.newInstance({
|
||||
numberOfComponents: 1, // labelmap with single component
|
||||
values
|
||||
});
|
||||
labelMapData.getPointData().setScalars(dataArray);
|
||||
|
||||
return labelMapData;
|
||||
}
|
||||
|
||||
class OHIFVTKViewport extends Component {
|
||||
state = {
|
||||
viewportData: null
|
||||
volumes: null,
|
||||
paintFilterLabelMapImageData: null,
|
||||
paintFilterBackgroundImageData: null
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@ -39,14 +72,14 @@ class OHIFVTKViewport extends Component {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
static id = 'OHIFCornerstoneViewport';
|
||||
static id = 'OHIFVTKViewport';
|
||||
|
||||
static init() {
|
||||
console.log('OHIFCornerstoneViewport init()');
|
||||
console.log('OHIFVTKViewport init()');
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
console.log('OHIFCornerstoneViewport destroy()');
|
||||
console.log('OHIFVTKViewport destroy()');
|
||||
StackManager.clearStacks();
|
||||
}
|
||||
|
||||
@ -97,22 +130,6 @@ class OHIFVTKViewport extends Component {
|
||||
return stack;
|
||||
}
|
||||
|
||||
static getViewportData = (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
) => {
|
||||
return OHIFVTKViewport.getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
};
|
||||
|
||||
getViewportData = (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
@ -121,63 +138,64 @@ class OHIFVTKViewport extends Component {
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stack = OHIFVTKViewport.getViewportData(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
const stack = OHIFVTKViewport.getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
let imageDataObject;
|
||||
let labelmapDataObject;
|
||||
let doneLoadingCallback;
|
||||
let callbacks;
|
||||
let imageDataObject;
|
||||
let labelmapDataObject;
|
||||
|
||||
switch (sopClassUid) {
|
||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||
throw new Error("Not yet implemented");
|
||||
switch (sopClassUid) {
|
||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||
throw new Error('Not yet implemented');
|
||||
|
||||
const data = handleSegmentationStorage(stack.imageIds, displaySetInstanceUid, cornerstone);
|
||||
imageDataObject = data.referenceDataObject;
|
||||
labelmapDataObject = data.labelmapDataObject;
|
||||
const data = handleSegmentationStorage(
|
||||
stack.imageIds,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
|
||||
doneLoadingCallback = () => {
|
||||
resolve({
|
||||
data: imageDataObject.vtkImageData,
|
||||
labelmap: labelmapDataObject
|
||||
});
|
||||
imageDataObject = data.referenceDataObject;
|
||||
labelmapDataObject = data.labelmapDataObject;
|
||||
|
||||
return loadImageData(imageDataObject).then(() => {
|
||||
return {
|
||||
data: imageDataObject.vtkImageData,
|
||||
labelmap: labelmapDataObject
|
||||
};
|
||||
});
|
||||
default:
|
||||
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
|
||||
|
||||
callbacks = [
|
||||
doneLoadingCallback
|
||||
];
|
||||
|
||||
loadImageData(imageDataObject, callbacks, cornerstone);
|
||||
|
||||
break;
|
||||
default:
|
||||
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid, cornerstone);
|
||||
doneLoadingCallback = () => {
|
||||
resolve({
|
||||
data: imageDataObject.vtkImageData,
|
||||
});
|
||||
return loadImageData(imageDataObject).then(() => {
|
||||
return {
|
||||
data: imageDataObject.vtkImageData
|
||||
};
|
||||
|
||||
callbacks = [
|
||||
doneLoadingCallback
|
||||
];
|
||||
|
||||
loadImageData(imageDataObject, callbacks, cornerstone);
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setStateFromProps() {
|
||||
getOrCreateVolume(data, displaySetInstanceUid) {
|
||||
if (volumeCache[displaySetInstanceUid]) {
|
||||
return volumeCache[displaySetInstanceUid];
|
||||
}
|
||||
|
||||
const volumeActor = vtkVolume.newInstance();
|
||||
const volumeMapper = vtkVolumeMapper.newInstance();
|
||||
|
||||
volumeActor.setMapper(volumeMapper);
|
||||
volumeMapper.setInputData(data);
|
||||
|
||||
volumeCache[displaySetInstanceUid] = volumeActor;
|
||||
|
||||
return volumeActor;
|
||||
}
|
||||
|
||||
async setStateFromProps() {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const {
|
||||
studyInstanceUid,
|
||||
@ -195,18 +213,25 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
const sopClassUid = sopClassUids[0];
|
||||
|
||||
this.getViewportData(
|
||||
let { data, labelmap } = await this.getViewportData(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
).then(({ data, labelmap })=> {
|
||||
this.setState({
|
||||
viewportData: data,
|
||||
labelmap
|
||||
});
|
||||
);
|
||||
|
||||
if (!labelmap) {
|
||||
labelmap = createLabelMapImageData(data);
|
||||
}
|
||||
|
||||
const volumeActor = this.getOrCreateVolume(data, displaySetInstanceUid);
|
||||
|
||||
this.setState({
|
||||
volumes: [volumeActor],
|
||||
paintFilterBackgroundImageData: data,
|
||||
paintFilterLabelMapImageData: labelmap
|
||||
});
|
||||
}
|
||||
|
||||
@ -245,15 +270,22 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.state.viewportData ? (
|
||||
{this.state.volumes ? (
|
||||
<ConnectedVTKViewport
|
||||
data={this.state.viewportData}
|
||||
labelmap={this.state.labelmap}
|
||||
volumes={this.state.volumes}
|
||||
paintFilterLabelMapImageData={
|
||||
this.state.paintFilterLabelMapImageData
|
||||
}
|
||||
paintFilterBackgroundImageData={
|
||||
this.state.paintFilterBackgroundImageData
|
||||
}
|
||||
viewportIndex={this.props.viewportIndex}
|
||||
/>
|
||||
) : <div style={style}>
|
||||
<LoadingIndicator/>
|
||||
</div>}
|
||||
) : (
|
||||
<div style={style}>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
)}
|
||||
{childrenWithProps}
|
||||
</>
|
||||
);
|
||||
|
||||
273
extensions/ohif-vtk-extension/src/commands.js
Normal file
273
extensions/ohif-vtk-extension/src/commands.js
Normal file
@ -0,0 +1,273 @@
|
||||
import {
|
||||
vtkInteractorStyleMPRWindowLevel,
|
||||
vtkInteractorStyleMPRSlice,
|
||||
vtkInteractorStyleMPRCrosshairs
|
||||
} from 'react-vtkjs-viewport';
|
||||
|
||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||
import setMPRLayout from './utils/setMPRLayout.js';
|
||||
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
|
||||
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
|
||||
|
||||
// TODO: Should be another way to get this
|
||||
const commandsManager = window.commandsManager;
|
||||
|
||||
// TODO: Put this somewhere else
|
||||
let apis = {};
|
||||
|
||||
function getCrosshairCallbackForIndex(index) {
|
||||
return worldPos => {
|
||||
// Set camera focal point to world coordinate for linked views
|
||||
apis.forEach((api, viewportIndex) => {
|
||||
if (viewportIndex === index) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
camera.setViewUp(...viewUp);
|
||||
|
||||
api.volumes[0].getMapper().setSampleDistance(5.0);
|
||||
api.volumes[0].getMapper().setMaximumSamplesPerRay(2000);
|
||||
|
||||
renderWindow.render();
|
||||
}
|
||||
|
||||
function switchMPRInteractors(api, istyle) {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
let currentNormal;
|
||||
if (istyle.getSliceNormal) {
|
||||
currentNormal = istyle.getSliceNormal();
|
||||
}
|
||||
|
||||
// TODO: This is a hacky workaround because disabling the vtkInteractorStyleMPRSlice is currently
|
||||
// broken. The camera.onModified is never removed. (https://github.com/Kitware/vtk-js/issues/1110)
|
||||
renderWindow
|
||||
.getInteractor()
|
||||
.getInteractorStyle()
|
||||
.setInteractor(null);
|
||||
|
||||
renderWindow.getInteractor().setInteractorStyle(istyle);
|
||||
|
||||
// TODO: Not sure why this is required the second time this function is called
|
||||
istyle.setInteractor(renderWindow.getInteractor());
|
||||
|
||||
if (istyle.getVolumeMapper() !== api.volumes[0]) {
|
||||
if (currentNormal) {
|
||||
istyle.setSliceNormal(currentNormal);
|
||||
}
|
||||
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: async ({ viewports }) => {
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRSlice.newInstance();
|
||||
|
||||
switchMPRInteractors(api, istyle);
|
||||
});
|
||||
},
|
||||
enableCrosshairsTool: async ({ viewports }) => {
|
||||
apis.forEach((api, index) => {
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
switchMPRInteractors(api, istyle);
|
||||
|
||||
istyle.setCallback(getCrosshairCallbackForIndex(index));
|
||||
});
|
||||
},
|
||||
enableLevelTool: async ({ viewports }) => {
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||
|
||||
switchMPRInteractors(api, istyle);
|
||||
});
|
||||
},
|
||||
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);
|
||||
rgbTransferFunction.onModified(() => {
|
||||
apiByViewport.forEach(a => {
|
||||
const renderWindow = a.genericRenderWindow.getRenderWindow();
|
||||
|
||||
renderWindow.render();
|
||||
});
|
||||
});*/
|
||||
|
||||
apis[0].volumes[0].getMapper().setSampleDistance(1.5);
|
||||
|
||||
apiByViewport.forEach((api, index) => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const renderer = api.genericRenderWindow.getRenderer();
|
||||
const camera = renderer.getActiveCamera();
|
||||
|
||||
// TODO: This is a hacky workaround because disabling the vtkInteractorStyleMPRSlice is currently
|
||||
// broken. The camera.onModified is never removed. (https://github.com/Kitware/vtk-js/issues/1110)
|
||||
renderWindow
|
||||
.getInteractor()
|
||||
.getInteractorStyle()
|
||||
.setInteractor(null);
|
||||
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
renderWindow.getInteractor().setInteractorStyle(istyle);
|
||||
|
||||
istyle.setVolumeMapper(api.volumes[0]);
|
||||
istyle.setCallback(getCrosshairCallbackForIndex(index));
|
||||
|
||||
switch (index) {
|
||||
default:
|
||||
case 0:
|
||||
//Axial
|
||||
istyle.setSliceNormal(0, 0, 1);
|
||||
camera.setViewUp(0, -1, 0);
|
||||
|
||||
break;
|
||||
case 1:
|
||||
// sagittal
|
||||
istyle.setSliceNormal(1, 0, 0);
|
||||
camera.setViewUp(0, 0, 1);
|
||||
break;
|
||||
case 2:
|
||||
// Coronal
|
||||
istyle.setSliceNormal(0, 1, 0);
|
||||
camera.setViewUp(0, 0, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
renderWindow.render();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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: ['viewports'],
|
||||
options: {}
|
||||
},
|
||||
enableCrosshairsTool: {
|
||||
commandFn: actions.enableCrosshairsTool,
|
||||
storeContexts: ['viewports'],
|
||||
options: {}
|
||||
},
|
||||
enableLevelTool: {
|
||||
commandFn: actions.enableLevelTool,
|
||||
storeContexts: ['viewports'],
|
||||
options: {}
|
||||
},
|
||||
mpr2d: {
|
||||
commandFn: actions.mpr2d,
|
||||
storeContexts: ['viewports'],
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
|
||||
export { definitions };
|
||||
@ -1,5 +1,6 @@
|
||||
import OHIF from 'ohif-core';
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import { api } from 'dicomweb-client';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
@ -47,12 +48,25 @@ function getCornerstoneStack(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
return stackClone;
|
||||
}
|
||||
|
||||
function retrieveDicomData(wadoUri) {
|
||||
// TODO: Authorization header depends on the server. If we ever have multiple servers
|
||||
// we will need to figure out how / when to pass this information in.
|
||||
return fetch(wadoUri, {
|
||||
headers: OHIF.DICOMWeb.getAuthorizationHeader()
|
||||
}).then(response => response.arrayBuffer());
|
||||
function retrieveDicomData(
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID,
|
||||
wadoRoot
|
||||
) {
|
||||
const config = {
|
||||
url: wadoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader()
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const options = {
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID
|
||||
};
|
||||
|
||||
return dicomWeb.retrieveInstance(options);
|
||||
}
|
||||
|
||||
async function handleSegmentationStorage(
|
||||
@ -68,8 +82,22 @@ async function handleSegmentationStorage(
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
const segWadoUri = displaySet.images[0].getData().wadouri;
|
||||
const arrayBuffer = await retrieveDicomData(segWadoUri);
|
||||
|
||||
// TODO: This is terrible but we need to use WADO-RS or we can't retrieve the SEG
|
||||
// from google cloud
|
||||
const wadoRoot = displaySet.images[0].getData().wadoRoot;
|
||||
|
||||
const StudyInstanceUID = displaySet.images[0].getStudyInstanceUID();
|
||||
const SeriesInstanceUID = displaySet.images[0].getSeriesInstanceUID();
|
||||
const SOPInstanceUID = displaySet.images[0].getSOPInstanceUID();
|
||||
|
||||
const arrayBuffer = await retrieveDicomData(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
wadoRoot
|
||||
);
|
||||
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
@ -109,7 +137,7 @@ async function handleSegmentationStorage(
|
||||
return {
|
||||
referenceDataObject,
|
||||
labelmapDataObject
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { redux } from 'ohif-core';
|
||||
|
||||
const { setViewportLayoutAndData } = redux.actions;
|
||||
|
||||
// TODO: Should not be getting dispatch from the window, but I'm not sure how else to do it cleanly
|
||||
export default function setLayoutAndViewportData(layout, viewportSpecificData) {
|
||||
const action = setViewportLayoutAndData(layout, viewportSpecificData);
|
||||
|
||||
window.store.dispatch(action);
|
||||
}
|
||||
61
extensions/ohif-vtk-extension/src/utils/setMPRLayout.js
Normal file
61
extensions/ohif-vtk-extension/src/utils/setMPRLayout.js
Normal file
@ -0,0 +1,61 @@
|
||||
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
|
||||
import setSingleLayoutData from './setSingleLayoutData.js';
|
||||
|
||||
export default function setMPRLayout(displaySet) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let viewports = [];
|
||||
const rows = 1;
|
||||
const columns = 3;
|
||||
const numViewports = rows * columns;
|
||||
const viewportSpecificData = {};
|
||||
for (let i = 0; i < numViewports; i++) {
|
||||
viewports.push({
|
||||
height: `${100 / rows}%`,
|
||||
width: `${100 / columns}%`
|
||||
});
|
||||
|
||||
viewportSpecificData[i] = displaySet;
|
||||
viewportSpecificData[i].plugin = 'vtk';
|
||||
}
|
||||
const layout = {
|
||||
viewports
|
||||
};
|
||||
|
||||
const viewportIndices = [0, 1, 2];
|
||||
let updatedViewports = layout.viewports;
|
||||
|
||||
const apis = [];
|
||||
viewportIndices.forEach(viewportIndex => {
|
||||
apis[viewportIndex] = null;
|
||||
/*const currentData = layout.viewports[viewportIndex];
|
||||
if (currentData && currentData.plugin === 'vtk') {
|
||||
reject(new Error('Should not have reached this point??'));
|
||||
}*/
|
||||
|
||||
const data = {
|
||||
plugin: 'vtk',
|
||||
vtk: {
|
||||
mode: 'mpr', // TODO: not used
|
||||
afterCreation: api => {
|
||||
apis[viewportIndex] = api;
|
||||
|
||||
if (apis.every(a => !!a)) {
|
||||
resolve(apis);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updatedViewports = setSingleLayoutData(
|
||||
updatedViewports,
|
||||
viewportIndex,
|
||||
data
|
||||
);
|
||||
});
|
||||
|
||||
setLayoutAndViewportData(
|
||||
{ viewports: updatedViewports },
|
||||
viewportSpecificData
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
export default function setSingleLayoutData(
|
||||
originalArray,
|
||||
viewportIndex,
|
||||
data
|
||||
) {
|
||||
const viewports = originalArray.slice();
|
||||
const layoutData = Object.assign({}, viewports[viewportIndex], data);
|
||||
|
||||
viewports[viewportIndex] = layoutData;
|
||||
|
||||
return viewports;
|
||||
}
|
||||
39
extensions/ohif-vtk-extension/src/utils/setViewportToVTK.js
Normal file
39
extensions/ohif-vtk-extension/src/utils/setViewportToVTK.js
Normal file
@ -0,0 +1,39 @@
|
||||
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
|
||||
import setSingleLayoutData from './setSingleLayoutData.js';
|
||||
|
||||
export default function setViewportToVTK(
|
||||
displaySet,
|
||||
viewportIndex,
|
||||
layout,
|
||||
viewportSpecificData
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
/*const currentData = layout.viewports[viewportIndex];
|
||||
if (currentData && currentData.plugin === 'vtk') {
|
||||
reject(new Error('Should not have reached this point??'));
|
||||
}*/
|
||||
|
||||
const data = {
|
||||
plugin: 'vtk',
|
||||
vtk: {
|
||||
mode: 'mpr', // TODO: not used
|
||||
afterCreation: api => {
|
||||
resolve(api);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updatedViewports = setSingleLayoutData(
|
||||
layout.viewports,
|
||||
viewportIndex,
|
||||
data
|
||||
);
|
||||
|
||||
const updatedViewportData = viewportSpecificData;
|
||||
|
||||
setLayoutAndViewportData(
|
||||
{ viewports: updatedViewports },
|
||||
updatedViewportData
|
||||
);
|
||||
});
|
||||
}
|
||||
108
extensions/ohif-vtk-extension/webpack.config.js
Normal file
108
extensions/ohif-vtk-extension/webpack.config.js
Normal file
@ -0,0 +1,108 @@
|
||||
var path = require('path')
|
||||
var webpack = require('webpack')
|
||||
|
||||
const autoprefixer = require('autoprefixer');
|
||||
|
||||
const cssRules = [
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: /\.module\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
plugins: () => [autoprefixer('last 2 version', 'ie >= 10')],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.glsl$/i,
|
||||
include: /vtk\.js[\/\\]Sources/,
|
||||
loader: 'shader-loader',
|
||||
},
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
include: /vtk\.js[\/\\]Sources/,
|
||||
use: [
|
||||
{
|
||||
loader: 'worker-loader',
|
||||
options: { inline: true, fallback: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: /\.module\.css$/,
|
||||
use: [
|
||||
{ loader: 'style-loader' },
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
localIdentName: '[name]-[local]_[sha512:hash:base64:5]',
|
||||
modules: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
plugins: () => [autoprefixer('last 2 version', 'ie >= 10')],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
var entry = path.join(__dirname, './src/index.js')
|
||||
const sourcePath = path.join(__dirname, './src')
|
||||
const outputPath = path.join(__dirname, './dist')
|
||||
|
||||
module.exports = {
|
||||
entry,
|
||||
output: {
|
||||
path: outputPath,
|
||||
filename: 'index.umd.js',
|
||||
library: '@ohif/extension-vtk',
|
||||
libraryTarget: 'umd',
|
||||
globalObject: 'this',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: ['babel-loader'],
|
||||
}
|
||||
].concat(cssRules),
|
||||
},
|
||||
resolve: {
|
||||
modules: [path.resolve(__dirname, 'node_modules'), sourcePath],
|
||||
},
|
||||
externals: [{
|
||||
'cornerstone-core': {
|
||||
commonjs: 'cornerstone-core',
|
||||
commonjs2: 'cornerstone-core',
|
||||
amd: 'cornerstone-core',
|
||||
root: 'cornerstone',
|
||||
},
|
||||
'cornerstone-math': {
|
||||
commonjs: 'cornerstone-math',
|
||||
commonjs2: 'cornerstone-math',
|
||||
amd: 'cornerstone-math',
|
||||
root: 'cornerstoneMath',
|
||||
},
|
||||
},
|
||||
'ohif-core',
|
||||
'dcmjs',
|
||||
'react-viewerbase',
|
||||
'react',//: 'React',
|
||||
'react-dom',//: 'ReactDOM',
|
||||
'react-redux',//: 'ReactRedux',
|
||||
'react-resize-detector',//: 'ReactResizeDetector',
|
||||
'react-viewerbase',//: 'reactViewerbase',
|
||||
'prop-types'//: 'PropTypes'
|
||||
/*/\b(vtk.js)/*/
|
||||
],
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user