OfflineLocalView

Live example

# Offline Local View

It uses a static export of the dynamic synchronizable view of vtk.js which get used within trame for its local view.

This is notably use by trame and PyVista

You can download PyVista example files:

/* eslint-disable import/prefer-default-export */
/* eslint-disable import/no-extraneous-dependencies */

import '@kitware/vtk.js/favicon';

// Load the rendering pieces we want to use (for both WebGL and WebGPU)
import '@kitware/vtk.js/Rendering/OpenGL/Profiles/All';

import macro from '@kitware/vtk.js/macros';
import Base64 from '@kitware/vtk.js/Common/Core/Base64';
import DataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper';
import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper';
import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract';
import vtkSynchronizableRenderWindow from '@kitware/vtk.js/Rendering/Misc/SynchronizableRenderWindow';

// Force DataAccessHelper to have access to various data source
import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper';
import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper';

import style from './viewer.module.css';

let autoInit = true;

function emptyContainer(container) {
while (container.firstChild) {
container.removeChild(container.firstChild);
}
}

function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}

function onVisible(element, callback) {
new window.parent.IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0) {
callback();
observer.disconnect();
}
});
}).observe(element);
}

function runOnVisible(callback) {
if (!window.frameElement) {
callback();
return true;
}
const visible = window.frameElement.getClientRects().length > 0;
if (!visible) {
onVisible(window.frameElement, callback);
} else {
callback();
}
return visible;
}

export function load(container, options) {
autoInit = false;
emptyContainer(container);

const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
background: [1, 1, 1],
rootContainer: container,
containerStyle: { height: '100%', width: '100%', position: 'absolute' },
});
const renderWindow = fullScreenRenderer.getRenderWindow();
const syncCTX = vtkSynchronizableRenderWindow.getSynchronizerContext();
const syncRW = vtkSynchronizableRenderWindow.decorate(renderWindow);
global.renderWindow = renderWindow;

function onReady(data) {
syncCTX.setFetchArrayFunction((sha) =>
Promise.resolve(data.hashes[sha].content)
);
console.log(Object.keys(data));
syncRW.synchronize(data.scene);
syncRW.render();
}

if (options.fileURL || options.url) {
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
);
}
};

if (options.fileURL) {
HttpDataAccessHelper.fetchBinary(options.fileURL, {
progressCallback,
}).then((zipContent) => {
container.removeChild(progressContainer);
const dataAccessHelper = DataAccessHelper.get('zip', {
zipContent,
callback: (zip) => {
dataAccessHelper.fetchJSON(null, 'index.json').then(onReady);
},
});
});
} else {
HttpDataAccessHelper.fetchJSON(options.url, {
progressCallback,
}).then((data) => {
container.removeChild(progressContainer);
onReady(data);
});
}
} else if (options.file) {
const dataAccessHelper = DataAccessHelper.get('zip', {
zipContent: options.file,
callback: (zip) => {
dataAccessHelper.fetchJSON(null, 'index.json').then(onReady);
},
});
} else if (options.base64Str) {
const zipContent = Base64.toArrayBuffer(options.base64Str);
const dataAccessHelper = DataAccessHelper.get('zip', {
zipContent,
callback: (zip) => {
dataAccessHelper.fetchJSON(null, 'index.json').then(onReady);
},
});
}
}

export function initLocalFileLoader(container) {
autoInit = false;
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" accept=".zip,.vtksz" 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];
load(myContainer, { file: files[0], ext });
}
}

fileInput.addEventListener('change', handleFile);
fileContainer.addEventListener('drop', handleFile);
fileContainer.addEventListener('click', (e) => fileInput.click());
fileContainer.addEventListener('dragover', preventDefaults);
}

const userParams = vtkURLExtract.extractURLParameters();

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';
}

autoInit = false;
runOnVisible(() => load(myContainer, userParams));
}

// Auto setup if no method get called within 100ms
setTimeout(() => {
if (autoInit) {
initLocalFileLoader();
}
}, 100);

window.OfflineLocalView = {
initLocalFileLoader,
load,
};