import '@kitware/vtk.js/favicon';
import '@kitware/vtk.js/Rendering/Profiles/Geometry';
import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract'; import vtkXMLPolyDataReader from '@kitware/vtk.js/IO/XML/XMLPolyDataReader'; import vtkTubeFilter from '@kitware/vtk.js/Filters/General/TubeFilter'; import { ColorMode, ScalarMode, } from '@kitware/vtk.js/Rendering/Core/Mapper/Constants'; import { VaryRadius } from '@kitware/vtk.js/Filters/General/TubeFilter/Constants';
import style from './TubesViewer.module.css'; import icon from '../../../Documentation/content/icon/favicon-96x96.png';
let autoInit = true; let background = [0, 0, 0]; let renderWindow; let renderer;
global.pipeline = {};
const userParams = vtkURLExtract.extractURLParameters();
if (userParams.background) { background = userParams.background.split(',').map((s) => Number(s)); } const selectorClass = background.length === 3 && background.reduce((a, b) => a + b, 0) < 1.5 ? style.dark : style.light;
const defaultName = userParams.name || '';
const lutName = userParams.lut || 'erdc_rainbow_bright';
const field = userParams.field || '';
function updateCamera(camera) { ['zoom', 'pitch', 'elevation', 'yaw', 'azimuth', 'roll', 'dolly'].forEach( (key) => { if (userParams[key]) { camera[key](userParams[key]); } renderWindow.render(); } ); }
function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
const rootControllerContainer = document.createElement('div'); rootControllerContainer.setAttribute('class', style.rootController);
const addDataSetButton = document.createElement('img'); addDataSetButton.setAttribute('class', style.button); addDataSetButton.setAttribute('src', icon); addDataSetButton.addEventListener('click', () => { const isVisible = rootControllerContainer.style.display !== 'none'; rootControllerContainer.style.display = isVisible ? 'none' : 'flex'; });
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 createViewer(container) { const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ background, rootContainer: container, containerStyle: { height: '100%', width: '100%', position: 'absolute' }, }); renderer = fullScreenRenderer.getRenderer(); renderWindow = fullScreenRenderer.getRenderWindow(); renderWindow.getInteractor().setDesiredUpdateRate(15);
container.appendChild(rootControllerContainer); container.appendChild(addDataSetButton); }
function createPipeline(fileName, fileContents) { const presetSelector = document.createElement('select'); presetSelector.setAttribute('class', selectorClass); presetSelector.innerHTML = vtkColorMaps.rgbPresetNames .map( (name) => `<option value="${name}" ${ lutName === name ? 'selected="selected"' : '' }>${name}</option>` ) .join('');
const representationSelector = document.createElement('select'); representationSelector.setAttribute('class', selectorClass); representationSelector.innerHTML = [ 'Hidden', 'Points', 'Wireframe', 'Surface', 'Surface with Edge', ] .map( (name, idx) => `<option value="${idx === 0 ? 0 : 1}:${idx < 4 ? idx - 1 : 2}:${ idx === 4 ? 1 : 0 }">${name}</option>` ) .join(''); representationSelector.value = '1:2:0';
const colorBySelector = document.createElement('select'); colorBySelector.setAttribute('class', selectorClass);
const componentSelector = document.createElement('select'); componentSelector.setAttribute('class', selectorClass); componentSelector.style.display = 'none';
const opacitySelector = document.createElement('input'); opacitySelector.setAttribute('class', selectorClass); opacitySelector.setAttribute('type', 'range'); opacitySelector.setAttribute('value', '100'); opacitySelector.setAttribute('max', '100'); opacitySelector.setAttribute('min', '1');
const tubingRLabel = document.createElement('label'); tubingRLabel.setAttribute('class', selectorClass); tubingRLabel.innerHTML = 'Radius'; const radiusSelector = document.createElement('input'); radiusSelector.setAttribute('class', selectorClass); radiusSelector.setAttribute('type', 'range'); radiusSelector.setAttribute('value', '5'); radiusSelector.setAttribute('max', '100'); radiusSelector.setAttribute('min', '1');
const tubingnSLabel = document.createElement('label'); tubingnSLabel.setAttribute('class', selectorClass); tubingnSLabel.innerHTML = 'No. of Sides'; const nsidesSelector = document.createElement('input'); nsidesSelector.setAttribute('class', selectorClass); nsidesSelector.setAttribute('type', 'range'); nsidesSelector.setAttribute('value', '20'); nsidesSelector.setAttribute('max', '100'); nsidesSelector.setAttribute('min', '3');
const tubingCLabel = document.createElement('label'); tubingCLabel.setAttribute('class', selectorClass); tubingCLabel.innerHTML = 'Capping'; const cappingSelector = document.createElement('input'); cappingSelector.setAttribute('class', 'checkbox'); cappingSelector.setAttribute('type', 'checkbox'); cappingSelector.setAttribute('checked', 'true');
const tubingoRLabel = document.createElement('label'); tubingoRLabel.setAttribute('class', selectorClass); tubingoRLabel.innerHTML = 'On Ratio'; const onRatioSelector = document.createElement('input'); onRatioSelector.setAttribute('type', 'number'); onRatioSelector.setAttribute('value', '1'); onRatioSelector.setAttribute('max', '6'); onRatioSelector.setAttribute('min', '1');
const labelSelector = document.createElement('label'); labelSelector.setAttribute('class', selectorClass); labelSelector.innerHTML = fileName;
const tubeCheckBox = document.createElement('input'); tubeCheckBox.setAttribute('class', 'checkbox'); tubeCheckBox.setAttribute('type', 'checkbox'); tubeCheckBox.setAttribute('checked', true);
const tubingLabel = document.createElement('label'); tubingLabel.setAttribute('class', selectorClass); tubingLabel.innerHTML = 'Tubing';
const controlContainer = document.createElement('div'); controlContainer.setAttribute('class', style.control); controlContainer.appendChild(labelSelector); controlContainer.appendChild(representationSelector); controlContainer.appendChild(presetSelector); controlContainer.appendChild(colorBySelector); controlContainer.appendChild(componentSelector); controlContainer.appendChild(opacitySelector); controlContainer.appendChild(tubingLabel); controlContainer.appendChild(tubeCheckBox); rootControllerContainer.appendChild(controlContainer);
const tubeControlContainer = document.createElement('div'); tubeControlContainer.setAttribute('class', style.control); tubeControlContainer.appendChild(tubingRLabel); tubeControlContainer.appendChild(radiusSelector); tubeControlContainer.appendChild(tubingnSLabel); tubeControlContainer.appendChild(nsidesSelector); tubeControlContainer.appendChild(tubingoRLabel); tubeControlContainer.appendChild(onRatioSelector); tubeControlContainer.appendChild(tubingCLabel); tubeControlContainer.appendChild(cappingSelector); rootControllerContainer.appendChild(tubeControlContainer);
const vtpReader = vtkXMLPolyDataReader.newInstance(); vtpReader.parseAsArrayBuffer(fileContents);
const tubeFilter = vtkTubeFilter.newInstance(); tubeFilter.setRadius(0.05); tubeFilter.setCapping(true); tubeFilter.setNumberOfSides(20); tubeFilter.setInputConnection(vtpReader.getOutputPort()); tubeFilter.setInputArrayToProcess(0, 'Radius', 'PointData', 'Scalars'); tubeFilter.setVaryRadius(VaryRadius.VARY_RADIUS_BY_SCALAR);
const lookupTable = vtkColorTransferFunction.newInstance(); const mapper = vtkMapper.newInstance({ interpolateScalarsBeforeMapping: false, useLookupTableScalarRange: true, lookupTable, scalarVisibility: false, }); const tubeMapper = vtkMapper.newInstance({ interpolateScalarsBeforeMapping: false, useLookupTableScalarRange: true, lookupTable, scalarVisibility: false, }); const actor = vtkActor.newInstance(); const tubeActor = vtkActor.newInstance(); const scalars = tubeFilter.getOutputData().getPointData().getScalars(); const dataRange = [].concat(scalars ? scalars.getRange() : [0, 1]);
function applyPreset() { const preset = vtkColorMaps.getPresetByName(presetSelector.value); lookupTable.applyColorMap(preset); lookupTable.setMappingRange(dataRange[0], dataRange[1]); lookupTable.updateRange(); } applyPreset(); presetSelector.addEventListener('change', applyPreset);
function updateRepresentation(event) { const [visibility, representation, edgeVisibility] = event.target.value .split(':') .map(Number); tubeActor.getProperty().set({ representation, edgeVisibility }); tubeActor.setVisibility(!!visibility); renderWindow.render(); } representationSelector.addEventListener('change', updateRepresentation);
function updateOpacity(event) { const opacity = Number(event.target.value) / 100; actor.getProperty().setOpacity(opacity); tubeActor.getProperty().setOpacity(opacity); renderWindow.render(); }
opacitySelector.addEventListener('input', updateOpacity);
const colorByOptions = [{ value: ':', label: 'Solid color' }].concat( tubeFilter .getOutputData() .getPointData() .getArrays() .map((a) => ({ label: `(p) ${a.getName()}`, value: `PointData:${a.getName()}`, })), tubeFilter .getOutputData() .getCellData() .getArrays() .map((a) => ({ label: `(c) ${a.getName()}`, value: `CellData:${a.getName()}`, })) ); colorBySelector.innerHTML = colorByOptions .map( ({ label, value }) => `<option value="${value}" ${ field === value ? 'selected="selected"' : '' }>${label}</option>` ) .join('');
function updateColorBy(event) { const [location, colorByArrayName] = event.target.value.split(':'); const interpolateScalarsBeforeMapping = location === 'PointData'; let colorMode = ColorMode.DEFAULT; let scalarMode = ScalarMode.DEFAULT; const scalarVisibility = location.length > 0; if (scalarVisibility) { const activeArray = tubeFilter .getOutputData() [`get${location}`]() .getArrayByName(colorByArrayName); const newDataRange = activeArray.getRange(); dataRange[0] = newDataRange[0]; dataRange[1] = newDataRange[1]; colorMode = ColorMode.MAP_SCALARS; scalarMode = location === 'PointData' ? ScalarMode.USE_POINT_FIELD_DATA : ScalarMode.USE_CELL_FIELD_DATA;
const numberOfComponents = activeArray.getNumberOfComponents(); if (numberOfComponents > 1) { if (tubeMapper.getLookupTable()) { const lut = tubeMapper.getLookupTable(); lut.setVectorModeToMagnitude(); } componentSelector.style.display = 'block'; const compOpts = ['Magnitude']; while (compOpts.length <= numberOfComponents) { compOpts.push(`Component ${compOpts.length}`); } componentSelector.innerHTML = compOpts .map((t, index) => `<option value="${index - 1}">${t}</option>`) .join(''); } else { componentSelector.style.display = 'none'; } } else { componentSelector.style.display = 'none'; } mapper.set({ colorByArrayName, colorMode, interpolateScalarsBeforeMapping, scalarMode, scalarVisibility, }); tubeMapper.set({ colorByArrayName, colorMode, interpolateScalarsBeforeMapping, scalarMode, scalarVisibility, }); applyPreset(); } colorBySelector.addEventListener('change', updateColorBy); updateColorBy({ target: colorBySelector });
function updateColorByComponent(event) { if (tubeMapper.getLookupTable()) { const tubeLut = tubeMapper.getLookupTable(); if (event.target.value === -1) { tubeLut.setVectorModeToMagnitude(); } else { tubeLut.setVectorModeToComponent(); tubeLut.setVectorComponent(Number(event.target.value)); } renderWindow.render(); } } componentSelector.addEventListener('change', updateColorByComponent);
function showTubingControl(event) { const check = event.target.checked; if (check) { tubeControlContainer.style.display = 'block'; tubeActor.setVisibility(true); } else { tubeControlContainer.style.display = 'none'; tubeActor.setVisibility(false); } renderWindow.render(); }
tubeCheckBox.addEventListener('change', showTubingControl);
function updateTubeRadius(event) { const radius = Number(event.target.value) / 100; tubeFilter.setRadius(radius); renderWindow.render(); }
radiusSelector.addEventListener('input', updateTubeRadius);
function updateTubeNumberOfSides(event) { const nSides = Number(event.target.value); tubeFilter.setNumberOfSides(nSides); renderWindow.render(); }
nsidesSelector.addEventListener('input', updateTubeNumberOfSides);
function updateTubeOnRatio(event) { const onRatio = Number(event.target.value); tubeFilter.setOnRatio(onRatio); renderWindow.render(); }
onRatioSelector.addEventListener('input', updateTubeOnRatio);
function updateTubeCapping(event) { const checked = event.target.checked; tubeFilter.setCapping(checked); renderWindow.render(); }
cappingSelector.addEventListener('change', updateTubeCapping);
tubeActor.setMapper(tubeMapper); tubeMapper.setInputConnection(tubeFilter.getOutputPort()); renderer.addActor(tubeActor); actor.setMapper(mapper); mapper.setInputConnection(vtpReader.getOutputPort()); renderer.addActor(actor);
lookupTable.onModified(() => { renderWindow.render(); });
renderer.resetCamera(); renderWindow.render();
global.pipeline[fileName] = { actor, tubeActor, tubeFilter, tubeMapper, mapper, lookupTable, renderer, renderWindow, vtpReader, }; }
function loadFile(file) { const reader = new FileReader(); reader.onload = function onLoad(e) { createPipeline(file.name, reader.result); }; reader.readAsArrayBuffer(file); }
export function load(container, options) { autoInit = false; emptyContainer(container);
if (options.files) { createViewer(container); let count = options.files.length; while (count--) { loadFile(options.files[count]); } updateCamera(renderer.getActiveCamera()); } else if (options.fileURL) { const progressContainer = document.createElement('div'); progressContainer.setAttribute('class', style.progress); container.appendChild(progressContainer);
const progressCallback = (progressEvent) => { const percent = Math.floor( (100 * progressEvent.loaded) / progressEvent.total ); progressContainer.innerHTML = `Loading ${percent}%`; };
HttpDataAccessHelper.fetchBinary(options.fileURL, { progressCallback, }).then((binary) => { container.removeChild(progressContainer); createViewer(container); createPipeline(defaultName, binary); updateCamera(renderer.getActiveCamera()); }); } }
export function initLocalFileLoader(container) { const exampleContainer = document.querySelector('.content'); const rootBody = document.querySelector('body'); const myContainer = container || exampleContainer || rootBody;
if (myContainer !== container) { myContainer.classList.add(style.fullScreen); rootBody.style.margin = '0'; rootBody.style.padding = '0'; } else { rootBody.style.margin = '0'; rootBody.style.padding = '0'; }
const fileContainer = document.createElement('div'); fileContainer.innerHTML = `<div class="${style.bigFileDrop}"/><input type="file" multiple accept=".vtp" 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 > 0) { myContainer.removeChild(fileContainer); load(myContainer, { files }); } }
fileInput.addEventListener('change', handleFile); fileContainer.addEventListener('drop', handleFile); fileContainer.addEventListener('click', (e) => fileInput.click()); fileContainer.addEventListener('dragover', preventDefaults); }
if (userParams.url || userParams.fileURL) { const exampleContainer = document.querySelector('.content'); const rootBody = document.querySelector('body'); const myContainer = exampleContainer || rootBody;
if (myContainer) { myContainer.classList.add(style.fullScreen); rootBody.style.margin = '0'; rootBody.style.padding = '0'; }
load(myContainer, userParams); }
setTimeout(() => { if (autoInit) { initLocalFileLoader(); } }, 100);
|