import '@kitware/vtk.js/favicon';
import '@kitware/vtk.js/Rendering/Profiles/Geometry';
import { formatBytesToProperUnit, debounce } from '@kitware/vtk.js/macros'; import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; import vtkScalarBarActor from '@kitware/vtk.js/Rendering/Core/ScalarBarActor'; 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 vtkWebXRRenderWindowHelper from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper'; 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 vtkFPSMonitor from '@kitware/vtk.js/Interaction/UI/FPSMonitor'; import { XrSessionTypes } from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper/Constants';
import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper';
import { ColorMode, ScalarMode, } from '@kitware/vtk.js/Rendering/Core/Mapper/Constants';
import style from './GeometryViewer.module.css'; import icon from '../../../Documentation/content/icon/favicon-96x96.png';
let autoInit = true; let background = [0, 0, 0]; let fullScreenRenderWindow; let renderWindow; let xrRenderWindowHelper; let renderer; let scalarBarActor;
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 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(); }
let requestedXrSessionType = userParams.xrSessionType !== undefined ? userParams.xrSessionType : null; if ( requestedXrSessionType !== null && !Object.values(XrSessionTypes).includes(requestedXrSessionType) ) { console.warn( 'Could not parse requested XR session type: ', requestedXrSessionType ); requestedXrSessionType = null; }
if (requestedXrSessionType === XrSessionTypes.LookingGlassVR) { import( 'https://unpkg.com/@lookingglass/webxr@0.3.0/dist/@lookingglass/bundle/webxr.js' ).then((obj) => { new obj.LookingGlassWebXRPolyfill(); }); } else if (requestedXrSessionType === null && navigator.xr !== undefined) { navigator.xr.isSessionSupported('immersive-vr').then((vrSupported) => { if (vrSupported) { requestedXrSessionType = XrSessionTypes.HmdVr; } else { navigator.xr.isSessionSupported('immersive-ar').then((arSupported) => { requestedXrSessionType = arSupported ? XrSessionTypes.MobileAR : null; }); } }); }
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 fpsMonitor = vtkFPSMonitor.newInstance(); const fpsElm = fpsMonitor.getFpsMonitorContainer(); fpsElm.classList.add(style.fpsMonitor);
const iOS = /iPad|iPhone|iPod/.test(window.navigator.platform);
if (iOS) { document.querySelector('body').classList.add('is-ios-device'); }
function emptyContainer(container) { fpsMonitor.setContainer(null); while (container.firstChild) { container.removeChild(container.firstChild); } }
function createViewer(container) { fullScreenRenderWindow = vtkFullScreenRenderWindow.newInstance({ background, rootContainer: container, containerStyle: { height: '100%', width: '100%', position: 'absolute' }, }); renderer = fullScreenRenderWindow.getRenderer(); renderWindow = fullScreenRenderWindow.getRenderWindow(); xrRenderWindowHelper = vtkWebXRRenderWindowHelper.newInstance({ renderWindow: fullScreenRenderWindow.getApiSpecificRenderWindow(), }); renderWindow.getInteractor().setDesiredUpdateRate(15);
container.appendChild(rootControllerContainer); container.appendChild(addDataSetButton);
scalarBarActor = vtkScalarBarActor.newInstance(); renderer.addActor(scalarBarActor);
if (userParams.fps) { if (Array.isArray(userParams.fps)) { fpsMonitor.setMonitorVisibility(...userParams.fps); if (userParams.fps.length === 4) { fpsMonitor.setOrientation(userParams.fps[3]); } } fpsMonitor.setRenderWindow(renderWindow); fpsMonitor.setContainer(container); fullScreenRenderWindow.setResizeCallback(fpsMonitor.update); } }
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 labelSelector = document.createElement('label'); labelSelector.setAttribute('class', selectorClass); labelSelector.innerHTML = fileName;
const immersionSelector = document.createElement('button'); immersionSelector.setAttribute('class', selectorClass); immersionSelector.innerHTML = requestedXrSessionType === XrSessionTypes.MobileAR ? 'Start AR' : 'Start VR';
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);
if ( navigator.xr !== undefined && xrRenderWindowHelper.getXrSupported() && requestedXrSessionType !== null ) { controlContainer.appendChild(immersionSelector); } rootControllerContainer.appendChild(controlContainer);
const vtpReader = vtkXMLPolyDataReader.newInstance(); vtpReader.parseAsArrayBuffer(fileContents);
const lookupTable = vtkColorTransferFunction.newInstance(); const source = vtpReader.getOutputData(0); const mapper = vtkMapper.newInstance({ interpolateScalarsBeforeMapping: true, useLookupTableScalarRange: true, lookupTable, scalarVisibility: false, }); const actor = vtkActor.newInstance(); const scalars = source.getPointData().getScalars(); const dataRange = [].concat(scalars ? scalars.getRange() : [0, 1]); let activeArray = vtkDataArray;
function applyPreset() { const preset = vtkColorMaps.getPresetByName(presetSelector.value); lookupTable.applyColorMap(preset); const offset = dataRange[1] === dataRange[0] ? 1 : 0; lookupTable.setMappingRange(dataRange[0] - offset, dataRange[1] + offset); lookupTable.updateRange(); renderWindow.render(); } applyPreset(); presetSelector.addEventListener('change', applyPreset);
function updateRepresentation(event) { const [visibility, representation, edgeVisibility] = event.target.value .split(':') .map(Number); actor.getProperty().set({ representation, edgeVisibility }); actor.setVisibility(!!visibility); renderWindow.render(); } representationSelector.addEventListener('change', updateRepresentation);
function updateOpacity(event) { const opacity = Number(event.target.value) / 100; actor.getProperty().setOpacity(opacity); renderWindow.render(); }
opacitySelector.addEventListener('input', updateOpacity);
const colorByOptions = [{ value: ':', label: 'Solid color' }].concat( source .getPointData() .getArrays() .map((a) => ({ label: `(p) ${a.getName()}`, value: `PointData:${a.getName()}`, })), source .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(':'); let colorMode = ColorMode.DEFAULT; let scalarMode = ScalarMode.DEFAULT; const scalarVisibility = location.length > 0; if (scalarVisibility) { const newArray = source[`get${location}`]().getArrayByName(colorByArrayName); activeArray = newArray; 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 (mapper.getLookupTable()) { const lut = mapper.getLookupTable(); lut.setVectorModeToMagnitude(); } componentSelector.style.display = 'block'; const compOpts = [['Magnitude', -1]]; while (compOpts.length <= numberOfComponents) { compOpts.push([`Component ${compOpts.length}`, compOpts.length - 1]); } if (numberOfComponents === 3 || numberOfComponents === 4) { compOpts.push([`Use direct mapping`, -2]); } componentSelector.innerHTML = compOpts .map((t) => `<option value="${t[1]}">${t[0]}</option>`) .join(''); } else { componentSelector.style.display = 'none'; } scalarBarActor.setAxisLabel(colorByArrayName); scalarBarActor.setVisibility(true); } else { componentSelector.style.display = 'none'; scalarBarActor.setVisibility(false); } mapper.set({ colorByArrayName, colorMode, interpolateScalarsBeforeMapping: true, scalarMode, scalarVisibility, }); applyPreset(); } colorBySelector.addEventListener('change', updateColorBy); updateColorBy({ target: colorBySelector });
function updateColorByComponent(event) { if (mapper.getLookupTable()) { const lut = mapper.getLookupTable(); mapper.setColorModeToMapScalars(); if (event.target.value === '-2') { mapper.setColorModeToDirectScalars(); } else if (event.target.value === '-1') { lut.setVectorModeToMagnitude(); } else { lut.setVectorModeToComponent(); lut.setVectorComponent(Number(event.target.value)); const newDataRange = activeArray.getRange(Number(event.target.value)); dataRange[0] = newDataRange[0]; dataRange[1] = newDataRange[1]; lookupTable.setMappingRange(dataRange[0], dataRange[1]); lut.updateRange(); } renderWindow.render(); } } componentSelector.addEventListener('change', updateColorByComponent);
function toggleXR() { if (immersionSelector.textContent.startsWith('Start')) { xrRenderWindowHelper.startXR(requestedXrSessionType); immersionSelector.textContent = [ XrSessionTypes.HmdAR, XrSessionTypes.MobileAR, ].includes(requestedXrSessionType) ? 'Exit AR' : 'Exit VR'; } else { xrRenderWindowHelper.stopXR(); immersionSelector.textContent = [ XrSessionTypes.HmdAR, XrSessionTypes.MobileAR, ].includes(requestedXrSessionType) ? 'Start AR' : 'Start VR'; } } immersionSelector.addEventListener('click', toggleXR);
actor.setMapper(mapper); mapper.setInputData(source); renderer.addActor(actor);
scalarBarActor.setScalarsToColors(mapper.getLookupTable());
const debouncedRender = debounce(renderWindow.render, 10); lookupTable.onModified(debouncedRender, -1);
renderer.resetCamera(); renderWindow.render();
global.pipeline[fileName] = { actor, mapper, source, lookupTable, renderer, renderWindow, };
fpsMonitor.update(); }
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 urls = [].concat(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 = formatBytesToProperUnit( progressEvent.loaded ); } };
createViewer(container); const nbURLs = urls.length; let nbLoadedData = 0;
while (urls.length) { const url = urls.pop(); const name = Array.isArray(userParams.name) ? userParams.name[urls.length] : `Data ${urls.length + 1}`; HttpDataAccessHelper.fetchBinary(url, { progressCallback, }).then((binary) => { nbLoadedData++; if (nbLoadedData === nbURLs) { container.removeChild(progressContainer); } createPipeline(name, 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);
|