diglettk
Version:
A medical imaging toolkit, built on top of vtk.js
524 lines (412 loc) • 20.1 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8">
<title>vtk/vtkInteractorMPRSlice.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">vtk/vtkInteractorMPRSlice.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* Based on the vtk.js's MPR Slice interactor Style, but with improvements.
*/
// Temporarily using a modified version of this interactor to deal with a camera subscription issue
import macro from "@kitware/vtk.js/macro";
import vtkMath from "@kitware/vtk.js/Common/Core/Math";
import vtkMatrixBuilder from "@kitware/vtk.js/Common/Core/MatrixBuilder";
import vtkInteractorStyleManipulator from "@kitware/vtk.js/Interaction/Style/InteractorStyleManipulator";
import vtkMouseCameraTrackballRotateManipulator from "@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballRotateManipulator";
import vtkMouseCameraTrackballPanManipulator from "@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballPanManipulator";
import vtkMouseCameraTrackballZoomManipulator from "@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballZoomManipulator";
import vtkMouseRangeManipulator from "@kitware/vtk.js/Interaction/Manipulators/MouseRangeManipulator";
// ----------------------------------------------------------------------------
// Global methods
// ----------------------------------------------------------------------------
function boundsToCorners(bounds) {
return [
[bounds[0], bounds[2], bounds[4]],
[bounds[0], bounds[2], bounds[5]],
[bounds[0], bounds[3], bounds[4]],
[bounds[0], bounds[3], bounds[5]],
[bounds[1], bounds[2], bounds[4]],
[bounds[1], bounds[2], bounds[5]],
[bounds[1], bounds[3], bounds[4]],
[bounds[1], bounds[3], bounds[5]]
];
}
// ----------------------------------------------------------------------------
function clamp(value, min, max) {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
// ----------------------------------------------------------------------------
// vtkInteractorStyleMPRSlice methods
// ----------------------------------------------------------------------------
function vtkInteractorStyleMPRSlice(publicAPI, model) {
// Set our className
model.classHierarchy.push("vtkInteractorStyleMPRSlice");
model.trackballManipulator =
vtkMouseCameraTrackballRotateManipulator.newInstance({
button: 1
});
model.panManipulator = vtkMouseCameraTrackballPanManipulator.newInstance({
button: 1,
shift: true
});
model.zoomManipulator = vtkMouseCameraTrackballZoomManipulator.newInstance({
button: 3
});
model.scrollManipulator = vtkMouseRangeManipulator.newInstance({
scrollEnabled: true,
dragEnabled: false
});
// cache for sliceRange
const cache = {
sliceNormal: [0, 0, 0],
sliceRange: [0, 0],
slicePosition: [0, 0, 0]
};
function updateScrollManipulator() {
const range = publicAPI.getSliceRange();
// console.log("updating the manipulator", range);
model.scrollManipulator.removeScrollListener();
// The Scroll listener has min, max, step, and getValue setValue as params.
// Internally, it checks that the result of the GET has changed, and only calls SET if it is new.
model.scrollManipulator.setScrollListener(
range[0],
range[1],
1,
publicAPI.getSlice,
publicAPI.setSlice
);
}
function setManipulators() {
publicAPI.removeAllMouseManipulators();
publicAPI.addMouseManipulator(model.trackballManipulator);
publicAPI.addMouseManipulator(model.panManipulator);
publicAPI.addMouseManipulator(model.zoomManipulator);
publicAPI.addMouseManipulator(model.scrollManipulator);
updateScrollManipulator();
}
let cameraSub = null;
let interactorSub = null;
const superSetInteractor = publicAPI.setInteractor;
publicAPI.setInteractor = interactor => {
superSetInteractor(interactor);
if (cameraSub) {
cameraSub.unsubscribe();
cameraSub = null;
}
if (interactorSub) {
interactorSub.unsubscribe();
interactorSub = null;
}
if (interactor) {
const renderer = interactor.getCurrentRenderer();
const camera = renderer.getActiveCamera();
cameraSub = camera.onModified(() => {
updateScrollManipulator();
publicAPI.modified();
});
interactorSub = interactor.onAnimation(() => {
const { slabThickness } = model;
const dist = camera.getDistance();
const near = dist - slabThickness / 2;
const far = dist + slabThickness / 2;
camera.setClippingRange(near, far);
});
}
};
publicAPI.handleMouseMove = macro.chain(publicAPI.handleMouseMove, () => {
const renderer = model._interactor.getCurrentRenderer();
const { slabThickness } = model;
const camera = renderer.getActiveCamera();
const dist = camera.getDistance();
const near = dist - slabThickness / 2;
const far = dist + slabThickness / 2;
camera.setClippingRange(near, far);
});
const superSetVolumeMapper = publicAPI.setVolumeMapper;
publicAPI.setVolumeMapper = mapper => {
if (superSetVolumeMapper(mapper)) {
const renderer = model._interactor.getCurrentRenderer();
const camera = renderer.getActiveCamera();
if (mapper) {
// prevent zoom manipulator from messing with our focal point
// TODO: remove the zoom maninipulator instead?
camera.setFreezeFocalPoint(true);
// NOTE: Disabling this because it makes it more difficult to switch
// interactor styles. Need to find a better way to do this!
//publicAPI.setSliceNormal(...publicAPI.getSliceNormal());
} else {
camera.setFreezeFocalPoint(false);
}
}
};
publicAPI.getSlice = () => {
const renderer = model._interactor.getCurrentRenderer();
const camera = renderer.getActiveCamera();
const sliceNormal = publicAPI.getSliceNormal();
// Get rotation matrix from normal to +X (since bounds is aligned to XYZ)
const transform = vtkMatrixBuilder
.buildFromDegree()
.identity()
.rotateFromDirections(sliceNormal, [1, 0, 0]);
const fp = camera.getFocalPoint();
transform.apply(fp);
return fp[0];
};
publicAPI.setSlice = slice => {
const renderer = model._interactor.getCurrentRenderer();
const camera = renderer.getActiveCamera();
// console.log("slice", slice);
if (model.volumeMapper) {
const range = publicAPI.getSliceRange();
const bounds = model.volumeMapper.getBounds();
const clampedSlice = clamp(slice, ...range);
const center = [
(bounds[0] + bounds[1]) / 2.0,
(bounds[2] + bounds[3]) / 2.0,
(bounds[4] + bounds[5]) / 2.0
];
const distance = camera.getDistance();
const dop = camera.getDirectionOfProjection();
vtkMath.normalize(dop);
const midPoint = (range[1] + range[0]) / 2.0;
const zeroPoint = [
center[0] - dop[0] * midPoint,
center[1] - dop[1] * midPoint,
center[2] - dop[2] * midPoint
];
const slicePoint = [
zeroPoint[0] + dop[0] * clampedSlice,
zeroPoint[1] + dop[1] * clampedSlice,
zeroPoint[2] + dop[2] * clampedSlice
];
const cameraPos = [
slicePoint[0] - dop[0] * distance,
slicePoint[1] - dop[1] * distance,
slicePoint[2] - dop[2] * distance
];
camera.setPosition(...cameraPos);
camera.setFocalPoint(...slicePoint);
// run Callback
const onScroll = publicAPI.getOnScroll();
if (onScroll) onScroll(slicePoint);
}
};
publicAPI.getSliceRange = () => {
if (model.volumeMapper) {
const sliceNormal = publicAPI.getSliceNormal();
if (
sliceNormal[0] === cache.sliceNormal[0] &&
sliceNormal[1] === cache.sliceNormal[1] &&
sliceNormal[2] === cache.sliceNormal[2]
) {
return cache.sliceRange;
}
const bounds = model.volumeMapper.getBounds();
const points = boundsToCorners(bounds);
// Get rotation matrix from normal to +X (since bounds is aligned to XYZ)
const transform = vtkMatrixBuilder
.buildFromDegree()
.identity()
.rotateFromDirections(sliceNormal, [1, 0, 0]);
points.forEach(pt => transform.apply(pt));
// range is now maximum X distance
let minX = Infinity;
let maxX = -Infinity;
for (let i = 0; i < 8; i++) {
const x = points[i][0];
if (x > maxX) {
maxX = x;
}
if (x < minX) {
minX = x;
}
}
cache.sliceNormal = sliceNormal;
cache.sliceRange = [minX, maxX];
return cache.sliceRange;
}
return [0, 0];
};
// Slice normal is just camera DOP
publicAPI.getSliceNormal = () => {
if (model.volumeMapper && model._interactor) {
const renderer = model._interactor.getCurrentRenderer();
const camera = renderer.getActiveCamera();
return camera.getDirectionOfProjection();
}
return [0, 0, 0];
};
// Thought this was a good idea, but no.
// publicAPI.getSliceNormal = () => cache.sliceNormal;
/**
* Move the camera to the given slice normal and viewup direction. Viewup can be used to rotate the display of the image around the direction of view.
*
* TODO: setting the slice ALWAYS resets to the volume center, but we need to be able to rotate from an arbitrary position, AKA the intersection of all 3 slice planes.
*/
// in world space
publicAPI.setSliceNormal = (normal, viewUp = [0, 1, 0]) => {
const renderer = model._interactor.getCurrentRenderer();
const camera = renderer.getActiveCamera();
// Copy arguments to the model, so they can be GET-ed later
model.sliceNormal = [...normal];
model.viewUp = [...viewUp];
//copy arguments for internal editing so we don't cause sideeffects
const _normal = [...normal];
const _viewUp = [...viewUp];
if (model.volumeMapper) {
vtkMath.normalize(_normal);
let mapper = model.volumeMapper;
// get the mapper if the model is actually the actor, not the mapper
if (!model.volumeMapper.getInputData && model.volumeMapper.getMapper) {
mapper = model.volumeMapper.getMapper();
}
let volumeCoordinateSpace = vec9toMat3(
mapper.getInputData().getDirection()
);
// Transpose the volume's coordinate space to create a transformation matrix
vtkMath.transpose3x3(volumeCoordinateSpace, volumeCoordinateSpace);
// Convert the provided normal into the volume's space
vtkMath.multiply3x3_vect3(volumeCoordinateSpace, _normal, _normal);
let center = camera.getFocalPoint();
let dist = camera.getDistance();
let angle = camera.getViewAngle();
if (Number.isNaN(dist) || dist === undefined) {
// Default the volume center
const bounds = model.volumeMapper.getBounds();
// diagonal will be used as "width" of camera scene
const diagonal = Math.sqrt(
vtkMath.distance2BetweenPoints(
[bounds[0], bounds[2], bounds[4]],
[bounds[1], bounds[3], bounds[5]]
)
);
// center will be used as initial focal point
center = [
(bounds[0] + bounds[1]) / 2.0,
(bounds[2] + bounds[3]) / 2.0,
(bounds[4] + bounds[5]) / 2.0
];
angle = 90;
// distance from camera to focal point
dist = diagonal / (2 * Math.tan((angle / 360) * Math.PI));
}
const cameraPos = [
center[0] - _normal[0] * dist,
center[1] - _normal[1] * dist,
center[2] - _normal[2] * dist
];
// set viewUp based on DOP rotation
// const oldDop = camera.getDirectionOfProjection();
// const transform = vtkMatrixBuilder
// .buildFromDegree()
// .identity()
// .rotateFromDirections(oldDop, normal);
// const viewUp = [0, 1, 0];
// transform.apply(viewUp);
vtkMath.multiply3x3_vect3(volumeCoordinateSpace, _viewUp, _viewUp);
const { slabThickness } = model;
camera.setPosition(...cameraPos);
camera.setDistance(dist);
// should be set after pos and distance
camera.setDirectionOfProjection(..._normal);
camera.setViewUp(..._viewUp);
camera.setViewAngle(angle);
camera.setClippingRange(
dist - slabThickness / 2,
dist + slabThickness / 2
);
publicAPI.setCenterOfRotation(center);
}
};
publicAPI.setSlabThickness = slabThickness => {
model.slabThickness = slabThickness;
// Update the camera clipping range if the slab
// thickness property is changed
const renderer = model._interactor.getCurrentRenderer();
const camera = renderer.getActiveCamera();
const dist = camera.getDistance();
camera.setClippingRange(dist - slabThickness / 2, dist + slabThickness / 2);
};
setManipulators();
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
slabThickness: 0.1
};
// ----------------------------------------------------------------------------
export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Inheritance
vtkInteractorStyleManipulator.extend(publicAPI, model, initialValues);
macro.setGet(publicAPI, model, ["volumeMapper", "onScroll"]);
macro.get(publicAPI, model, ["slabThickness", "viewUp"]);
// Object specific methods
vtkInteractorStyleMPRSlice(publicAPI, model);
}
// ----------------------------------------------------------------------------
export const newInstance = macro.newInstance(
extend,
"vtkInteractorStyleMPRSlice"
);
// ----------------------------------------------------------------------------
export default Object.assign({ newInstance, extend });
// TODO: work with VTK to change the internal formatting of arrays.
function vec9toMat3(vec9) {
if (vec9.length !== 9) {
throw Error("Array not length 9");
}
//prettier-ignore
return [
[vec9[0], vec9[1], vec9[2]],
[vec9[3], vec9[4], vec9[5]],
[vec9[6], vec9[7], vec9[8]],
];
}
</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>