diglettk
Version:
A medical imaging toolkit, built on top of vtk.js
477 lines (384 loc) • 17.5 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>mprView.js - DigleTTK</title>
<meta name="keywords" content="medical, imaging, dicom, webgl" />
<meta name="keyword" content="medical, imaging, dicom, webgl" />
<script src="scripts/prettify/prettify.js"></script>
<script src="scripts/prettify/lang-css.js"></script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
<script src="scripts/nav.js" defer></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
<label for="nav-trigger" class="navicon-button x">
<div class="navicon"></div>
</label>
<label for="nav-trigger" class="overlay"></label>
<nav >
<input type="text" id="nav-search" placeholder="Search" />
<h2><a href="index.html">Home</a></h2><h2><a href="https://github.com/dvisionlab/DigletTK" target="_blank" class="menu-item" id="repository" >Github repo</a></h2><h3>Classes</h3><ul><li><a href="baseView.html">baseView</a></li><li><a href="MPRManager.html">MPRManager</a><ul class='methods'><li data-type='method' style='display: none;'><a href="MPRManager.html#destroy">destroy</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#getInitialState">getInitialState</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#onRotate">onRotate</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#onThickness">onThickness</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#resize">resize</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#setImage">setImage</a></li><li data-type='method' style='display: none;'><a href="MPRManager.html#setTool">setTool</a></li></ul></li><li><a href="VRView.html">VRView</a><ul class='methods'><li data-type='method' style='display: none;'><a href="VRView.html#_initCropWidget">_initCropWidget</a></li><li data-type='method' style='display: none;'><a href="VRView.html#_initPicker">_initPicker</a></li><li data-type='method' style='display: none;'><a href="VRView.html#addLandmarks">addLandmarks</a></li><li data-type='method' style='display: none;'><a href="VRView.html#addSurface">addSurface</a></li><li data-type='method' style='display: none;'><a href="VRView.html#destroy">destroy</a></li><li data-type='method' style='display: none;'><a href="VRView.html#getLutList">getLutList</a></li><li data-type='method' style='display: none;'><a href="VRView.html#resetMeasurementState">resetMeasurementState</a></li><li data-type='method' style='display: none;'><a href="VRView.html#resetView">resetView</a></li><li data-type='method' style='display: none;'><a href="VRView.html#resize">resize</a></li><li data-type='method' style='display: none;'><a href="VRView.html#setImage">setImage</a></li><li data-type='method' style='display: none;'><a href="VRView.html#setSurfaceVisibility">setSurfaceVisibility</a></li><li data-type='method' style='display: none;'><a href="VRView.html#setTool">setTool</a></li><li data-type='method' style='display: none;'><a href="VRView.html#turnPickingOff">turnPickingOff</a></li><li data-type='method' style='display: none;'><a href="VRView.html#turnPickingOn">turnPickingOn</a></li><li data-type='method' style='display: none;'><a href="VRView.html#updateLandmarkPosition">updateLandmarkPosition</a></li><li data-type='method' style='display: none;'><a href="VRView.html#updateSurface">updateSurface</a></li></ul></li></ul><h3>Global</h3><ul><li><a href="global.html#addSphereInPoint">addSphereInPoint</a></li><li><a href="global.html#applyAngleStrategy">applyAngleStrategy</a></li><li><a href="global.html#applyLengthStrategy">applyLengthStrategy</a></li><li><a href="global.html#buildVtkVolume">buildVtkVolume</a></li><li><a href="global.html#createRGBStringFromRGBValues">createRGBStringFromRGBValues</a></li><li><a href="global.html#createSurfaceActor">createSurfaceActor</a></li><li><a href="global.html#createVolumeActor">createVolumeActor</a></li><li><a href="global.html#degrees2radians">degrees2radians</a></li><li><a href="global.html#fitToWindow">fitToWindow</a></li><li><a href="global.html#getAbsoluteRange">getAbsoluteRange</a></li><li><a href="global.html#getCroppingPlanes">getCroppingPlanes</a></li><li><a href="global.html#getRelativeRange">getRelativeRange</a></li><li><a href="global.html#getVideoCardInfo">getVideoCardInfo</a></li><li><a href="global.html#getVOI">getVOI</a></li><li><a href="global.html#getVolumeCenter">getVolumeCenter</a></li><li><a href="global.html#larvitarInitialized">larvitarInitialized</a></li><li><a href="global.html#setActorProperties">setActorProperties</a></li><li><a href="global.html#setCamera">setCamera</a></li><li><a href="global.html#setupCropWidget">setupCropWidget</a></li><li><a href="global.html#setupPickingPlane">setupPickingPlane</a></li><li><a href="global.html#State">State</a></li></ul>
</nav>
<div id="main">
<h1 class="page-title">mprView.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>import vtkGenericRenderWindow from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
import vtkInteractorStyleMPRSlice from "./vtk/vtkInteractorMPRSlice";
import { quat, vec3, mat4 } from "gl-matrix";
import { degrees2radians } from "./utils/utils";
import { baseView } from "./baseView";
/**
* MPRView class
* This is not intended to be used directly by user
* Use MPRManager instead: it will create three instances of MPRView
* @private
*
*/
// TODO move to constants (calculate from image directions?)
const PLANE_NORMALS = [
[0, 0, 1],
[-1, 0, 0],
[0, 1, 0]
];
const VIEW_UPS = [
[0, -1, 0],
[0, 0, 1],
[0, 0, 1]
];
export class MPRView extends baseView {
constructor(key, i, element) {
super();
this.VERBOSE = false;
this._key = key;
this._element = element;
this._volume = null;
this._renderer = null;
this._parallel = true; // TODO setter
// init global data
this.slicePlaneNormal = PLANE_NORMALS[i];
this.sliceViewUp = VIEW_UPS[i];
this.slicePlaneXRotation = 0;
this.slicePlaneYRotation = 0;
this.viewRotation = 0;
this._sliceThickness = 0.1;
this._blendMode = "MIP";
this.window = {
width: 0,
center: 0
};
// cache the view vectors so we can apply the rotations without modifying the original value
this._cachedSlicePlane = [...this.slicePlaneNormal];
this._cachedSliceViewUp = [...this.sliceViewUp];
this._genericRenderWindow = vtkGenericRenderWindow.newInstance({
background: [0, 0, 0]
});
this._genericRenderWindow.setContainer(element);
this._renderWindow = this._genericRenderWindow.getRenderWindow();
this._renderer = this._genericRenderWindow.getRenderer();
if (this._parallel) {
this._renderer.getActiveCamera().setParallelProjection(true);
}
// update view node tree so that vtkOpenGLHardwareSelector can access the vtkOpenGLRenderer instance.
const oglrw = this._genericRenderWindow.getOpenGLRenderWindow();
oglrw.buildPass(true);
/*
// Use for maintaining clipping range for MIP (TODO)
const interactor = this._renderWindow.getInteractor();
//const clippingRange = renderer.getActiveCamera().getClippingRange();
interactor.onAnimation(() => {
renderer.getActiveCamera().setClippingRange(...r);
});
*/
// force the initial draw to set the canvas to the parent bounds.
this.onResize();
}
/**
* blendMode - "MIP", "MinIP", "Average"
* @type {String}
*/
set blendMode(blendMode) {
this._blendMode = blendMode;
this.updateBlendMode(this._sliceThickness, this._blendMode);
}
/**
* sliceThickness
* @type {Number}
*/
set sliceThickness(thickness) {
this._sliceThickness = thickness;
const istyle = this._renderWindow.getInteractor().getInteractorStyle();
// set thickness if the current interactor has it (it should, but just in case)
istyle.setSlabThickness && istyle.setSlabThickness(this._sliceThickness);
this.updateBlendMode(this._sliceThickness, this._blendMode);
}
/**
* wwwl
* @type {Array}
*/
set wwwl([ww, wl]) {
this.window.center = wl;
this.window.width = ww;
this._genericRenderWindow.getRenderWindow().render();
}
/**
* camera
* @type {vtkCamera}
*/
get camera() {
return this._genericRenderWindow.getRenderer().getActiveCamera();
}
/**
* Initialize view: add actor to scene and setup controls & props
* @param {vtkActor} actor
* @param {State} data
* @param {Function} onScrollCb
*/
initView(actor, data, onScrollCb, onInitialized) {
// dv: store volumes and element in viewport data
this._volume = actor;
const istyle = vtkInteractorStyleMPRSlice.newInstance();
istyle.setOnScroll(onScrollCb);
const inter = this._renderWindow.getInteractor();
inter.setInteractorStyle(istyle);
// TODO: assumes the volume is always set for this mounted state...Throw an error?
if (this.VERBOSE) console.log(this._volumes);
const istyleVolumeMapper = this._volume.getMapper();
istyle.setVolumeMapper(istyleVolumeMapper);
//start with the volume center slice
const range = istyle.getSliceRange();
// if (this.VERBOSE) console.log('view mounted: setting the initial range', range)
istyle.setSlice((range[0] + range[1]) / 2);
// add the current volumes to the vtk renderer
this.updateVolumesForRendering();
if (this.VERBOSE) console.log("view data", this._key, data.views[this.key]);
this.updateSlicePlane(data.views[this._key]);
// set camera to fill viewport
this.fill2DView(this._genericRenderWindow, this._key);
onInitialized();
}
/**
* cleanup the scene and add new volume
* @private
*/
updateVolumesForRendering() {
this._renderer.removeAllVolumes();
if (this._volume) {
if (!this._volume.isA("vtkVolume")) {
console.warn("Data to <Vtk2D> is not vtkVolume data");
} else {
this._renderer.addVolume(this._volume);
}
}
this._renderWindow.render();
}
/**
* Recompute slice plane after changes
* @param {State} viewData
*/
updateSlicePlane(viewData) {
// cached things are in viewport data
let cachedSlicePlane = this._cachedSlicePlane;
let cachedSliceViewUp = this._cachedSliceViewUp;
if (this.VERBOSE) console.log(viewData);
// TODO: optimize so you don't have to calculate EVERYTHING every time?
// rotate around the vector of the cross product of the plane and viewup as the X component
let sliceXRotVector = [];
vec3.cross(
sliceXRotVector,
viewData.sliceViewUp,
viewData.slicePlaneNormal
);
vec3.normalize(sliceXRotVector, sliceXRotVector);
// rotate the viewUp vector as the Y component
let sliceYRotVector = viewData.sliceViewUp;
const planeMat = mat4.create();
mat4.rotate(
planeMat,
planeMat,
degrees2radians(viewData.slicePlaneYRotation),
sliceYRotVector
);
mat4.rotate(
planeMat,
planeMat,
degrees2radians(viewData.slicePlaneXRotation),
sliceXRotVector
);
if (this.VERBOSE)
console.log(cachedSlicePlane, viewData.slicePlaneNormal, planeMat);
vec3.transformMat4(cachedSlicePlane, viewData.slicePlaneNormal, planeMat);
// Rotate the viewUp in 90 degree increments
const viewRotQuat = quat.create();
// Use - degrees since the axis of rotation should really be the direction of projection, which is the negative of the plane normal
quat.setAxisAngle(
viewRotQuat,
cachedSlicePlane,
degrees2radians(-viewData.viewRotation)
);
quat.normalize(viewRotQuat, viewRotQuat);
// rotate the ViewUp with the x and z rotations
const xQuat = quat.create();
quat.setAxisAngle(
xQuat,
sliceXRotVector,
degrees2radians(viewData.slicePlaneXRotation)
);
quat.normalize(xQuat, xQuat);
const viewUpQuat = quat.create();
quat.add(viewUpQuat, xQuat, viewRotQuat);
vec3.transformQuat(cachedSliceViewUp, viewData.sliceViewUp, viewRotQuat);
// update the view's slice
const renderWindow = this._genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
if (istyle && istyle.setSliceNormal) {
istyle.setSliceNormal(cachedSlicePlane, cachedSliceViewUp);
}
renderWindow.render();
}
// fit to window (vtk.js 11 version: https://github.com/Kitware/paraview-glance/issues/230)
fill2DView() {
// Based this code: https://github.com/Kitware/paraview-glance/issues/230#issuecomment-445779222
const bounds = this._renderer.computeVisiblePropBounds();
const dim = [
(bounds[1] - bounds[0]) / 2,
(bounds[3] - bounds[2]) / 2,
(bounds[5] - bounds[4]) / 2
];
const w = this._genericRenderWindow.getContainer().clientWidth;
const h = this._genericRenderWindow.getContainer().clientHeight;
const r = w / h;
let x;
let y;
if (this._key === "left") {
x = dim[1];
y = dim[2];
} else if (this._key === "front") {
x = dim[0];
y = dim[2];
} else if (this._key === "top") {
x = dim[0];
y = dim[1];
}
if (r >= x / y) {
// use width
this._renderer.getActiveCamera().setParallelScale(y + 1);
} else {
// use height
this._renderer.getActiveCamera().setParallelScale(x / r + 1);
}
this.onResize();
}
/**
* on resize callback
* @private
*/
onResize() {
// TODO: debounce for performance reasons?
this._genericRenderWindow.resize();
}
/**
* update blending after changes
* @private
* @param {Number} thickness
* @param {String} blendMode
*/
updateBlendMode(thickness, blendMode) {
if (thickness >= 1) {
switch (blendMode) {
case "MIP":
this._volume.getMapper().setBlendModeToMaximumIntensity();
break;
case "MINIP":
this._volume.getMapper().setBlendModeToMinimumIntensity();
break;
case "AVG":
this._volume.getMapper().setBlendModeToAverageIntensity();
break;
case "none":
default:
this._volume.getMapper().setBlendModeToComposite();
break;
}
} else {
this._volume.getMapper().setBlendModeToComposite();
}
this._renderWindow.render();
}
/**
* Setup interactor
* @param {vtkInteractorStyle} istyle
*/
setInteractor(istyle) {
const renderWindow = this._genericRenderWindow.getRenderWindow();
// We are assuming the old style is always extended from the MPRSlice style
const oldStyle = renderWindow.getInteractor().getInteractorStyle();
renderWindow.getInteractor().setInteractorStyle(istyle);
istyle.setInteractor(renderWindow.getInteractor());
// Make sure to set the style to the interactor itself, because reasons...?!
const inter = renderWindow.getInteractor();
inter.setInteractorStyle(istyle);
// Copy previous interactors styles into the new one.
if (istyle.setSliceNormal && oldStyle.getSliceNormal()) {
// if (VERBOSE) console.log("setting slicenormal from old normal");
istyle.setSliceNormal(oldStyle.getSliceNormal(), oldStyle.getViewUp());
}
if (istyle.setSlabThickness && oldStyle.getSlabThickness()) {
istyle.setSlabThickness(oldStyle.getSlabThickness());
}
istyle.setVolumeMapper(this._volume);
// set current slice (fake) to make distance widget working
// istyle.setCurrentImageNumber(0);
}
/**
* Destroy webgl content and release listeners
*/
destroy() {
if (this.VERBOSE) console.log("DESTROY", this._key);
this.VERBOSE = null;
this._key = null;
this._element = null;
// mapper is in common btw views, check that it has not already been deleted by other view
if (this._volume.getMapper()) {
this._volume.getMapper().delete();
}
this._volume.delete();
this._volume = null;
this._renderer.delete();
this._renderer = null;
this._parallel = null;
this.slicePlaneNormal = null;
this.sliceViewUp = null;
this.slicePlaneXRotation = null;
this.slicePlaneYRotation = null;
this.viewRotation = null;
this._sliceThickness = null;
this._blendMode = null;
this.window = null;
this._cachedSlicePlane = null;
this._cachedSliceViewUp = null;
this._genericRenderWindow.delete();
// delete resize listener ?
}
}
</code></pre>
</article>
</section>
</div>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.6</a> using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
</footer>
<script>prettyPrint();</script>
<script src="scripts/polyfill.js"></script>
<script src="scripts/linenumber.js"></script>
<script src="scripts/search.js" defer></script>
<script src="scripts/collapse.js" defer></script>
</body>
</html>