@acransac/vtk.js
Version:
Visualization Toolkit for the Web
591 lines (494 loc) • 17.5 kB
JavaScript
import { radiansFromDegrees } from 'vtk.js/Sources/Common/Core/Math';
import { FieldAssociations } from 'vtk.js/Sources/Common/DataModel/DataSet/Constants';
import macro from 'vtk.js/Sources/macro';
import vtkOpenGLHardwareSelector from 'vtk.js/Sources/Rendering/OpenGL/HardwareSelector';
import Constants from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';
import vtkSVGRepresentation from 'vtk.js/Sources/Widgets/SVG/SVGRepresentation';
import { diff } from './vdom';
const { ViewTypes, RenderingTypes, CaptureOn } = Constants;
const { vtkErrorMacro } = macro;
const { createSvgElement, createSvgDomElement } = vtkSVGRepresentation;
let viewIdCount = 1;
// ----------------------------------------------------------------------------
// Helper
// ----------------------------------------------------------------------------
export function extractRenderingComponents(renderer) {
const camera = renderer.getActiveCamera();
const renderWindow = renderer.getRenderWindow();
const interactor = renderWindow.getInteractor();
const openGLRenderWindow = interactor.getView();
return { renderer, renderWindow, interactor, openGLRenderWindow, camera };
}
// ----------------------------------------------------------------------------
function createSvgRoot(id) {
const svgRoot = createSvgDomElement('svg');
svgRoot.setAttribute(
'style',
'position: absolute; top: 0; left: 0; width: 100%; height: 100%;'
);
svgRoot.setAttribute('version', '1.1');
svgRoot.setAttribute('baseProfile', 'full');
return svgRoot;
}
// ----------------------------------------------------------------------------
// vtkWidgetManager methods
// ----------------------------------------------------------------------------
function vtkWidgetManager(publicAPI, model) {
if (!model.viewId) {
model.viewId = `view-${viewIdCount++}`;
}
model.classHierarchy.push('vtkWidgetManager');
const propsWeakMap = new WeakMap();
const widgetToSvgMap = new WeakMap();
const svgVTrees = new WeakMap();
const subscriptions = [];
// --------------------------------------------------------------------------
// Internal variable
// --------------------------------------------------------------------------
model.selector = vtkOpenGLHardwareSelector.newInstance();
model.selector.setFieldAssociation(
FieldAssociations.FIELD_ASSOCIATION_POINTS
);
model.svgRoot = createSvgRoot(model.viewId);
// --------------------------------------------------------------------------
// API internal
// --------------------------------------------------------------------------
function updateWidgetWeakMap(widget) {
const representations = widget.getRepresentations();
for (let i = 0; i < representations.length; i++) {
const representation = representations[i];
const origin = { widget, representation };
const actors = representation.getActors();
for (let j = 0; j < actors.length; j++) {
const actor = actors[j];
propsWeakMap.set(actor, origin);
}
}
}
function getViewWidget(widget) {
return (
widget &&
(widget.isA('vtkAbstractWidget')
? widget
: widget.getWidgetForView({ viewId: model.viewId }))
);
}
// --------------------------------------------------------------------------
// internal SVG API
// --------------------------------------------------------------------------
const pendingSvgRenders = new WeakMap();
function enableSvgLayer() {
const container = model.openGLRenderWindow.getReferenceByName('el');
const canvas = model.openGLRenderWindow.getCanvas();
container.insertBefore(model.svgRoot, canvas.nextSibling);
const containerStyles = window.getComputedStyle(container);
if (containerStyles.position === 'static') {
container.style.position = 'relative';
}
}
function disableSvgLayer() {
const container = model.openGLRenderWindow.getReferenceByName('el');
container.removeChild(model.svgRoot);
}
function removeFromSvgLayer(viewWidget) {
const group = widgetToSvgMap.get(viewWidget);
if (group) {
widgetToSvgMap.delete(viewWidget);
svgVTrees.delete(viewWidget);
model.svgRoot.removeChild(group);
}
}
function setSvgSize() {
const [cwidth, cheight] = model.openGLRenderWindow.getSize();
const ratio = window.devicePixelRatio || 1;
const bwidth = String(cwidth / ratio);
const bheight = String(cheight / ratio);
const viewBox = `0 0 ${cwidth} ${cheight}`;
const origWidth = model.svgRoot.getAttribute('width');
const origHeight = model.svgRoot.getAttribute('height');
const origViewBox = model.svgRoot.getAttribute('viewBox');
if (origWidth !== bwidth) {
model.svgRoot.setAttribute('width', bwidth);
}
if (origHeight !== bheight) {
model.svgRoot.setAttribute('height', bheight);
}
if (origViewBox !== viewBox) {
model.svgRoot.setAttribute('viewBox', viewBox);
}
}
function updateSvg() {
if (model.useSvgLayer) {
for (let i = 0; i < model.widgets.length; i++) {
const widget = model.widgets[i];
const svgReps = widget
.getRepresentations()
.filter((r) => r.isA('vtkSVGRepresentation'));
let pendingContent = [];
if (widget.getVisibility()) {
pendingContent = svgReps
.filter((r) => r.getVisibility())
.map((r) => r.render());
}
const promise = Promise.all(pendingContent);
const renders = pendingSvgRenders.get(widget) || [];
renders.push(promise);
pendingSvgRenders.set(widget, renders);
promise.then((vnodes) => {
let pendingRenders = pendingSvgRenders.get(widget) || [];
const idx = pendingRenders.indexOf(promise);
if (model.deleted || idx === -1) {
return;
}
// throw away previous renders
pendingRenders = pendingRenders.slice(idx + 1);
pendingSvgRenders.set(widget, pendingRenders);
const oldVTree = svgVTrees.get(widget);
const newVTree = createSvgElement('g');
for (let ni = 0; ni < vnodes.length; ni++) {
newVTree.appendChild(vnodes[ni]);
}
const widgetGroup = widgetToSvgMap.get(widget);
let node = widgetGroup;
const patchFns = diff(oldVTree, newVTree);
for (let j = 0; j < patchFns.length; j++) {
node = patchFns[j](node);
}
if (!widgetGroup && node) {
// add
model.svgRoot.appendChild(node);
widgetToSvgMap.set(widget, node);
} else if (widgetGroup && !node) {
// delete
widgetGroup.remove();
widgetToSvgMap.delete(widget);
}
svgVTrees.set(widget, newVTree);
});
}
}
}
// --------------------------------------------------------------------------
// Widget scaling
// --------------------------------------------------------------------------
function updateDisplayScaleParams() {
const { openGLRenderWindow, camera, renderer } = model;
if (renderer && openGLRenderWindow && camera) {
const [rwW, rwH] = openGLRenderWindow.getSize();
const [vxmin, vymin, vxmax, vymax] = renderer.getViewport();
const rendererPixelDims = [rwW * (vxmax - vxmin), rwH * (vymax - vymin)];
const cameraPosition = camera.getPosition();
const cameraDir = camera.getDirectionOfProjection();
const isParallel = camera.getParallelProjection();
const dispHeightFactor = isParallel
? camera.getParallelScale()
: 2 * Math.tan(radiansFromDegrees(camera.getViewAngle()) / 2);
model.widgets.forEach((w) => {
w.getNestedProps().forEach((r) => {
if (r.getScaleInPixels()) {
r.setDisplayScaleParams({
dispHeightFactor,
cameraPosition,
cameraDir,
isParallel,
rendererPixelDims,
});
}
});
});
}
}
// --------------------------------------------------------------------------
// API public
// --------------------------------------------------------------------------
function updateWidgetForRender(w) {
w.updateRepresentationForRender(model.renderingType);
}
function renderPickingBuffer() {
model.renderingType = RenderingTypes.PICKING_BUFFER;
model.widgets.forEach(updateWidgetForRender);
}
function renderFrontBuffer() {
model.renderingType = RenderingTypes.FRONT_BUFFER;
model.widgets.forEach(updateWidgetForRender);
}
function captureBuffers(x1, y1, x2, y2) {
renderPickingBuffer();
model.selector.setArea(x1, y1, x2, y2);
model.selector.releasePixBuffers();
model.previousSelectedData = null;
return model.selector.captureBuffers();
}
publicAPI.enablePicking = () => {
model.pickingEnabled = true;
model.pickingAvailable = true;
publicAPI.renderWidgets();
};
publicAPI.renderWidgets = () => {
if (model.pickingEnabled && model.captureOn === CaptureOn.MOUSE_RELEASE) {
const [w, h] = model.openGLRenderWindow.getSize();
model.pickingAvailable = captureBuffers(0, 0, w, h);
}
renderFrontBuffer();
publicAPI.modified();
};
publicAPI.disablePicking = () => {
model.pickingEnabled = false;
model.pickingAvailable = false;
};
publicAPI.setRenderer = (renderer) => {
Object.assign(model, extractRenderingComponents(renderer));
while (subscriptions.length) {
subscriptions.pop().unsubscribe();
}
model.selector.attach(model.openGLRenderWindow, model.renderer);
subscriptions.push(model.interactor.onRenderEvent(updateSvg));
subscriptions.push(model.openGLRenderWindow.onModified(setSvgSize));
setSvgSize();
subscriptions.push(
model.openGLRenderWindow.onModified(updateDisplayScaleParams)
);
subscriptions.push(model.camera.onModified(updateDisplayScaleParams));
updateDisplayScaleParams();
subscriptions.push(
model.interactor.onStartAnimation(() => {
model.isAnimating = true;
})
);
subscriptions.push(
model.interactor.onEndAnimation(() => {
model.isAnimating = false;
publicAPI.renderWidgets();
})
);
subscriptions.push(
model.interactor.onMouseMove(({ position }) => {
if (model.isAnimating || !model.pickingAvailable) {
return;
}
publicAPI.updateSelectionFromXY(position.x, position.y);
const {
requestCount,
selectedState,
representation,
widget,
} = publicAPI.getSelectedData();
if (requestCount) {
// Call activate only once
return;
}
// Default cursor behavior
model.openGLRenderWindow.setCursor(widget ? 'pointer' : 'default');
if (model.widgetInFocus === widget && widget.hasFocus()) {
widget.activateHandle({ selectedState, representation });
// Ken FIXME
model.interactor.render();
model.interactor.render();
} else {
for (let i = 0; i < model.widgets.length; i++) {
const w = model.widgets[i];
if (w === widget && w.getPickable()) {
w.activateHandle({ selectedState, representation });
model.activeWidget = w;
} else {
w.deactivateAllHandles();
}
}
// Ken FIXME
model.interactor.render();
model.interactor.render();
}
})
);
publicAPI.modified();
if (model.pickingEnabled) {
// also sets pickingAvailable
publicAPI.enablePicking();
}
if (model.useSvgLayer) {
enableSvgLayer();
}
};
publicAPI.addWidget = (widget, viewType, initialValues) => {
if (!model.renderer) {
vtkErrorMacro(
'Widget manager MUST BE link to a view before registering widgets'
);
return null;
}
const { viewId, renderer } = model;
const w = widget.getWidgetForView({
viewId,
renderer,
viewType: viewType || ViewTypes.DEFAULT,
initialValues,
});
if (model.widgets.indexOf(w) === -1) {
model.widgets.push(w);
w.setWidgetManager(publicAPI);
updateWidgetWeakMap(w);
// Register to renderer
model.renderer.addActor(w);
publicAPI.modified();
}
return w;
};
function removeWidgetInternal(viewWidget) {
model.renderer.removeActor(viewWidget);
removeFromSvgLayer(viewWidget);
viewWidget.delete();
}
function onWidgetRemoved() {
model.renderer.getRenderWindow().getInteractor().render();
publicAPI.renderWidgets();
}
publicAPI.removeWidgets = () => {
model.widgets.forEach(removeWidgetInternal);
model.widgets = [];
model.widgetInFocus = null;
onWidgetRemoved();
};
publicAPI.removeWidget = (widget) => {
const viewWidget = getViewWidget(widget);
const index = model.widgets.indexOf(viewWidget);
if (index !== -1) {
model.widgets.splice(index, 1);
const isWidgetInFocus = model.widgetInFocus === viewWidget;
if (isWidgetInFocus) {
publicAPI.releaseFocus();
}
removeWidgetInternal(viewWidget);
onWidgetRemoved();
}
};
publicAPI.updateSelectionFromXY = (x, y) => {
if (model.pickingEnabled) {
let pickingAvailable = model.pickingAvailable;
if (model.captureOn === CaptureOn.MOUSE_MOVE) {
pickingAvailable = captureBuffers(x, y, x, y);
renderFrontBuffer();
}
if (pickingAvailable) {
model.selections = model.selector.generateSelection(x, y, x, y);
}
}
};
publicAPI.updateSelectionFromMouseEvent = (event) => {
const { pageX, pageY } = event;
const {
top,
left,
height,
} = model.openGLRenderWindow.getCanvas().getBoundingClientRect();
const x = pageX - left;
const y = height - (pageY - top);
publicAPI.updateSelectionFromXY(x, y);
};
publicAPI.getSelectedData = () => {
if (!model.selections || !model.selections.length) {
model.previousSelectedData = null;
return {};
}
const { propID, compositeID, prop } = model.selections[0].getProperties();
if (
model.previousSelectedData &&
model.previousSelectedData.prop === prop &&
model.previousSelectedData.compositeID === compositeID
) {
model.previousSelectedData.requestCount++;
return model.previousSelectedData;
}
if (!propsWeakMap.has(prop)) {
return {};
}
const { widget, representation } = propsWeakMap.get(prop);
if (widget && representation) {
const selectedState = representation.getSelectedState(prop, compositeID);
model.previousSelectedData = {
requestCount: 0,
propID,
compositeID,
prop,
widget,
representation,
selectedState,
};
return model.previousSelectedData;
}
model.previousSelectedData = null;
return {};
};
publicAPI.grabFocus = (widget) => {
const viewWidget = getViewWidget(widget);
if (model.widgetInFocus && model.widgetInFocus !== viewWidget) {
model.widgetInFocus.loseFocus();
}
model.widgetInFocus = viewWidget;
if (model.widgetInFocus) {
model.widgetInFocus.grabFocus();
}
};
publicAPI.releaseFocus = () => publicAPI.grabFocus(null);
publicAPI.setUseSvgLayer = (useSvgLayer) => {
if (useSvgLayer !== model.useSvgLayer) {
model.useSvgLayer = useSvgLayer;
if (model.renderer) {
if (useSvgLayer) {
enableSvgLayer();
// force a render so svg widgets can be drawn
updateSvg();
} else {
disableSvgLayer();
}
}
return true;
}
return false;
};
const superDelete = publicAPI.delete;
publicAPI.delete = () => {
while (subscriptions.length) {
subscriptions.pop().unsubscribe();
}
superDelete();
};
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
viewId: null,
widgets: [],
renderer: null,
viewType: ViewTypes.DEFAULT,
pickingAvailable: false,
isAnimating: false,
pickingEnabled: true,
selections: null,
previousSelectedData: null,
widgetInFocus: null,
useSvgLayer: true,
captureOn: CaptureOn.MOUSE_MOVE,
};
// ----------------------------------------------------------------------------
export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
macro.obj(publicAPI, model);
macro.setGet(publicAPI, model, [
'captureOn',
{ type: 'enum', name: 'viewType', enum: ViewTypes },
]);
macro.get(publicAPI, model, [
'selections',
'widgets',
'viewId',
'pickingEnabled',
'useSvgLayer',
]);
// Object specific methods
vtkWidgetManager(publicAPI, model);
}
// ----------------------------------------------------------------------------
export const newInstance = macro.newInstance(extend, 'vtkWidgetManager');
// ----------------------------------------------------------------------------
export default { newInstance, extend, Constants };