UNPKG

@thewtex/vtk.js-esm

Version:

Visualization Toolkit for the Web

421 lines (341 loc) 18.6 kB
import _toConsumableArray from '@babel/runtime/helpers/toConsumableArray'; import macro from '../../macro.js'; import vtkAbstractWidgetFactory from '../Core/AbstractWidgetFactory.js'; import vtkPlane from '../../Common/DataModel/Plane.js'; import vtkPlaneSource from '../../Filters/Sources/PlaneSource.js'; import vtkResliceCursorContextRepresentation from '../Representations/ResliceCursorContextRepresentation.js'; import { f as distance2BetweenPoints, Q as multiplyAccumulate, g as subtract, l as normalize, t as multiplyScalar, k as add } from '../../Common/Core/Math/index.js'; import widgetBehavior from './ResliceCursorWidget/behavior.js'; import generateState from './ResliceCursorWidget/state.js'; import { updateState, transformPlane, boundPlane } from './ResliceCursorWidget/helpers.js'; import { ViewTypes } from '../Core/WidgetManager/Constants.js'; import vtkMatrixBuilder from '../../Common/Core/MatrixBuilder.js'; import { i as identity, j as transpose } from '../../vendor/gl-matrix/esm/mat4.js'; import { t as transformMat4 } from '../../vendor/gl-matrix/esm/vec4.js'; var VTK_INT_MAX = 2147483647; var vtkErrorMacro = macro.vtkErrorMacro; // ---------------------------------------------------------------------------- // Factory // ---------------------------------------------------------------------------- function vtkResliceCursorWidget(publicAPI, model) { model.classHierarchy.push('vtkResliceCursorWidget'); // -------------------------------------------------------------------------- // Private methods // -------------------------------------------------------------------------- /** * Compute the origin of the reslice plane prior to transformations * It does not take into account the current view normal. (always axis aligned) * @param {*} viewType axial, coronal or sagittal */ function computeReslicePlaneOrigin(viewType) { var bounds = model.widgetState.getImage().getBounds(); var center = publicAPI.getWidgetState().getCenter(); var imageCenter = model.widgetState.getImage().getCenter(); // Offset based on the center of the image and how far from it the // reslice cursor is. This allows us to capture the whole image even // if we resliced in awkward places. var offset = []; for (var i = 0; i < 3; i++) { offset[i] = -Math.abs(center[i] - imageCenter[i]); offset[i] *= 2; // give us room } // Now set the size of the plane based on the location of the cursor so as to // at least completely cover the viewed region var planeSource = vtkPlaneSource.newInstance(); if (viewType === ViewTypes.XZ_PLANE) { planeSource.setOrigin(bounds[0] + offset[0], center[1], bounds[4] + offset[2]); planeSource.setPoint1(bounds[1] - offset[0], center[1], bounds[4] + offset[2]); planeSource.setPoint2(bounds[0] + offset[0], center[1], bounds[5] - offset[2]); } else if (viewType === ViewTypes.XY_PLANE) { planeSource.setOrigin(bounds[0] + offset[0], bounds[2] + offset[1], center[2]); planeSource.setPoint1(bounds[1] - offset[0], bounds[2] + offset[1], center[2]); planeSource.setPoint2(bounds[0] + offset[0], bounds[3] - offset[1], center[2]); } else if (viewType === ViewTypes.YZ_PLANE) { planeSource.setOrigin(center[0], bounds[2] + offset[1], bounds[4] + offset[2]); planeSource.setPoint1(center[0], bounds[3] - offset[1], bounds[4] + offset[2]); planeSource.setPoint2(center[0], bounds[2] + offset[1], bounds[5] - offset[2]); } return planeSource; } /** * Compute the offset between display reslice cursor position and * display focal point position * This will be used to keep the same offset between reslice cursor * center and focal point when needed. */ function computeFocalPointOffsetFromResliceCursorCenter(viewType, renderer) { var worldFocalPoint = renderer.getActiveCamera().getFocalPoint(); var worldResliceCenter = model.widgetState.getCenter(); var view = renderer.getRenderWindow().getViews()[0]; var dims = view.getViewportSize(renderer); var aspect = dims[0] / dims[1]; var displayFocalPoint = renderer.worldToNormalizedDisplay.apply(renderer, _toConsumableArray(worldFocalPoint).concat([aspect])); var displayResliceCenter = renderer.worldToNormalizedDisplay.apply(renderer, _toConsumableArray(worldResliceCenter).concat([aspect])); var newOffset = subtract(displayFocalPoint, displayResliceCenter, [0, 0, 0]); var cameraOffsets = model.widgetState.getCameraOffsets(); cameraOffsets[viewType] = newOffset; model.widgetState.setCameraOffsets(cameraOffsets); } function updateCamera(renderer, normal, viewType, resetFocalPoint, keepCenterFocalDistance) { // When the reslice plane is changed, update the camera to look at the // normal to the reslice plane. var focalPoint = renderer.getActiveCamera().getFocalPoint(); var distance = renderer.getActiveCamera().getDistance(); var estimatedCameraPosition = multiplyAccumulate(focalPoint, normal, distance, [0, 0, 0]); var newFocalPoint = focalPoint; if (resetFocalPoint) { // intersect with the plane to get updated focal point var intersection = vtkPlane.intersectWithLine(focalPoint, estimatedCameraPosition, model.widgetState.getCenter(), // reslice cursor center normal); newFocalPoint = intersection.x; } // Update the estimated focal point so that it will be at the same // distance from the reslice center if (keepCenterFocalDistance) { var worldResliceCenter = model.widgetState.getCenter(); var view = renderer.getRenderWindow().getViews()[0]; var dims = view.getViewportSize(renderer); var aspect = dims[0] / dims[1]; var displayResliceCenter = renderer.worldToNormalizedDisplay.apply(renderer, _toConsumableArray(worldResliceCenter).concat([aspect])); var realOffset = model.widgetState.getCameraOffsets()[viewType]; var displayFocal = add(displayResliceCenter, realOffset, [0, 0, 0]); var worldFocal = renderer.normalizedDisplayToWorld.apply(renderer, _toConsumableArray(displayFocal).concat([aspect])); // Reproject focal point on slice in order to keep it on the // same plane as the reslice cursor center var intersection2 = vtkPlane.intersectWithLine(worldFocal, estimatedCameraPosition, worldResliceCenter, normal); newFocalPoint[0] = intersection2.x[0]; newFocalPoint[1] = intersection2.x[1]; newFocalPoint[2] = intersection2.x[2]; } renderer.getActiveCamera().setFocalPoint(newFocalPoint[0], newFocalPoint[1], newFocalPoint[2]); var newCameraPosition = multiplyAccumulate(newFocalPoint, normal, distance, [0, 0, 0]); renderer.getActiveCamera().setPosition(newCameraPosition[0], newCameraPosition[1], newCameraPosition[2]); // Don't clip away any part of the data. // Renderer may not have yet actor bounds var bounds = model.widgetState.getImage().getBounds(); if (resetFocalPoint) { renderer.resetCamera(bounds); } renderer.resetCameraClippingRange(bounds); } // -------------------------------------------------------------------------- // initialization // -------------------------------------------------------------------------- model.behavior = widgetBehavior; model.widgetState = generateState(); publicAPI.getRepresentationsForViewType = function (viewType) { switch (viewType) { case ViewTypes.XY_PLANE: return [{ builder: vtkResliceCursorContextRepresentation, labels: ['AxisXinZ', 'AxisYinZ'], initialValues: { axis1Name: 'AxisXinZ', axis2Name: 'AxisYinZ', viewType: ViewTypes.XY_PLANE, rotationEnabled: model.widgetState.getEnableRotation() } }]; case ViewTypes.XZ_PLANE: return [{ builder: vtkResliceCursorContextRepresentation, labels: ['AxisXinY', 'AxisZinY'], initialValues: { axis1Name: 'AxisXinY', axis2Name: 'AxisZinY', viewType: ViewTypes.XZ_PLANE, rotationEnabled: model.widgetState.getEnableRotation() } }]; case ViewTypes.YZ_PLANE: return [{ builder: vtkResliceCursorContextRepresentation, labels: ['AxisYinX', 'AxisZinX'], initialValues: { axis1Name: 'AxisYinX', axis2Name: 'AxisZinX', viewType: ViewTypes.YZ_PLANE, rotationEnabled: model.widgetState.getEnableRotation() } }]; case ViewTypes.DEFAULT: case ViewTypes.GEOMETRY: case ViewTypes.SLICE: case ViewTypes.VOLUME: default: return []; } }; publicAPI.setImage = function (image) { model.widgetState.setImage(image); var center = image.getCenter(); model.widgetState.setCenter(center); updateState(model.widgetState); }; publicAPI.setCenter = function (center) { model.widgetState.setCenter(center); updateState(model.widgetState); publicAPI.modified(); }; // -------------------------------------------------------------------------- // Methods // -------------------------------------------------------------------------- publicAPI.updateCameraPoints = function (renderer, viewType, resetFocalPoint, keepCenterFocalDistance, computeFocalPointOffset) { publicAPI.resetCamera(renderer, viewType, resetFocalPoint, keepCenterFocalDistance); if (computeFocalPointOffset) { computeFocalPointOffsetFromResliceCursorCenter(viewType, renderer); } }; /** * * @param {*} renderer * @param {*} viewType * @param {*} resetFocalPoint Defines if the focal point is reset to the image center * @param {*} keepCenterFocalDistance Defines if the estimated focal point has to be updated * in order to keep the same distance to the center (according to the computed focal point * shift) */ publicAPI.resetCamera = function (renderer, viewType, resetFocalPoint, keepCenterFocalDistance) { var _renderer$getActiveCa, _renderer$getActiveCa2; var center = model.widgetState.getImage().getCenter(); var focalPoint = renderer.getActiveCamera().getFocalPoint(); var position = renderer.getActiveCamera().getPosition(); // Distance is preserved var distance = Math.sqrt(distance2BetweenPoints(position, focalPoint)); var normal = publicAPI.getPlaneNormalFromViewType(viewType); // ResetFocalPoint will reset focal point to the center of the image var estimatedFocalPoint = resetFocalPoint ? center : focalPoint; var estimatedCameraPosition = multiplyAccumulate(estimatedFocalPoint, normal, distance, [0, 0, 0]); (_renderer$getActiveCa = renderer.getActiveCamera()).setFocalPoint.apply(_renderer$getActiveCa, _toConsumableArray(estimatedFocalPoint)); (_renderer$getActiveCa2 = renderer.getActiveCamera()).setPosition.apply(_renderer$getActiveCa2, _toConsumableArray(estimatedCameraPosition)); renderer.getActiveCamera().setViewUp(model.widgetState.getPlanes()[viewType].viewUp); // Project focalPoint onto image plane and preserve distance updateCamera(renderer, normal, viewType, resetFocalPoint, keepCenterFocalDistance); }; publicAPI.updateReslicePlane = function (imageReslice, viewType) { // Calculate appropriate pixel spacing for the reslicing var spacing = model.widgetState.getImage().getSpacing(); // Compute original (i.e. before rotation) plane (i.e. origin, p1, p2) // centered on cursor center. var planeSource = computeReslicePlaneOrigin(viewType); var _model$widgetState$ge = model.widgetState.getPlanes()[viewType], normal = _model$widgetState$ge.normal, viewUp = _model$widgetState$ge.viewUp; // Adapt plane orientation in order to fit the correct viewUp // so that the rotations will be more understandable than now. transformPlane(planeSource, model.widgetState.getCenter(), normal, viewUp); // Clip to bounds var boundedOrigin = _toConsumableArray(planeSource.getOrigin()); var boundedP1 = _toConsumableArray(planeSource.getPoint1()); var boundedP2 = _toConsumableArray(planeSource.getPoint2()); boundPlane(model.widgetState.getImage().getBounds(), boundedOrigin, boundedP1, boundedP2); planeSource.setOrigin.apply(planeSource, _toConsumableArray(boundedOrigin)); planeSource.setPoint1.apply(planeSource, _toConsumableArray(boundedP1)); planeSource.setPoint2.apply(planeSource, _toConsumableArray(boundedP2)); var o = planeSource.getOrigin(); var p1 = planeSource.getPoint1(); var planeAxis1 = []; subtract(p1, o, planeAxis1); var p2 = planeSource.getPoint2(); var planeAxis2 = []; subtract(p2, o, planeAxis2); // The x,y dimensions of the plane var planeSizeX = normalize(planeAxis1); var planeSizeY = normalize(planeAxis2); var newResliceAxes = identity(new Float64Array(16)); for (var i = 0; i < 3; i++) { newResliceAxes[4 * i + 0] = planeAxis1[i]; newResliceAxes[4 * i + 1] = planeAxis2[i]; newResliceAxes[4 * i + 2] = normal[i]; } var spacingX = Math.abs(planeAxis1[0] * spacing[0]) + Math.abs(planeAxis1[1] * spacing[1]) + Math.abs(planeAxis1[2] * spacing[2]); var spacingY = Math.abs(planeAxis2[0] * spacing[0]) + Math.abs(planeAxis2[1] * spacing[1]) + Math.abs(planeAxis2[2] * spacing[2]); var planeOrigin = [].concat(_toConsumableArray(planeSource.getOrigin()), [1.0]); var originXYZW = []; var newOriginXYZW = []; transformMat4(originXYZW, planeOrigin, newResliceAxes); transpose(newResliceAxes, newResliceAxes); transformMat4(newOriginXYZW, originXYZW, newResliceAxes); newResliceAxes[4 * 3 + 0] = newOriginXYZW[0]; newResliceAxes[4 * 3 + 1] = newOriginXYZW[1]; newResliceAxes[4 * 3 + 2] = newOriginXYZW[2]; // Compute a new set of resliced extents var extentX = 0; var extentY = 0; // Pad extent up to a power of two for efficient texture mapping // make sure we're working with valid values var realExtentX = spacingX === 0 ? Number.MAX_SAFE_INTEGER : planeSizeX / spacingX; // Sanity check the input data: // * if realExtentX is too large, extentX will wrap // * if spacingX is 0, things will blow up. var value = VTK_INT_MAX >> 1; // eslint-disable-line no-bitwise if (realExtentX > value) { vtkErrorMacro('Invalid X extent: ', realExtentX, ' on view type : ', viewType); extentX = 0; } else { extentX = 1; while (extentX < realExtentX) { extentX <<= 1; // eslint-disable-line no-bitwise } } // make sure extentY doesn't wrap during padding var realExtentY = spacingY === 0 ? Number.MAX_SAFE_INTEGER : planeSizeY / spacingY; if (realExtentY > value) { vtkErrorMacro('Invalid Y extent:', realExtentY, ' on view type : ', viewType); extentY = 0; } else { extentY = 1; while (extentY < realExtentY) { extentY <<= 1; // eslint-disable-line no-bitwise } } var outputSpacingX = extentX === 0 ? 1.0 : planeSizeX / extentX; var outputSpacingY = extentY === 0 ? 1.0 : planeSizeY / extentY; var modified = imageReslice.setResliceAxes(newResliceAxes); modified = imageReslice.setOutputSpacing([outputSpacingX, outputSpacingY, 1]) || modified; modified = imageReslice.setOutputOrigin([0.5 * outputSpacingX, 0.5 * outputSpacingY, 0]) || modified; modified = imageReslice.setOutputExtent([0, extentX - 1, 0, extentY - 1, 0, 0]) || modified; return { modified: modified, origin: o, point1: p1, point2: p2 }; }; /** * Returns a plane source with origin at cursor center and * normal from the view. * @param {ViewType} type: Axial, Coronal or Sagittal */ publicAPI.getPlaneSourceFromViewType = function (type) { var planeSource = vtkPlaneSource.newInstance(); var origin = publicAPI.getWidgetState().getCenter(); var planeNormal = publicAPI.getPlaneNormalFromViewType(type); planeSource.setNormal(planeNormal); planeSource.setOrigin(origin); return planeSource; }; publicAPI.getPlaneNormalFromViewType = function (viewType) { return publicAPI.getWidgetState().getPlanes()[viewType].normal; }; /** * Returns the normals of the planes that are not viewType. * @param {ViewType} viewType ViewType to extract other normals */ publicAPI.getOtherPlaneNormals = function (viewType) { return [ViewTypes.YZ_PLANE, ViewTypes.XZ_PLANE, ViewTypes.XY_PLANE].filter(function (vt) { return vt !== viewType; }).map(function (vt) { return publicAPI.getPlaneNormalFromViewType(vt); }); }; /** * Return the reslice cursor matrix built as such: [YZ, XZ, XY, center] */ publicAPI.getResliceMatrix = function () { var _vtkMatrixBuilder$bui, _vtkMatrixBuilder$bui2; var resliceMatrix = identity(new Float64Array(16)); for (var i = 0; i < 3; i++) { resliceMatrix[4 * i + 0] = publicAPI.getPlaneNormalFromViewType(ViewTypes.YZ_PLANE)[i]; resliceMatrix[4 * i + 1] = publicAPI.getPlaneNormalFromViewType(ViewTypes.XZ_PLANE)[i]; resliceMatrix[4 * i + 2] = publicAPI.getPlaneNormalFromViewType(ViewTypes.XY_PLANE)[i]; } var origin = publicAPI.getWidgetState().getCenter(); var m = (_vtkMatrixBuilder$bui = (_vtkMatrixBuilder$bui2 = vtkMatrixBuilder.buildFromRadian()).translate.apply(_vtkMatrixBuilder$bui2, _toConsumableArray(origin)).multiply(resliceMatrix)).translate.apply(_vtkMatrixBuilder$bui, _toConsumableArray(multiplyScalar(_toConsumableArray(origin), -1))).getMatrix(); return m; }; } // ---------------------------------------------------------------------------- var DEFAULT_VALUES = {}; // ---------------------------------------------------------------------------- function extend(publicAPI, model) { var initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; Object.assign(model, DEFAULT_VALUES, initialValues); vtkAbstractWidgetFactory.extend(publicAPI, model, initialValues); vtkResliceCursorWidget(publicAPI, model); } // ---------------------------------------------------------------------------- var newInstance = macro.newInstance(extend, 'vtkResliceCursorWidget'); // ---------------------------------------------------------------------------- var vtkResliceCursorWidget$1 = { newInstance: newInstance, extend: extend }; export default vtkResliceCursorWidget$1; export { extend, newInstance };