import vtkGenericRenderWindow from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
import vtkColorTransferFunction from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction";
import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction";
import vtkColorMaps from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps";
import vtkSTLReader from "@kitware/vtk.js/IO/Geometry/STLReader";
import vtkMapper from "@kitware/vtk.js/Rendering/Core/Mapper";
import vtkActor from "@kitware/vtk.js/Rendering/Core/Actor";
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";
import vtkInteractorStyleManipulator from "@kitware/vtk.js/Interaction/Style/InteractorStyleManipulator";
import vtkPointPicker from "@kitware/vtk.js/Rendering/Core/PointPicker";
import vtkCoordinate from "@kitware/vtk.js/Rendering/Core/Coordinate";
import {
createVolumeActor,
setupPGwidget,
setCamera,
setActorProperties,
setupCropWidget,
setupPickingPlane,
getRelativeRange
} from "./utils/utils";
import { applyStrategy } from "./utils/strategies";
import { createPreset } from "./utils/colormaps";
import { getRenderPass } from "./renderPasses";
import { baseView } from "./baseView";
// Add custom presets
vtkColorMaps.addPreset(createPreset());
/** A class representing a Volume Rendering scene */
export class VRView extends baseView {
/**
* Create a volume rendering scene
* @param {HTMLElement} element - the target html element to render the scene
*/
constructor(element) {
super();
this.VERBOSE = false;
this._element = element;
this._renderer = null;
this._renderWindow = null;
this._genericRenderWindow = null;
this._actor = null;
this._raysDistance = null;
this._blurOnInteraction = null;
// piecewise gaussian widget stuff
this._PGwidgetElement = null;
this._PGwidget = null;
this._gaussians = null;
this._PGwidgetLoaded = false;
// crop widget
this._cropWidget = null;
// normalized ww wl
this._ww = 0.1;
this._wl = 0.4;
// absolute ww wl
this._wwwl = [0, 0];
// LUT options
this._rangeLUT = null;
this._rescaleLUT = false; // cannot initialize true (must set lut before)
// rendering passes
this._edgeEnhancement = false;
// measurement state
this._measurementState = null;
// surfaces
this._surfaces = new Map();
// initialize empty scene
this._init();
}
// ===========================================================
// ====== setters & getters ==================================
// ===========================================================
/**
* wwwl
* @type {Array}
*/
set wwwl(value) {
if (!this._actor) {
return;
}
let relativeWwwl = getRelativeRange(this._actor, value);
this._wl = relativeWwwl.wl;
this._ww = relativeWwwl.ww;
if (this._PGwidget) {
this._updateWidget();
this._setWidgetCallbacks();
}
}
get wwwl() {
let absoluteWwwl = getAbsoluteRange(this._actor, [this._ww, this._wl]);
return [absoluteWwwl.ww, absoluteWwwl.wl];
}
/**
* raysDistance
* @type {Number}
*/
set resolution(value) {
this._raysDistance = 1 / value;
this._actor.getMapper().setSampleDistance(this._raysDistance);
let maxSamples = value > 1 ? value * 1000 : 1000;
this._actor.getMapper().setMaximumSamplesPerRay(maxSamples);
this._renderWindow.render();
}
get resolution() {
return Math.round(1 / this._raysDistance);
}
/**
* Presets
* @type {Array}
*/
get presetsList() {
return vtkColorMaps.rgbPresetNames;
}
/**
* PGwidgetElement (set null to hide)
* @type {HTMLelement}
*/
set widgetElement(element) {
// initalize piecewise gaussian widget
this._PGwidgetElement = element;
if (!this._PGwidget) {
this._PGwidget = setupPGwidget(this._PGwidgetElement);
}
let h = element.offsetHeight ? element.offsetHeight - 5 : 100;
let w = element.offsetWidth ? element.offsetWidth - 5 : 300;
this._PGwidget.setSize(w, h);
this._PGwidget.setContainer(this._PGwidgetElement);
this._PGwidget.render();
}
/**
* Flag to set lut rescaling on opacity range
* @type {bool}
*/
set rescaleLUT(bool) {
this._rescaleLUT = bool;
let range;
if (this._rescaleLUT && this._PGwidget) {
range = this._PGwidget.getOpacityRange();
} else {
range = this._actor
.getMapper()
.getInputData()
.getPointData()
.getScalars()
.getRange();
}
this.ctfun.setMappingRange(...range);
this.ctfun.updateRange();
}
/**
* Set range to apply lut !!! WIP
* @type {Array}
*/
set rangeLUT([min, max]) {
this._rangeLUT = [min, max];
this._actor
.getProperty()
.getRGBTransferFunction(0)
.setMappingRange(min, max);
}
/**
* Crop widget on / off
* @type {bool}
*/
set cropWidget(visible) {
if (!this._cropWidget) this._initCropWidget();
this._cropWidget.setVisibility(visible);
this._widgetManager.renderWidgets();
this._renderWindow.render();
}
/**
* Set colormap and opacity function
* lutName - as in presets list
* @type {String}
*/
set lut(lutName) {
// set up color transfer function
const lookupTable = vtkColorTransferFunction.newInstance();
lookupTable.applyColorMap(vtkColorMaps.getPresetByName(lutName));
// update lookup table mapping range based on input dataset
let range;
if (this._rescaleLUT && this._PGwidgetLoaded) {
range = this._PGwidget.getOpacityRange();
} else {
range = this._actor
.getMapper()
.getInputData()
.getPointData()
.getScalars()
.getRange();
}
// TODO a function to set custom mapping range (unbind from opacity)
lookupTable.setMappingRange(...range);
lookupTable.updateRange();
this._actor.getProperty().setRGBTransferFunction(0, lookupTable);
// setup opacity function (values will be set by PGwidget)
const piecewiseFun = vtkPiecewiseFunction.newInstance();
this._actor.getProperty().setScalarOpacity(0, piecewiseFun);
this.ctfun = lookupTable;
this.ofun = piecewiseFun;
if (this._PGwidget) {
this._updateWidget();
this._setWidgetCallbacks();
}
}
/**
* Toggle blurring on interaction (Increase performance)
* @type {bool} toggle - if true, blur on interaction
*/
set blurOnInteraction(toggle) {
this._blurOnInteraction = toggle;
let interactor = this._renderWindow.getInteractor();
let mapper = this._actor.getMapper();
if (toggle) {
interactor.onLeftButtonPress(() => {
mapper.setSampleDistance(this._raysDistance * 5);
});
interactor.onLeftButtonRelease(() => {
mapper.setSampleDistance(this._raysDistance);
// update picking plane
let camera = this._renderer.getActiveCamera();
if (this._pickingPlane)
this._pickingPlane.setNormal(camera.getDirectionOfProjection());
this._renderWindow.render();
});
} else {
interactor.onLeftButtonPress(() => {
mapper.setSampleDistance(this._raysDistance);
});
interactor.onLeftButtonRelease(() => {
mapper.setSampleDistance(this._raysDistance);
// update picking plane
let camera = this._renderer.getActiveCamera();
if (this._pickingPlane)
this._pickingPlane.setNormal(camera.getDirectionOfProjection());
this._renderWindow.render();
});
}
}
/**
* Toggle edge enhancement
*/
set edgeEnhancement([type, value]) {
let renderPass = getRenderPass(type, value);
let view = this._renderWindow.getViews()[0];
view.setRenderPasses([renderPass]);
this._renderWindow.render();
}
// ===========================================================
// ====== public methods =====================================
// ===========================================================
/**
* Set the image to be rendered
* @param {ArrayBuffer} image - The image content data as buffer array
*/
setImage(image) {
// clean scene
this._renderer.removeAllVolumes();
this._actor = createVolumeActor(image);
this.lut = "Grayscale";
this.resolution = 2;
this._renderer.addVolume(this._actor);
// center camera on new volume
this._renderer.resetCamera();
setCamera(this._renderer.getActiveCamera(), this._actor.getCenter());
if (this._PGwidget) {
this._updateWidget();
this._setWidgetCallbacks();
}
// TODO if crop widget, update to new image (or set to null so that it will be initialized again)
// TODO implement a strategy to set rays distance
setActorProperties(this._actor);
this._setupInteractor();
this.blurOnInteraction = true;
this._genericRenderWindow.resize();
this._renderWindow.render();
}
// This is another method, providing the url
// reader.setUrl("./demo/die.stl").then(data => {
// console.log("read", data);
// const mapper = vtkMapper.newInstance({ scalarVisibility: false });
// ... etc ...
// });
/**
* Add surfaces to be rendered
* @param {Object} - {buffer: bufferarray, color: [r,g,b], label: string}
*/
addSurface({ buffer, color, label }) {
if (this._surfaces.has(label)) {
console.warn(
`DTK: A surface with label ${label} is already present. I will ignore this.`
);
return;
}
const reader = vtkSTLReader.newInstance();
reader.parseAsArrayBuffer(buffer);
const mapper = vtkMapper.newInstance({ scalarVisibility: false });
const actor = vtkActor.newInstance();
actor.setMapper(mapper);
mapper.setInputConnection(reader.getOutputPort());
const props = actor.getProperty();
props.setColor(...color);
// props.setOpacity(0.5);
// props.setDiffuse(1)
// props.setRepresentationToWireframe()
this._surfaces.set(label, actor);
this._renderer.addActor(actor);
this._renderer.resetCamera();
this._renderWindow.render();
}
/**
* Toggle surface visibility on/off
* @param {String} label - The string that identifies the surface
* @param {Boolean} toggle
*/
setSurfaceVisibility(label, toggle) {
this._surfaces.get(label).setVisibility(toggle);
this._renderWindow.render();
}
/**
* Update surface buffer
* TODO maybe there is a more efficient way
* @param {String} label - The string that identifies the surface
* @param {ArrayBuffer} buffer
*/
updateSurface(label, buffer) {
const reader = vtkSTLReader.newInstance();
reader.parseAsArrayBuffer(buffer);
const mapper = vtkMapper.newInstance({ scalarVisibility: false });
mapper.setInputConnection(reader.getOutputPort());
const actor = this._surfaces.get(label);
actor.setMapper(mapper);
this._renderer.resetCamera();
this._renderWindow.render();
}
/**
* Get vtk LUTs list
* @returns {Array} - Lut list as array of strings
*/
getLutList() {
return vtkColorMaps.rgbPresetNames;
}
/**
* Reset measurement state to default
* @param {*} measurementState
*/
resetMeasurementState(state) {
if (this._measurementState) {
this._measurementState.p1 = new Array(2);
this._measurementState.p2 = new Array(2);
this._measurementState.p3 = new Array(2);
this._measurementState.p1_world = new Array(2);
this._measurementState.p2_world = new Array(2);
this._measurementState.p3_world = new Array(2);
this._measurementState.label = null;
} else if (state) {
state.p1 = new Array(2);
state.p2 = new Array(2);
state.p3 = new Array(2);
state.p1_world = new Array(2);
state.p2_world = new Array(2);
state.p3_world = new Array(2);
state.label = null;
}
}
/**
* Set active tool
* ("Length/Angle", {mouseButtonMask:1}, measurementState)
* @param {*} toolName
* @param {*} options
* @param {*} measurementState
*/
setTool(toolName, options, measurementState) {
if (this._leftButtonCb) {
this._leftButtonCb.unsubscribe();
}
switch (toolName) {
case "Length":
this._initPicker(measurementState, toolName);
break;
case "Angle":
this._initPicker(measurementState, toolName);
break;
case "Rotation":
this.resetMeasurementState(measurementState);
this._setupInteractor();
break;
default:
console.warn("No tool found for", toolName);
}
}
/**
* Reset view
*/
resetView() {
let center = this._actor.getCenter();
let camera = this._renderer.getActiveCamera();
setCamera(camera, center);
this._renderWindow.render();
}
/**
* on resize callback
*/
resize() {
// TODO: debounce for performance reasons?
this._genericRenderWindow.resize();
}
/**
* Destroy webgl content and release listeners
*/
destroy() {
this._element = null;
this._genericRenderWindow.delete();
this._genericRenderWindow = null;
if (this._actor) {
this._actor.getMapper().delete();
this._actor.delete();
this._actor = null;
}
if (this._planeActor) {
this._planeActor.getMapper().delete();
this._planeActor.delete();
this._planeActor = null;
}
if (this._PGwidgetElement) {
this._PGwidgetElement = null;
this._PGwidget.getCanvas().remove();
this._PGwidget.delete();
this._PGwidget = null;
this._gaussians = null;
}
if (this._cropWidget) {
this._cropWidget.delete();
this._cropWidget = null;
}
}
// ===========================================================
// ====== private methods ====================================
// ===========================================================
/**
* Initialize rendering scene
* @private
*/
_init() {
const genericRenderWindow = vtkGenericRenderWindow.newInstance();
genericRenderWindow.setContainer(this._element);
genericRenderWindow.setBackground([0, 0, 0]);
//add custom resize cb
genericRenderWindow.onResize(() => {
// bypass genericRenderWindow resize method (do not consider devicePixelRatio)
// https://kitware.github.io/vtk-js/api/Rendering_Misc_GenericRenderWindow.html
let size = [
genericRenderWindow.getContainer().getBoundingClientRect().width,
genericRenderWindow.getContainer().getBoundingClientRect().height
];
genericRenderWindow.getRenderWindow().getViews()[0].setSize(size);
if (this.VERBOSE) console.log("resize", size);
});
// resize callback
window.addEventListener("resize", evt => {
genericRenderWindow.resize();
});
genericRenderWindow.resize();
this._renderer = genericRenderWindow.getRenderer();
this._renderWindow = genericRenderWindow.getRenderWindow();
this._genericRenderWindow = genericRenderWindow;
}
/**
* Update the PGwidget after an image has been loaded
* @private
*/
_updateWidget() {
const dataArray = this._actor
.getMapper()
.getInputData()
.getPointData()
.getScalars();
this._PGwidget.setDataArray(dataArray.getData());
let gaussians = this._PGwidget.getGaussians();
if (gaussians.length > 0) {
let gaussian = gaussians[0];
gaussian.position = this._wl;
gaussian.width = this._ww;
this._PGwidget.setGaussians([gaussian]);
} else {
// TODO initilize in a smarter way
const default_opacity = 1.0;
const default_bias = 0.0; // xBias
const default_skew = 1.8; // yBias
this._PGwidget.addGaussian(
this._wl,
default_opacity,
this._ww,
default_bias,
default_skew
); // x, y, ampiezza, sbilanciamento, andamento
}
this._PGwidget.applyOpacity(this.ofun);
this._PGwidget.setColorTransferFunction(this.ctfun);
this.ctfun.onModified(() => {
this._PGwidget.render();
this._renderWindow.render();
});
this._PGwidgetLoaded = true;
}
/**
* Binds callbacks to user interactions on PGwidget
* @private
*/
_setWidgetCallbacks() {
this._PGwidget.bindMouseListeners();
this._PGwidget.onAnimation(start => {
if (start) {
this._renderWindow.getInteractor().requestAnimation(this._PGwidget);
} else {
this._renderWindow.getInteractor().cancelAnimation(this._PGwidget);
}
});
this._PGwidget.onOpacityChange(widget => {
this._PGwidget = widget;
this._gaussians = widget.getGaussians().slice(); // store
this._PGwidget.applyOpacity(this.ofun);
if (!this._renderWindow.getInteractor().isAnimating()) {
this._renderWindow.render();
}
if (this._rescaleLUT && this._PGwidget) {
const range = this._PGwidget.getOpacityRange();
this.ctfun.setMappingRange(...range);
this.ctfun.updateRange();
}
});
}
/**
* Setup crop widget
*/
_initCropWidget() {
let cropWidget = setupCropWidget(this._renderer, this._actor.getMapper());
this._widgetManager = cropWidget.widgetManager;
this._cropWidget = cropWidget.widget;
this._renderWindow.render();
}
/**
* Init interactor
* @private
*/
_setupInteractor() {
// TODO setup from user
const rotateManipulator =
vtkMouseCameraTrackballRotateManipulator.newInstance({ button: 1 });
const panManipulator = vtkMouseCameraTrackballPanManipulator.newInstance({
button: 3,
control: true
});
const zoomManipulator = vtkMouseCameraTrackballZoomManipulator.newInstance({
button: 3,
scrollEnabled: true
});
const rangeManipulator = vtkMouseRangeManipulator.newInstance({
button: 1,
shift: true
});
let self = this;
function getWL() {
return self._wl;
}
function getWW() {
return self._ww;
}
function setWL(v) {
self._wl = self._wl + (v - self._wl) / 25; // 25 is a tweaking parameter
let gaussians = self._PGwidget.getGaussians().slice(); // NOTE: slice() to clone!
gaussians[0].position = self._wl; //TODO: foreach
self._PGwidget.setGaussians(gaussians);
}
function setWW(v) {
self._ww = self._ww + (v - self._ww) / 5; // 5 is a tweaking parameter
let gaussians = self._PGwidget.getGaussians().slice(); // NOTE: slice() to clone!
gaussians[0].width = self._ww; //TODO: foreach
self._PGwidget.setGaussians(gaussians);
}
rangeManipulator.setVerticalListener(-1, 1, 0.001, getWL, setWL);
rangeManipulator.setHorizontalListener(0.1, 2.1, 0.001, getWW, setWW);
const interactorStyle = vtkInteractorStyleManipulator.newInstance();
interactorStyle.addMouseManipulator(rangeManipulator);
interactorStyle.addMouseManipulator(rotateManipulator);
interactorStyle.addMouseManipulator(panManipulator);
interactorStyle.addMouseManipulator(zoomManipulator);
interactorStyle.setCenterOfRotation(this._actor.getCenter());
this._renderWindow.getInteractor().setInteractorStyle(interactorStyle);
// clear measurements on interactions
this._renderWindow
.getInteractor()
.onMouseWheel(() => this.resetMeasurementState());
this._renderWindow
.getInteractor()
.onRightButtonPress(() => this.resetMeasurementState());
}
/**
* initPicker
*/
_initPicker(state, mode) {
// no blur when measure
this.blurOnInteraction = false;
// de-activate rotation
let rotateManipulator = this._renderWindow
.getInteractor()
.getInteractorStyle()
.getMouseManipulators()
.filter(i => {
return i.getClassName() == "vtkMouseCameraTrackballRotateManipulator";
})
.pop();
this._renderWindow
.getInteractor()
.getInteractorStyle()
.removeMouseManipulator(rotateManipulator);
// Setup picking interaction
// TODO this is slow the first time we pick, maybe we could use cellPicker and decrease resolution
const picker = vtkPointPicker.newInstance();
picker.setPickFromList(1);
picker.initializePickList();
if (!this._pickingPlane) {
// add a 1000x1000 plane
let camera = this._renderer.getActiveCamera();
let { plane, planeActor } = setupPickingPlane(camera, this._actor);
this._renderer.addActor(planeActor);
this._pickingPlane = plane;
this._planeActor = planeActor;
}
// add picking plane to pick list
picker.addPickList(this._planeActor);
// Pick on mouse left click
this._leftButtonCb = this._renderWindow
.getInteractor()
.onLeftButtonPress(callData => {
if (this._renderer !== callData.pokedRenderer) {
return;
}
const pos = callData.position;
const point = [pos.x, pos.y, 0.0];
picker.pick(point, this._renderer);
if (picker.getActors().length === 0) {
const pickedPoint = picker.getPickPosition();
if (this.VERBOSE)
console.log(`No point picked, default: ${pickedPoint}`);
// addSphereInPoint(pickedPoint, this._renderer);
} else {
const pickedPoints = picker.getPickedPositions();
const pickedPoint = pickedPoints[0]; // always a single point on a plane
if (this.VERBOSE) console.log(`Picked: ${pickedPoint}`);
// addSphereInPoint(pickedPoint, this._renderer);
// canvas coord
const wPos = vtkCoordinate.newInstance();
wPos.setCoordinateSystemToWorld();
wPos.setValue(...pickedPoint);
const displayPosition = wPos.getComputedDisplayValue(this._renderer);
// apply changes on state based on active tool
applyStrategy(state, displayPosition, pickedPoint, mode);
if (this.VERBOSE) console.log(state);
this._measurementState = state;
}
this._renderWindow.render();
});
}
}