WidgetManager

Methods

disablePicking

Disable the picking.

enablePicking

Enable the picking.

extend

Method used to decorate a given object (publicAPI+model) with vtkWidgetManager characteristics.

Argument Type Required Description
publicAPI Yes object on which methods will be bounds (public)
model Yes object on which data structure will be bounds (protected)
initialValues Yes (default: {})

extractRenderingComponents

Extract the rendering components from the given renderer.

Argument Type Required Description
renderer vtkRenderer Yes The vtkRenderer instance.

getActiveWidget

Get the active widget.

If no widget is active, returns null.

getCaptureOn

Get the captureOn value.

getCursorStyles

Retrieves the current cursor styles.

getPickingEnabled

Returns true if picking is enabled, false otherwise.

getPixelWorldHeightAtCoord

This method returns the world distance that corresponds to the height of a
single display pixel at a given coordinate. For example, to determine the
(vertical) distance that matches a display distance of 30px for a coordinate
coord, you would compute 30 * getPixelWorldHeightAtCoord(coord).

getSelectedData

The all currently selected data.

getSelectedDataForXY

Given x and y parameter, get selected data.

Argument Type Required Description
x Number Yes
y Number Yes

getSelections

Get the current selection.

getViewId

Get the view id.

getViewType

Get the view type.

getWidgets

Get all the underlying widgets.

grabFocus

Given the focus to the given widget instance.

Argument Type Required Description
widget vtkAbstractWidget or vtkAbstractWidgetFactory Yes The widget instance which should get the focus.

newInstance

Method used to create a new instance of vtkCellArray

Argument Type Required Description
initialValues Yes for pre-setting some of its content

releaseFocus

Release the focus.

removeWidget

Remove a widget from the widget manager.

Argument Type Required Description
widget vtkAbstractWidget or vtkAbstractWidgetFactory Yes The widget to remove

removeWidgets

Unregister all widgets from the widget manager.

renderWidgets

Renders all the widgets.

setCaptureOn

The the captureOn value.
CaptureOn.MOUSE_MOVE: captures small region when moving mouse
CaptureOn.MOUSE_RELEASE: captures entire region when mouse button is released

Argument Type Required Description
captureOn CaptureOn Yes

setCursorStyles

Sets the default cursor styles.

Known style keys:

  • default: when not interacting with a widget
  • hover: when hovering over a widget.

If a known style key is not present, the cursor style will not be changed.

Argument Type Required Description
cursorStyles Record.<string, string> Yes

setRenderer

Set the renderer.

Argument Type Required Description
renderer vtkRenderer Yes

setViewType

The the view type.

Argument Type Required Description
type ViewTypes Yes

Source

Constants.d.ts
export declare enum ViewTypes {
DEFAULT = 0,
GEOMETRY = 1,
SLICE = 2,
VOLUME = 3,
YZ_PLANE = 4,
XZ_PLANE = 5,
XY_PLANE = 6,
}

export declare enum RenderingTypes {
PICKING_BUFFER = 0,
FRONT_BUFFER = 1,
}

export declare enum CaptureOn {
MOUSE_MOVE = 0,
MOUSE_RELEASE = 1,
}

declare const _default: {
ViewTypes: typeof ViewTypes;
RenderingTypes: typeof RenderingTypes;
CaptureOn: typeof CaptureOn;
};

export default _default;
Constants.js
export const ViewTypes = {
DEFAULT: 0,
GEOMETRY: 1,
SLICE: 2,
VOLUME: 3,
YZ_PLANE: 4, // Sagittal
XZ_PLANE: 5, // Coronal
XY_PLANE: 6, // Axial
};

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

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

export default {
ViewTypes,
RenderingTypes,
CaptureOn,
};
index.d.ts
import vtkAbstractWidget from '../AbstractWidget';
import vtkAbstractWidgetFactory from '../AbstractWidgetFactory';
import vtkCamera from '../../../Rendering/Core/Camera';
import vtkProp from '../../../Rendering/Core/Prop';
import vtkRenderer from '../../../Rendering/Core/Renderer';
import vtkRenderWindow from '../../../Rendering/Core/RenderWindow';
import vtkRenderWindowInteractor from '../../../Rendering/Core/RenderWindowInteractor';
import vtkSelectionNode from '../../../Common/DataModel/SelectionNode';
import vtkWidgetRepresentation from '../../Representations/WidgetRepresentation';
import vtkWidgetState from '../WidgetState';
import { vtkObject } from '../../../interfaces';
import { CaptureOn, ViewTypes } from './Constants';
import { Nullable } from '../../../types';

