WidgetManager

vtkWidgetManager manages view widgets for a given renderer.

enablePicking()

Enable widget picking in the renderer.

disablePicking()

Disable widget picking in the renderer.

setRenderer(ren)

Attaches the widget manager to the specified renderer. Note the current
implementation does not allow changing the renderer.

addWidget(widgetFactory, viewType?, initialValues?)

Adds or creates a view widget of a provided view type and initial values.
Internally, this will invoke widgetFactory.getWidgetForView() and attach the
resulting view widget to the view.

removeWidget(widgetFactory)

Removes the view widgets associated with a given widget factory.

grabFocus(widgetFactory)

Grabs the focus of the view widgets associated with the widget factory.

releaseFocus()

Clears focus flag for any focused widgets.

Source

Constants.js
export const ViewTypes = {
DEFAULT: 0,
GEOMETRY: 1,
SLICE: 2,
VOLUME: 3,
};

export const RenderingTypes = {
PICKING_BUFFER: 0,
FRONT_BUFFER: 1,
};

export const CaptureOn = {
MOUSE_MOVE: 0,
MOUSE_RELEASE: 1,
};

export default {
ViewTypes,
RenderingTypes,
CaptureOn,
};
index.js
import macro from 'vtk.js/Sources/macro';
import vtkOpenGLHardwareSelector from 'vtk.js/Sources/Rendering/OpenGL/HardwareSelector';
import { FieldAssociations } from 'vtk.js/Sources/Common/DataModel/DataSet/Constants';
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
// --------------------------------------------------------------------------

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());
}

Promise.all(pendingContent).then((vnodes) => {
if (model.deleted) {
return;
}
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);
});
}
}
}

// --------------------------------------------------------------------------
// 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.interactor.onStartAnimation(() => {
model.isAnimating = true;
})
);
subscriptions.push(
model.interactor.onEndAnimation(() => {
model.isAnimating = false;
if (model.pickingEnabled) {
publicAPI.enablePicking();
}
})
);

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) {
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;
};

publicAPI.removeWidget = (widget) => {
const viewWidget = getViewWidget(widget);
const index = model.widgets.indexOf(viewWidget);
if (index !== -1) {
model.widgets.splice(index, 1);
model.renderer.removeActor(viewWidget);
model.renderer
.getRenderWindow()
.getInteractor()
.render();
publicAPI.enablePicking();

removeFromSvgLayer(viewWidget);

if (model.widgetInFocus === viewWidget) {
publicAPI.releaseFocus();
}

// free internal model + unregister it from its parent
viewWidget.delete();
}
};

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 (useSvgLayer) {
if (model.renderer) {
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: [],
viewType: ViewTypes.DEFAULT,
pickingAvailable: false,
isAnimating: false,
pickingEnabled: true,
selections: null,
previousSelectedData: null,
widgetInFocus: null,
useSvgLayer: true,
captureOn: CaptureOn.MOUSE_RELEASE,
};

// ----------------------------------------------------------------------------

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 };
vdom.js
const SVG_XMLNS = 'http://www.w3.org/2000/svg';

function attrDelta(oldObj, newObj) {
const set = [];
const remove = [];
const oldKeysArray = Object.keys(oldObj);
const newKeysArray = Object.keys(newObj);
const oldKeys = new Set(oldKeysArray);
const newKeys = new Set(newKeysArray);
for (let i = 0; i < oldKeysArray.length; i++) {
const key = oldKeysArray[i];
if (newKeys.has(key)) {
if (oldObj[key] !== newObj[key]) {
set.push([key, newObj[key]]);
}
} else {
remove.push(key);
}
}
for (let i = 0; i < newKeysArray.length; i++) {
const key = newKeysArray[i];
if (!oldKeys.has(key)) {
set.push([key, newObj[key]]);
}
}

return [set, remove];
}

export function render(vnode) {
const node = document.createElementNS(SVG_XMLNS, vnode.name);

const keys = Object.keys(vnode.attrs);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
node.setAttribute(key, vnode.attrs[key]);
}

if (vnode.textContent) {
node.textContent = vnode.textContent;
} else {
for (let i = 0; i < vnode.children.length; i++) {
node.appendChild(render(vnode.children[i]));
}
}

return node;
}

/**
* Returns a set of patch functions to be applied to a document node.
*
* Patch functions must return the effective result node.
*/
export function diff(oldVTree, newVTree) {
if (newVTree.textContent !== null && newVTree.children.length) {
throw new Error('Tree cannot have both children and textContent!');
}

if (!oldVTree) {
return [() => render(newVTree)];
}

if (!newVTree) {
return [(node) => node.remove()];
}

if (oldVTree.name !== newVTree.name) {
return [
(node) => {
const newNode = render(newVTree);
node.replaceWith(newNode);
return newNode;
},
];
}

const patchFns = [];

const [attrsSet, attrsRemove] = attrDelta(oldVTree.attrs, newVTree.attrs);
if (attrsSet.length || attrsRemove.length) {
patchFns.push((node) => {
for (let i = 0; i < attrsSet.length; i++) {
const [name, value] = attrsSet[i];
node.setAttribute(name, value);
}
for (let i = 0; i < attrsRemove.length; i++) {
const name = attrsRemove[i];
node.removeAttribute(name);
}
return node;
});
}

if (
oldVTree.textContent !== newVTree.textContent &&
newVTree.textContent !== null
) {
patchFns.push((node) => {
node.textContent = newVTree.textContent;
return node;
});
}

if (newVTree.textContent === null) {
const min = Math.min(oldVTree.children.length, newVTree.children.length);
for (let i = 0; i < min; i++) {
const childPatches = diff(oldVTree.children[i], newVTree.children[i]);
patchFns.push((node) => {
for (let p = 0; p < childPatches.length; p++) {
childPatches[p](node.children[i]);
}
return node;
});
}
if (oldVTree.children.length < newVTree.children.length) {
for (let i = min; i < newVTree.children.length; i++) {
patchFns.push((node) => {
node.appendChild(render(newVTree.children[i]));
return node;
});
}
} else {
// always delete nodes in reverse
for (let i = oldVTree.children.length - 1; i >= min; i--) {
patchFns.push((node) => {
node.children[i].remove();
return node;
});
}
}
}

return patchFns;
}