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 ?
}
}