export interface ISelectedData {
requestCount: number;
propID: number;
compositeID: number;
prop: vtkProp;
widget: vtkAbstractWidget;
representation: vtkWidgetRepresentation;
selectedState: vtkWidgetState;
}

export interface IRenderingComponents {
renderer: vtkRenderer;
renderWindow: vtkRenderWindow;
interactor: vtkRenderWindowInteractor;
apiSpecificRenderWindow: vtkRenderWindow;
camera: vtkCamera;
}

/**
* Extract the rendering components from the given renderer.
*
* @param {vtkRenderer} renderer The vtkRenderer instance.
*/
export function extractRenderingComponents(
renderer: vtkRenderer
): IRenderingComponents;

/**
* This method returns the world distance that corresponds to the height of a
* single display pixel at a given coordinate. For example, to determine the
* (vertical) distance that matches a display distance of 30px for a coordinate
* `coord`, you would compute `30 * getPixelWorldHeightAtCoord(coord)`.
*/
export function getPixelWorldHeightAtCoord(coord: []): Number;

export interface vtkWidgetManager extends vtkObject {
/**
* The the captureOn value.
* `CaptureOn.MOUSE_MOVE`: captures small region when moving mouse
* `CaptureOn.MOUSE_RELEASE`: captures entire region when mouse button is released
*
* @param {CaptureOn} captureOn
*/
setCaptureOn(captureOn: CaptureOn): boolean;

/**
* Get the captureOn value.
*/
getCaptureOn(): CaptureOn;

/**
* The the view type.
*
* @param {ViewTypes} type
*/
setViewType(type: ViewTypes): boolean;

/**
* Get the view type.
*/
getViewType(): ViewTypes;

/**
* Get the current selection.
*/
getSelections(): vtkSelectionNode[];

/**
* Get all the underlying widgets.
*/
getWidgets(): vtkAbstractWidget[];

/**
* Get the active widget.
*
* If no widget is active, returns null.
*/
getActiveWidget(): Nullable<vtkAbstractWidget>;

/**
* Get the view id.
*/
getViewId(): string;

/**
* Returns true if picking is enabled, false otherwise.
*/
getPickingEnabled(): boolean;

/**
* Enable the picking.
*/
enablePicking(): void;

/**
* Renders all the widgets.
*/
renderWidgets(): void;

/**
* Disable the picking.
*/
disablePicking(): void;

/**
* Set the renderer.
*
* @param {vtkRenderer} renderer
*/
setRenderer(renderer: vtkRenderer): void;

/**
* Register a widget on the widget manager instance.
* Please note that one should link the widget manager to a view before calling this method.
*
* @param {vtkAbstractWidgetFactory} widget The abstract widget factory.
* @param {ViewTypes} [viewType]
* @param {Object} [initialValues]
*/
addWidget<WidgetFactory extends vtkAbstractWidgetFactory<any>>(
widget: WidgetFactory,
viewType?: ViewTypes,
initialValues?: object
): WidgetFactory extends vtkAbstractWidgetFactory<infer WidgetInstance>
? WidgetInstance
: never;

/**
* Unregister all widgets from the widget manager.
*/
removeWidgets(): void;

/**
* Remove a widget from the widget manager.
*
* @param {vtkAbstractWidget | vtkAbstractWidgetFactory} widget The widget to remove
*/
removeWidget(widget: vtkAbstractWidget | vtkAbstractWidgetFactory<any>): void;

/**
* Given x and y parameter, get selected data.
*
* @param {Number} x
* @param {Number} y
*/
getSelectedDataForXY(x: number, y: number): Promise<ISelectedData>;

/**
* The all currently selected data.
*/
getSelectedData(): ISelectedData | {};

/**
* Given the focus to the given widget instance.
*
* @param {vtkAbstractWidget | vtkAbstractWidgetFactory} widget The widget instance which should get the focus.
*/
grabFocus(widget: vtkAbstractWidget | vtkAbstractWidgetFactory<any>): void;

/**
* Release the focus.
*/
releaseFocus(): void;

/**
* Sets the default cursor styles.
*
* Known style keys:
* - default: when not interacting with a widget
* - hover: when hovering over a widget.
*
* If a known style key is not present, the cursor style will not be changed.
* @param {Record<string, string>} cursorStyles
*/
setCursorStyles(cursorStyles: Record<string, string>): boolean;

/**
* Retrieves the current cursor styles.
*/
getCursorStyles(): Record<string, string>;
}

