@kitware/vtk.js
Version:
Visualization Toolkit for the Web
479 lines (455 loc) • 15.6 kB
JavaScript
import { b as vtkMath } from '../../Common/Core/Math/index.js';
import { FieldAssociations } from '../../Common/DataModel/DataSet/Constants.js';
import { m as macro } from '../../macros2.js';
import WidgetManagerConst from './WidgetManager/Constants.js';
import { WIDGET_PRIORITY } from './AbstractWidget/Constants.js';
const {
ViewTypes,
RenderingTypes,
CaptureOn
} = WidgetManagerConst;
const {
vtkErrorMacro
} = macro;
let viewIdCount = 1;
// ----------------------------------------------------------------------------
// Helper
// ----------------------------------------------------------------------------
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
};
}
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 deactivateAllWidgets = () => {
let wantRender = false;
for (let i = 0; i < model.widgets.length; i++) {
const w = model.widgets[i];
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);
} else {
deactivateAllWidgets();
}
};
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'
}
});
// ----------------------------------------------------------------------------
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);
}
// ----------------------------------------------------------------------------
const newInstance = macro.newInstance(extend, 'vtkWidgetManager');
// ----------------------------------------------------------------------------
var vtkWidgetManager$1 = {
newInstance,
extend,
Constants: WidgetManagerConst,
getPixelWorldHeightAtCoord
};
export { vtkWidgetManager$1 as default, extend, extractRenderingComponents, getPixelWorldHeightAtCoord, newInstance };