UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

479 lines (455 loc) 15.6 kB
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 };