export interface IWidgetManagerInitialValues {
captureOn?: CaptureOn;
viewType?: ViewTypes;
pickingEnabled?: boolean;
}

/**
* Method used to decorate a given object (publicAPI+model) with vtkWidgetManager characteristics.
*
* @param publicAPI object on which methods will be bounds (public)
* @param model object on which data structure will be bounds (protected)
* @param initialValues (default: {})
*/
export function extend(
publicAPI: object,
model: object,
initialValues?: IWidgetManagerInitialValues
): vtkWidgetManager;

/**
* Method used to create a new instance of vtkCellArray
*
* @param initialValues for pre-setting some of its content
*/
export function newInstance(
initialValues?: IWidgetManagerInitialValues
): vtkWidgetManager;

export declare const vtkWidgetManager: {
newInstance: typeof newInstance;
extend: typeof extend;
};

export default vtkWidgetManager;
index.js
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
import { FieldAssociations } from 'vtk.js/Sources/Common/DataModel/DataSet/Constants';
import macro from 'vtk.js/Sources/macros';
import Constants from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';
import { WIDGET_PRIORITY } from 'vtk.js/Sources/Widgets/Core/AbstractWidget/Constants';

const { ViewTypes, RenderingTypes, CaptureOn } = Constants;
const { vtkErrorMacro } = macro;

let viewIdCount = 1;

// ----------------------------------------------------------------------------
// Helper
// ----------------------------------------------------------------------------

export function extractRenderingComponents(renderer) {
const camera = renderer.getActiveCamera();
const renderWindow = renderer.getRenderWindow();
const interactor = renderWindow.getInteractor();
const apiSpecificRenderWindow = interactor.getView();
return {
renderer,
renderWindow,
interactor,
apiSpecificRenderWindow,
camera,
};
}

export function getPixelWorldHeightAtCoord(worldCoord, displayScaleParams) {
const {
dispHeightFactor,
cameraPosition,
cameraDir,
isParallel,
rendererPixelDims,
} = displayScaleParams;
let scale = 1;
if (isParallel) {
scale = dispHeightFactor;
} else {
const worldCoordToCamera = [...worldCoord];
vtkMath.subtract(worldCoordToCamera, cameraPosition, worldCoordToCamera);
scale = vtkMath.dot(worldCoordToCamera, cameraDir) * dispHeightFactor;
}

const rHeight = rendererPixelDims[1];
return scale / rHeight;
}
// ----------------------------------------------------------------------------
// vtkWidgetManager methods
// ----------------------------------------------------------------------------

