@acransac/vtk.js
Version:
Visualization Toolkit for the Web
278 lines (238 loc) • 9.44 kB
JavaScript
/* eslint-disable import/prefer-default-export */
/* eslint-disable import/no-extraneous-dependencies */
import 'vtk.js/Sources/favicon';
import macro from 'vtk.js/Sources/macro';
import HttpDataAccessHelper from 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper';
import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox';
import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction';
import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow';
import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction';
import vtkVolumeController from 'vtk.js/Sources/Interaction/UI/VolumeController';
import vtkURLExtract from 'vtk.js/Sources/Common/Core/URLExtract';
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
import vtkXMLImageDataReader from 'vtk.js/Sources/IO/XML/XMLImageDataReader';
import vtkFPSMonitor from 'vtk.js/Sources/Interaction/UI/FPSMonitor';
import style from './VolumeViewer.module.css';
let autoInit = true;
const userParams = vtkURLExtract.extractURLParameters();
const fpsMonitor = vtkFPSMonitor.newInstance();
// ----------------------------------------------------------------------------
// Add class to body if iOS device
// ----------------------------------------------------------------------------
const iOS = /iPad|iPhone|iPod/.test(window.navigator.platform);
if (iOS) {
document.querySelector('body').classList.add('is-ios-device');
}
// ----------------------------------------------------------------------------
function emptyContainer(container) {
while (container.firstChild) {
container.removeChild(container.firstChild);
}
}
// ----------------------------------------------------------------------------
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// ----------------------------------------------------------------------------
function createViewer(rootContainer, fileContents, options) {
const background = options.background
? options.background.split(',').map((s) => Number(s))
: [0, 0, 0];
const containerStyle = options.containerStyle;
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
background,
rootContainer,
containerStyle,
});
const renderer = fullScreenRenderer.getRenderer();
const renderWindow = fullScreenRenderer.getRenderWindow();
renderWindow.getInteractor().setDesiredUpdateRate(15);
const vtiReader = vtkXMLImageDataReader.newInstance();
vtiReader.parseAsArrayBuffer(fileContents);
const source = vtiReader.getOutputData(0);
const mapper = vtkVolumeMapper.newInstance();
const actor = vtkVolume.newInstance();
const dataArray =
source.getPointData().getScalars() || source.getPointData().getArrays()[0];
const dataRange = dataArray.getRange();
const lookupTable = vtkColorTransferFunction.newInstance();
const piecewiseFunction = vtkPiecewiseFunction.newInstance();
// Pipeline handling
actor.setMapper(mapper);
mapper.setInputData(source);
renderer.addActor(actor);
// Configuration
const sampleDistance =
0.7 *
Math.sqrt(
source
.getSpacing()
.map((v) => v * v)
.reduce((a, b) => a + b, 0)
);
mapper.setSampleDistance(sampleDistance);
actor.getProperty().setRGBTransferFunction(0, lookupTable);
actor.getProperty().setScalarOpacity(0, piecewiseFunction);
// actor.getProperty().setInterpolationTypeToFastLinear();
actor.getProperty().setInterpolationTypeToLinear();
// For better looking volume rendering
// - distance in world coordinates a scalar opacity of 1.0
actor
.getProperty()
.setScalarOpacityUnitDistance(
0,
vtkBoundingBox.getDiagonalLength(source.getBounds()) /
Math.max(...source.getDimensions())
);
// - control how we emphasize surface boundaries
// => max should be around the average gradient magnitude for the
// volume or maybe average plus one std dev of the gradient magnitude
// (adjusted for spacing, this is a world coordinate gradient, not a
// pixel gradient)
// => max hack: (dataRange[1] - dataRange[0]) * 0.05
actor.getProperty().setGradientOpacityMinimumValue(0, 0);
actor
.getProperty()
.setGradientOpacityMaximumValue(0, (dataRange[1] - dataRange[0]) * 0.05);
// - Use shading based on gradient
actor.getProperty().setShade(true);
actor.getProperty().setUseGradientOpacity(0, true);
// - generic good default
actor.getProperty().setGradientOpacityMinimumOpacity(0, 0.0);
actor.getProperty().setGradientOpacityMaximumOpacity(0, 1.0);
actor.getProperty().setAmbient(0.2);
actor.getProperty().setDiffuse(0.7);
actor.getProperty().setSpecular(0.3);
actor.getProperty().setSpecularPower(8.0);
// Control UI
const controllerWidget = vtkVolumeController.newInstance({
size: [400, 150],
rescaleColorMap: true,
});
const isBackgroundDark = background[0] + background[1] + background[2] < 1.5;
controllerWidget.setContainer(rootContainer);
controllerWidget.setupContent(renderWindow, actor, isBackgroundDark);
// setUpContent above sets the size to the container.
// We need to set the size after that.
// controllerWidget.setExpanded(false);
fullScreenRenderer.setResizeCallback(({ width, height }) => {
// 2px padding + 2x1px boder + 5px edge = 14
if (width > 414) {
controllerWidget.setSize(400, 150);
} else {
controllerWidget.setSize(width - 14, 150);
}
controllerWidget.render();
fpsMonitor.update();
});
// First render
renderer.resetCamera();
renderWindow.render();
global.pipeline = {
actor,
renderer,
renderWindow,
lookupTable,
mapper,
source,
piecewiseFunction,
fullScreenRenderer,
};
if (userParams.fps) {
const fpsElm = fpsMonitor.getFpsMonitorContainer();
fpsElm.classList.add(style.fpsMonitor);
fpsMonitor.setRenderWindow(renderWindow);
fpsMonitor.setContainer(rootContainer);
fpsMonitor.update();
}
}
// ----------------------------------------------------------------------------
export function load(container, options) {
autoInit = false;
emptyContainer(container);
if (options.file) {
if (options.ext === 'vti') {
const reader = new FileReader();
reader.onload = function onLoad(e) {
createViewer(container, reader.result, options);
};
reader.readAsArrayBuffer(options.file);
} else {
console.error('Unkown file...');
}
} else if (options.fileURL) {
const progressContainer = document.createElement('div');
progressContainer.setAttribute('class', style.progress);
container.appendChild(progressContainer);
const progressCallback = (progressEvent) => {
if (progressEvent.lengthComputable) {
const percent = Math.floor(
(100 * progressEvent.loaded) / progressEvent.total
);
progressContainer.innerHTML = `Loading ${percent}%`;
} else {
progressContainer.innerHTML = macro.formatBytesToProperUnit(
progressEvent.loaded
);
}
};
HttpDataAccessHelper.fetchBinary(options.fileURL, {
progressCallback,
}).then((binary) => {
container.removeChild(progressContainer);
createViewer(container, binary, options);
});
}
}
export function initLocalFileLoader(container) {
const exampleContainer = document.querySelector('.content');
const rootBody = document.querySelector('body');
const myContainer = container || exampleContainer || rootBody;
const fileContainer = document.createElement('div');
fileContainer.innerHTML = `<div class="${style.bigFileDrop}"/><input type="file" accept=".vti" style="display: none;"/>`;
myContainer.appendChild(fileContainer);
const fileInput = fileContainer.querySelector('input');
function handleFile(e) {
preventDefaults(e);
const dataTransfer = e.dataTransfer;
const files = e.target.files || dataTransfer.files;
if (files.length === 1) {
myContainer.removeChild(fileContainer);
const ext = files[0].name.split('.').slice(-1)[0];
const options = { file: files[0], ext, ...userParams };
load(myContainer, options);
}
}
fileInput.addEventListener('change', handleFile);
fileContainer.addEventListener('drop', handleFile);
fileContainer.addEventListener('click', (e) => fileInput.click());
fileContainer.addEventListener('dragover', preventDefaults);
}
// Look at URL an see if we should load a file
// ?fileURL=https://data.kitware.com/api/v1/item/59cdbb588d777f31ac63de08/download
if (userParams.fileURL) {
const exampleContainer = document.querySelector('.content');
const rootBody = document.querySelector('body');
const myContainer = exampleContainer || rootBody;
load(myContainer, userParams);
}
const viewerContainers = document.querySelectorAll('.vtkjs-volume-viewer');
let nbViewers = viewerContainers.length;
while (nbViewers--) {
const viewerContainer = viewerContainers[nbViewers];
const fileURL = viewerContainer.dataset.url;
const options = {
containerStyle: { height: '100%' },
...userParams,
fileURL,
};
load(viewerContainer, options);
}
// Auto setup if no method get called within 100ms
setTimeout(() => {
if (autoInit) {
initLocalFileLoader();
}
}, 100);