@acransac/vtk.js
Version:
Visualization Toolkit for the Web
470 lines (384 loc) • 14.9 kB
JavaScript
import 'vtk.js/Sources/favicon';
import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow';
import vtkWidgetManager from 'vtk.js/Sources/Widgets/Core/WidgetManager';
import vtkPaintWidget from 'vtk.js/Sources/Widgets/Widgets3D/PaintWidget';
import vtkRectangleWidget from 'vtk.js/Sources/Widgets/Widgets3D/RectangleWidget';
import vtkEllipseWidget from 'vtk.js/Sources/Widgets/Widgets3D/EllipseWidget';
import vtkSplineWidget from 'vtk.js/Sources/Widgets/Widgets3D/SplineWidget';
import vtkInteractorStyleImage from 'vtk.js/Sources/Interaction/Style/InteractorStyleImage';
import vtkHttpDataSetReader from 'vtk.js/Sources/IO/Core/HttpDataSetReader';
import vtkImageMapper from 'vtk.js/Sources/Rendering/Core/ImageMapper';
import vtkImageSlice from 'vtk.js/Sources/Rendering/Core/ImageSlice';
import vtkPaintFilter from 'vtk.js/Sources/Filters/General/PaintFilter';
import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction';
import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction';
import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox';
import {
BehaviorCategory,
ShapeBehavior,
} from 'vtk.js/Sources/Widgets/Widgets3D/ShapeWidget/Constants';
import {
TextAlign,
VerticalAlign,
} from 'vtk.js/Sources/Interaction/Widgets/LabelRepresentation/Constants';
import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';
import { vec3 } from 'gl-matrix';
import controlPanel from './controlPanel.html';
// ----------------------------------------------------------------------------
// Standard rendering code setup
// ----------------------------------------------------------------------------
// scene
const scene = {};
scene.fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
rootContainer: document.body,
background: [0.1, 0.1, 0.1],
});
scene.renderer = scene.fullScreenRenderer.getRenderer();
scene.renderWindow = scene.fullScreenRenderer.getRenderWindow();
scene.openGLRenderWindow = scene.fullScreenRenderer.getOpenGLRenderWindow();
scene.camera = scene.renderer.getActiveCamera();
// setup 2D view
scene.camera.setParallelProjection(true);
scene.iStyle = vtkInteractorStyleImage.newInstance();
scene.iStyle.setInteractionMode('IMAGE_SLICING');
scene.renderWindow.getInteractor().setInteractorStyle(scene.iStyle);
scene.fullScreenRenderer.addController(controlPanel);
function setCamera(sliceMode, renderer, data) {
const ijk = [0, 0, 0];
const position = [0, 0, 0];
const focalPoint = [0, 0, 0];
data.indexToWorldVec3(ijk, focalPoint);
ijk[sliceMode] = 1;
data.indexToWorldVec3(ijk, position);
renderer.getActiveCamera().set({ focalPoint, position });
renderer.resetCamera();
}
// ----------------------------------------------------------------------------
// Widget manager and vtkPaintFilter
// ----------------------------------------------------------------------------
scene.widgetManager = vtkWidgetManager.newInstance();
scene.widgetManager.setRenderer(scene.renderer);
// Widgets
const widgets = {};
widgets.paintWidget = vtkPaintWidget.newInstance();
widgets.rectangleWidget = vtkRectangleWidget.newInstance();
widgets.ellipseWidget = vtkEllipseWidget.newInstance();
widgets.circleWidget = vtkEllipseWidget.newInstance({
modifierBehavior: {
None: {
[BehaviorCategory.PLACEMENT]:
ShapeBehavior[BehaviorCategory.PLACEMENT].CLICK_AND_DRAG,
[BehaviorCategory.POINTS]: ShapeBehavior[BehaviorCategory.POINTS].RADIUS,
[BehaviorCategory.RATIO]: ShapeBehavior[BehaviorCategory.RATIO].FREE,
},
Control: {
[BehaviorCategory.POINTS]:
ShapeBehavior[BehaviorCategory.POINTS].DIAMETER,
},
},
});
widgets.splineWidget = vtkSplineWidget.newInstance();
widgets.polygonWidget = vtkSplineWidget.newInstance({
resolution: 1,
});
scene.paintHandle = scene.widgetManager.addWidget(
widgets.paintWidget,
ViewTypes.SLICE
);
scene.rectangleHandle = scene.widgetManager.addWidget(
widgets.rectangleWidget,
ViewTypes.SLICE
);
scene.ellipseHandle = scene.widgetManager.addWidget(
widgets.ellipseWidget,
ViewTypes.SLICE
);
scene.circleHandle = scene.widgetManager.addWidget(
widgets.circleWidget,
ViewTypes.SLICE
);
scene.splineHandle = scene.widgetManager.addWidget(
widgets.splineWidget,
ViewTypes.SLICE
);
scene.polygonHandle = scene.widgetManager.addWidget(
widgets.polygonWidget,
ViewTypes.SLICE
);
scene.splineHandle.setOutputBorder(true);
scene.polygonHandle.setOutputBorder(true);
scene.widgetManager.grabFocus(widgets.paintWidget);
let activeWidget = 'paintWidget';
// Paint filter
const painter = vtkPaintFilter.newInstance();
// ----------------------------------------------------------------------------
// Ready logic
// ----------------------------------------------------------------------------
function ready(scope, picking = false) {
scope.renderer.resetCamera();
scope.fullScreenRenderer.resize();
if (picking) {
scope.widgetManager.enablePicking();
} else {
scope.widgetManager.disablePicking();
}
}
function readyAll() {
ready(scene, true);
}
function updateControlPanel(im, ds) {
const slicingMode = im.getSlicingMode();
const extent = ds.getExtent();
document.querySelector('.slice').setAttribute('min', extent[slicingMode * 2]);
document
.querySelector('.slice')
.setAttribute('max', extent[slicingMode * 2 + 1]);
}
// ----------------------------------------------------------------------------
// Load image
// ----------------------------------------------------------------------------
const image = {
imageMapper: vtkImageMapper.newInstance(),
actor: vtkImageSlice.newInstance(),
};
const labelMap = {
imageMapper: vtkImageMapper.newInstance(),
actor: vtkImageSlice.newInstance(),
cfun: vtkColorTransferFunction.newInstance(),
ofun: vtkPiecewiseFunction.newInstance(),
};
// background image pipeline
image.actor.setMapper(image.imageMapper);
// labelmap pipeline
labelMap.actor.setMapper(labelMap.imageMapper);
labelMap.imageMapper.setInputConnection(painter.getOutputPort());
// set up labelMap color and opacity mapping
labelMap.cfun.addRGBPoint(1, 0, 0, 1); // label "1" will be blue
labelMap.ofun.addPoint(0, 0); // our background value, 0, will be invisible
labelMap.ofun.addPoint(1, 1); // all values above 1 will be fully opaque
labelMap.actor.getProperty().setRGBTransferFunction(labelMap.cfun);
labelMap.actor.getProperty().setPiecewiseFunction(labelMap.ofun);
// opacity is applied to entire labelmap
labelMap.actor.getProperty().setOpacity(0.5);
const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true });
reader
.setUrl(`${__BASE_PATH__}/data/volume/LIDC2.vti`, { loadData: true })
.then(() => {
const data = reader.getOutputData();
image.data = data;
// set input data
image.imageMapper.setInputData(data);
// add actors to renderers
scene.renderer.addViewProp(image.actor);
scene.renderer.addViewProp(labelMap.actor);
// update paint filter
painter.setBackgroundImage(image.data);
// don't set to 0, since that's our empty label color from our pwf
painter.setLabel(1);
// set custom threshold
// painter.setVoxelFunc((bgValue, idx) => bgValue < 145);
// default slice orientation/mode and camera view
const sliceMode = vtkImageMapper.SlicingMode.K;
image.imageMapper.setSlicingMode(sliceMode);
image.imageMapper.setSlice(0);
painter.setSlicingMode(sliceMode);
// set 2D camera position
setCamera(sliceMode, scene.renderer, image.data);
updateControlPanel(image.imageMapper, data);
scene.circleHandle.setLabelTextCallback((worldBounds, screenBounds) => {
const center = vtkBoundingBox.getCenter(screenBounds);
const radius =
vec3.distance(center, [
screenBounds[0],
screenBounds[2],
screenBounds[4],
]) / 2;
const position = [0, 0, 0];
vec3.scaleAndAdd(position, center, [1, 1, 1], radius);
return {
text: `radius: ${(
vec3.distance(
[worldBounds[0], worldBounds[2], worldBounds[4]],
[worldBounds[1], worldBounds[3], worldBounds[5]]
) / 2
).toFixed(2)}`,
position,
textAlign: TextAlign.CENTER,
verticalAlign: VerticalAlign.CENTER,
};
});
scene.splineHandle
.getWidgetState()
.getMoveHandle()
.setScale1(2 * Math.max(...image.data.getSpacing()));
scene.splineHandle.setFreehandMinDistance(
4 * Math.max(...image.data.getSpacing())
);
scene.polygonHandle
.getWidgetState()
.getMoveHandle()
.setScale1(2 * Math.max(...image.data.getSpacing()));
scene.polygonHandle.setFreehandMinDistance(
4 * Math.max(...image.data.getSpacing())
);
const update = () => {
const slicingMode = image.imageMapper.getSlicingMode() % 3;
if (slicingMode > -1) {
const ijk = [0, 0, 0];
const position = [0, 0, 0];
// position
ijk[slicingMode] = image.imageMapper.getSlice();
data.indexToWorldVec3(ijk, position);
widgets.paintWidget.getManipulator().setOrigin(position);
widgets.rectangleWidget.getManipulator().setOrigin(position);
widgets.ellipseWidget.getManipulator().setOrigin(position);
widgets.circleWidget.getManipulator().setOrigin(position);
widgets.splineWidget.getManipulator().setOrigin(position);
widgets.polygonWidget.getManipulator().setOrigin(position);
painter.setSlicingMode(slicingMode);
scene.paintHandle.updateRepresentationForRender();
scene.rectangleHandle.updateRepresentationForRender();
scene.ellipseHandle.updateRepresentationForRender();
scene.circleHandle.updateRepresentationForRender();
scene.splineHandle.updateRepresentationForRender();
scene.polygonHandle.updateRepresentationForRender();
// update labelMap layer
labelMap.imageMapper.set(image.imageMapper.get('slice', 'slicingMode'));
// update UI
document
.querySelector('.slice')
.setAttribute('max', data.getDimensions()[slicingMode] - 1);
}
};
image.imageMapper.onModified(update);
// trigger initial update
update();
// readyAll();
});
// register readyAll to resize event
window.addEventListener('resize', readyAll);
readyAll();
// ----------------------------------------------------------------------------
// UI logic
// ----------------------------------------------------------------------------
document.querySelector('.radius').addEventListener('input', (ev) => {
const r = Number(ev.target.value);
widgets.paintWidget.setRadius(r);
painter.setRadius(r);
});
document.querySelector('.slice').addEventListener('input', (ev) => {
image.imageMapper.setSlice(Number(ev.target.value));
});
document.querySelector('.axis').addEventListener('input', (ev) => {
const sliceMode = 'IJKXYZ'.indexOf(ev.target.value) % 3;
image.imageMapper.setSlicingMode(sliceMode);
painter.setSlicingMode(sliceMode);
const direction = [0, 0, 0];
direction[sliceMode] = 1;
scene.paintHandle.getWidgetState().getHandle().setDirection(direction);
setCamera(sliceMode, scene.renderer, image.data);
scene.renderWindow.render();
});
document.querySelector('.widget').addEventListener('input', (ev) => {
activeWidget = ev.target.value;
scene.widgetManager.grabFocus(widgets[activeWidget]);
scene.paintHandle.setVisibility(activeWidget === 'paintWidget');
scene.paintHandle.updateRepresentationForRender();
scene.splineHandle.reset();
scene.splineHandle.setVisibility(activeWidget === 'splineWidget');
scene.splineHandle.updateRepresentationForRender();
scene.polygonHandle.reset();
scene.polygonHandle.setVisibility(activeWidget === 'polygonWidget');
scene.polygonHandle.updateRepresentationForRender();
});
document.querySelector('.focus').addEventListener('click', () => {
scene.widgetManager.grabFocus(widgets[activeWidget]);
});
document.querySelector('.undo').addEventListener('click', () => {
painter.undo();
});
document.querySelector('.redo').addEventListener('click', () => {
painter.redo();
});
// ----------------------------------------------------------------------------
// Painting
// ----------------------------------------------------------------------------
function initializeHandle(handle) {
handle.onStartInteractionEvent(() => {
painter.startStroke();
});
handle.onEndInteractionEvent(() => {
painter.endStroke();
});
}
initializeHandle(scene.paintHandle);
scene.paintHandle.onStartInteractionEvent(() => {
painter.startStroke();
painter.addPoint(widgets.paintWidget.getWidgetState().getTrueOrigin());
});
scene.paintHandle.onInteractionEvent(() => {
painter.addPoint(widgets.paintWidget.getWidgetState().getTrueOrigin());
});
initializeHandle(scene.rectangleHandle);
scene.rectangleHandle.onInteractionEvent(() => {
const rectangleHandle = scene.rectangleHandle
.getWidgetState()
.getRectangleHandle();
painter.paintRectangle(
rectangleHandle.getOrigin(),
rectangleHandle.getCorner()
);
});
initializeHandle(scene.ellipseHandle);
scene.ellipseHandle.onInteractionEvent(() => {
const center = scene.ellipseHandle
.getWidgetState()
.getEllipseHandle()
.getOrigin();
const point2 = scene.ellipseHandle
.getWidgetState()
.getPoint2Handle()
.getOrigin();
const corner = [
center[0] - point2[0],
center[1] - point2[1],
center[2] - point2[2],
];
painter.paintEllipse(center, corner);
});
initializeHandle(scene.circleHandle);
scene.circleHandle.onInteractionEvent(() => {
const center = scene.circleHandle
.getWidgetState()
.getEllipseHandle()
.getOrigin();
const point2 = scene.circleHandle
.getWidgetState()
.getPoint1Handle()
.getOrigin();
const radius = vec3.distance(center, point2);
const corner = [radius, radius, radius];
painter.paintEllipse(center, corner);
});
scene.splineHandle.onStartInteractionEvent(() => {
painter.startStroke();
});
scene.splineHandle.onEndInteractionEvent(() => {
const points = scene.splineHandle.getPoints();
painter.paintPolygon(points);
painter.endStroke();
scene.splineHandle.reset();
scene.splineHandle.updateRepresentationForRender();
scene.widgetManager.grabFocus(widgets.splineWidget);
});
scene.polygonHandle.onStartInteractionEvent(() => {
painter.startStroke();
});
scene.polygonHandle.onEndInteractionEvent(() => {
const points = scene.polygonHandle.getPoints();
painter.paintPolygon(points);
painter.endStroke();
scene.polygonHandle.reset();
scene.polygonHandle.updateRepresentationForRender();
scene.widgetManager.grabFocus(widgets.polygonWidget);
});