function vtkWidgetManager(publicAPI, model) {
if (!model.viewId) {
model.viewId = `view-${viewIdCount++}`;
}
model.classHierarchy.push('vtkWidgetManager');
const propsWeakMap = new WeakMap();
const subscriptions = [];

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

// --------------------------------------------------------------------------
// Widget scaling
// --------------------------------------------------------------------------

function updateDisplayScaleParams() {
const { _apiSpecificRenderWindow, _camera, _renderer } = model;
if (_renderer && _apiSpecificRenderWindow && _camera) {
const [rwW, rwH] = _apiSpecificRenderWindow.getSize();
const [vxmin, vymin, vxmax, vymax] = _renderer.getViewport();
const pixelRatio = _apiSpecificRenderWindow.getComputedDevicePixelRatio();
const rendererPixelDims = [
(rwW * (vxmax - vxmin)) / pixelRatio,
(rwH * (vymax - vymin)) / pixelRatio,
];

const cameraPosition = _camera.getPosition();
const cameraDir = _camera.getDirectionOfProjection();
const isParallel = _camera.getParallelProjection();
const dispHeightFactor = isParallel
? 2 * _camera.getParallelScale()
: 2 * Math.tan(vtkMath.radiansFromDegrees(_camera.getViewAngle()) / 2);

model.widgets.forEach((w) => {
w.getNestedProps().forEach((r) => {
if (r.getScaleInPixels()) {
r.setDisplayScaleParams({
dispHeightFactor,
cameraPosition,
cameraDir,
isParallel,
rendererPixelDims,
});
}
});
});
}
}

// --------------------------------------------------------------------------
// API public
// --------------------------------------------------------------------------

async function updateSelection(callData, fromTouchEvent, callID) {
const { position } = callData;
const { requestCount, selectedState, representation, widget } =
await publicAPI.getSelectedDataForXY(position.x, position.y);

if (requestCount || callID !== model._currentUpdateSelectionCallID) {
// requestCount > 0: Call activate only once
// callID check: drop old calls
return;
}

function activateHandle(w) {
if (fromTouchEvent) {
// release any previous left button interaction
model._interactor.invokeLeftButtonRelease(callData);
}
w.activateHandle({ selectedState, representation });
if (fromTouchEvent) {
// re-trigger the left button press to pick the now-active widget
model._interactor.invokeLeftButtonPress(callData);
}
}

// Default cursor behavior
const cursorStyles = publicAPI.getCursorStyles();
const style = widget ? 'hover' : 'default';
const cursor = cursorStyles[style];
if (cursor) {
model._apiSpecificRenderWindow.setCursor(cursor);
}

model.activeWidget = null;
let wantRender = false;
if (model.widgetInFocus === widget && widget.hasFocus()) {
activateHandle(widget);
model.activeWidget = widget;
wantRender = true;
} else {
for (let i = 0; i < model.widgets.length; i++) {
const w = model.widgets[i];
if (w === widget && w.getNestedPickable()) {
activateHandle(w);
model.activeWidget = w;
wantRender = true;
} else {
wantRender ||= !!w.getActiveState();
w.deactivateAllHandles();
}
}
}

if (wantRender) {
model._interactor.render();
}
}

const handleEvent = async (callData, fromTouchEvent = false) => {
if (
!model.isAnimating &&
model.pickingEnabled &&
callData.pokedRenderer === model._renderer
) {
const callID = Symbol('UpdateSelection');
model._currentUpdateSelectionCallID = callID;
await updateSelection(callData, fromTouchEvent, callID);
}
};

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

async function captureBuffers(x1, y1, x2, y2) {
if (model._captureInProgress) {
await model._captureInProgress;
return;
}
renderPickingBuffer();

model._capturedBuffers = null;
model._captureInProgress = model._selector.getSourceDataAsync(
model._renderer,
x1,
y1,
x2,
y2
);
model._capturedBuffers = await model._captureInProgress;
model._captureInProgress = null;
model.previousSelectedData = null;
renderFrontBuffer();
}

publicAPI.enablePicking = () => {
model.pickingEnabled = true;
publicAPI.renderWidgets();
};

publicAPI.renderWidgets = () => {
if (model.pickingEnabled && model.captureOn === CaptureOn.MOUSE_RELEASE) {
const [w, h] = model._apiSpecificRenderWindow.getSize();
captureBuffers(0, 0, w, h);
}

renderFrontBuffer();
publicAPI.modified();
};

publicAPI.disablePicking = () => {
model.pickingEnabled = false;
};

publicAPI.setRenderer = (renderer) => {
const renderingComponents = extractRenderingComponents(renderer);
Object.assign(model, renderingComponents);
macro.moveToProtected({}, model, Object.keys(renderingComponents));
while (subscriptions.length) {
subscriptions.pop().unsubscribe();
}

model._selector = model._apiSpecificRenderWindow.createSelector();
model._selector.setFieldAssociation(
FieldAssociations.FIELD_ASSOCIATION_POINTS
);

subscriptions.push(
model._apiSpecificRenderWindow.onWindowResizeEvent(
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((eventData) => {
handleEvent(eventData);
return macro.VOID;
})
);

// must be handled after widgets, hence the given priority.
subscriptions.push(
model._interactor.onLeftButtonPress((eventData) => {
const { deviceType } = eventData;
const touchEvent = deviceType === 'touch' || deviceType === 'pen';
// only try selection if the left button press is from touch.
if (touchEvent) {
handleEvent(eventData, touchEvent);
}
return macro.VOID;
}, WIDGET_PRIORITY / 2)
);

publicAPI.modified();

if (model.pickingEnabled) {
publicAPI.enablePicking();
}
};

function addWidgetInternal(viewWidget) {
viewWidget.setWidgetManager(publicAPI);
updateWidgetWeakMap(viewWidget);
updateDisplayScaleParams();

// Register to renderer
model._renderer.addActor(viewWidget);
}

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: _renderer,
viewType: viewType || ViewTypes.DEFAULT,
initialValues,
});

if (w != null && model.widgets.indexOf(w) === -1) {
model.widgets.push(w);
addWidgetInternal(w);
publicAPI.modified();
}

return w;
};

function removeWidgetInternal(viewWidget) {
model._renderer.removeActor(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.getSelectedDataForXY = async (x, y) => {
model.selections = null;
if (model.pickingEnabled) {
// do we require a new capture?
if (!model._capturedBuffers || model.captureOn === CaptureOn.MOUSE_MOVE) {
await captureBuffers(x, y, x, y);
} else {
// or do we need a pixel that is outside the last capture?
const capturedRegion = model._capturedBuffers.area;
if (
x < capturedRegion[0] ||
x > capturedRegion[2] ||
y < capturedRegion[1] ||
y > capturedRegion[3]
) {
await captureBuffers(x, y, x, y);
}
}

model.selections = model._capturedBuffers.generateSelection(x, y, x, y);
}
return publicAPI.getSelectedData();
};

publicAPI.getSelectedData = () => {
if (!model.selections || !model.selections.length) {
model.previousSelectedData = null;
return {};
}
const { propID, compositeID, prop } = model.selections[0].getProperties();
let { widget, representation } = model.selections[0].getProperties();
// widget is undefined for handle representation.
if (
model.previousSelectedData &&
model.previousSelectedData.prop === prop &&
model.previousSelectedData.widget === widget &&
model.previousSelectedData.compositeID === compositeID
) {
model.previousSelectedData.requestCount++;
return model.previousSelectedData;
}

if (propsWeakMap.has(prop)) {
const props = propsWeakMap.get(prop);
widget = props.widget;
representation = props.representation;
}

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

const superDelete = publicAPI.delete;
publicAPI.delete = () => {
while (subscriptions.length) {
subscriptions.pop().unsubscribe();
}
superDelete();
};
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

const defaultValues = (initialValues = {}) => ({
// _camera: null,
// _selector: null,
// _currentUpdateSelectionCallID: null,
viewId: null,
widgets: [],
activeWidget: null,
renderer: null,
viewType: ViewTypes.DEFAULT,
isAnimating: false,
pickingEnabled: true,
selections: null,
previousSelectedData: null,
widgetInFocus: null,
captureOn: CaptureOn.MOUSE_MOVE,
...initialValues,

cursorStyles: initialValues.cursorStyles
? { ...initialValues.cursorStyles }
: {
default: 'default',
hover: 'pointer',
},
});

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

export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, defaultValues(initialValues));

macro.obj(publicAPI, model);
macro.setGet(publicAPI, model, [
'captureOn',
'cursorStyles',
{ type: 'enum', name: 'viewType', enum: ViewTypes },
]);
macro.get(publicAPI, model, [
'selections',
'widgets',
'viewId',
'pickingEnabled',
'activeWidget',
]);

// Object specific methods
vtkWidgetManager(publicAPI, model);
}

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

export const newInstance = macro.newInstance(extend, 'vtkWidgetManager');

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

export default { newInstance, extend, Constants, getPixelWorldHeightAtCoord };