UNPKG

@acransac/vtk.js

Version:

Visualization Toolkit for the Web

327 lines (291 loc) 9.43 kB
import vtkBoundingBox, { STATIC, } from 'vtk.js/Sources/Common/DataModel/BoundingBox'; import vtkBox from 'vtk.js/Sources/Common/DataModel/Box'; import vtkCubeSource from 'vtk.js/Sources/Filters/Sources/CubeSource'; import vtkCutter from 'vtk.js/Sources/Filters/Core/Cutter'; import vtkPlane from 'vtk.js/Sources/Common/DataModel/Plane'; import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder'; import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants'; const EPSILON = 0.00001; /** * Fit the plane defined by origin, p1, p2 onto the bounds. * Plane is untouched if does not intersect bounds. * @param {Array} bounds * @param {Array} origin * @param {Array} p1 * @param {Array} p2 */ export function boundPlane(bounds, origin, p1, p2) { const v1 = []; vtkMath.subtract(p1, origin, v1); vtkMath.normalize(v1); const v2 = []; vtkMath.subtract(p2, origin, v2); vtkMath.normalize(v2); const n = [0, 0, 1]; vtkMath.cross(v1, v2, n); vtkMath.normalize(n); const plane = vtkPlane.newInstance(); plane.setOrigin(...origin); plane.setNormal(...n); const cubeSource = vtkCubeSource.newInstance(); cubeSource.setBounds(bounds); const cutter = vtkCutter.newInstance(); cutter.setCutFunction(plane); cutter.setInputConnection(cubeSource.getOutputPort()); const cutBounds = cutter.getOutputData(); if (cutBounds.getNumberOfPoints() === 0) { return; } const localBounds = STATIC.computeLocalBounds( cutBounds.getPoints(), v1, v2, n ); for (let i = 0; i < 3; i += 1) { origin[i] = localBounds[0] * v1[i] + localBounds[2] * v2[i] + localBounds[4] * n[i]; p1[i] = localBounds[1] * v1[i] + localBounds[2] * v2[i] + localBounds[4] * n[i]; p2[i] = localBounds[0] * v1[i] + localBounds[3] * v2[i] + localBounds[4] * n[i]; } } // Project point (inPoint) to the bounds of the image according to a plane // defined by two vectors (v1, v2) export function boundPoint(inPoint, v1, v2, bounds) { const absT1 = v1.map((val) => Math.abs(val)); const absT2 = v2.map((val) => Math.abs(val)); let o1 = 0.0; let o2 = 0.0; for (let i = 0; i < 3; i++) { let axisOffset = 0; const useT1 = absT1[i] > absT2[i]; const t = useT1 ? v1 : v2; const absT = useT1 ? absT1 : absT2; if (inPoint[i] < bounds[i * 2]) { axisOffset = absT[i] > EPSILON ? (bounds[2 * i] - inPoint[i]) / t[i] : 0; } else if (inPoint[i] > bounds[2 * i + 1]) { axisOffset = absT[i] > EPSILON ? (bounds[2 * i + 1] - inPoint[i]) / t[i] : 0; } if (useT1) { if (Math.abs(axisOffset) > Math.abs(o1)) { o1 = axisOffset; } } else if (Math.abs(axisOffset) > Math.abs(o2)) { o2 = axisOffset; } } const outPoint = [inPoint[0], inPoint[1], inPoint[2]]; if (o1 !== 0.0) { vtkMath.multiplyAccumulate(outPoint, v1, o1, outPoint); } if (o2 !== 0.0) { vtkMath.multiplyAccumulate(outPoint, v2, o2, outPoint); } return outPoint; } // Compute the intersection between p1 and p2 on bounds export function boundPointOnPlane(p1, p2, bounds) { const dir12 = [0, 0, 0]; vtkMath.subtract(p2, p1, dir12); const out = [0, 0, 0]; const tolerance = [0, 0, 0]; vtkBox.intersectBox(bounds, p1, dir12, out, tolerance); return out; } /** * Rotates a vector around another. * @param {vec3} vectorToBeRotated Vector to rate * @param {vec3} axis Axis to rotate around * @param {Number} angle Angle in radian * @returns The rotated vector */ export function rotateVector(vectorToBeRotated, axis, angle) { const rotatedVector = [...vectorToBeRotated]; vtkMatrixBuilder.buildFromRadian().rotate(angle, axis).apply(rotatedVector); return rotatedVector; } // Update the extremities and the rotation point coordinate of the line function updateLine(lineState, center, axis, lineLength, rotationLength) { const p1 = [ center[0] - lineLength * axis[0], center[1] - lineLength * axis[1], center[2] - lineLength * axis[2], ]; const p2 = [ center[0] + lineLength * axis[0], center[1] + lineLength * axis[1], center[2] + lineLength * axis[2], ]; const rotationP1 = [ center[0] - rotationLength * axis[0], center[1] - rotationLength * axis[1], center[2] - rotationLength * axis[2], ]; const rotationP2 = [ center[0] + rotationLength * axis[0], center[1] + rotationLength * axis[1], center[2] + rotationLength * axis[2], ]; lineState.setPoint1(p1); lineState.setPoint2(p2); lineState.setRotationPoint1(rotationP1); lineState.setRotationPoint2(rotationP2); } // Update the reslice cursor state according to the three planes normals and the origin export function updateState(widgetState) { // Compute line axis const xNormal = widgetState.getPlanes()[ViewTypes.YZ_PLANE].normal; const yNormal = widgetState.getPlanes()[ViewTypes.XZ_PLANE].normal; const zNormal = widgetState.getPlanes()[ViewTypes.XY_PLANE].normal; const yzIntersectionLineAxis = vtkMath.cross(yNormal, zNormal, []); const xzIntersectionLineAxis = vtkMath.cross(zNormal, xNormal, []); const xyIntersectionLineAxis = vtkMath.cross(xNormal, yNormal, []); const bounds = widgetState.getImage().getBounds(); const center = widgetState.getCenter(); // Factor used to define where the rotation point will be displayed // according to the plane size where there will be visible const factor = 0.5 * 0.85; const xRotationLength = (bounds[1] - bounds[0]) * factor; const yRotationLength = (bounds[3] - bounds[2]) * factor; const zRotationLength = (bounds[5] - bounds[4]) * factor; // Length of the principal diagonal. const pdLength = 20 * 0.5 * vtkBoundingBox.getDiagonalLength(bounds); updateLine( widgetState.getAxisXinY(), center, xyIntersectionLineAxis, pdLength, zRotationLength ); updateLine( widgetState.getAxisYinX(), center, xyIntersectionLineAxis, pdLength, zRotationLength ); updateLine( widgetState.getAxisYinZ(), center, yzIntersectionLineAxis, pdLength, xRotationLength ); updateLine( widgetState.getAxisZinY(), center, yzIntersectionLineAxis, pdLength, xRotationLength ); updateLine( widgetState.getAxisXinZ(), center, xzIntersectionLineAxis, pdLength, yRotationLength ); updateLine( widgetState.getAxisZinX(), center, xzIntersectionLineAxis, pdLength, yRotationLength ); } /** * First rotate planeToTransform to match targetPlane normal. * Then rotate around targetNormal to enforce targetViewUp "up" vector (i.e. Origin->p2 ). * There is an infinite number of options to rotate a plane normal to another. Here we attempt to * preserve Origin, P1 and P2 when rotating around targetPlane. * @param {vtkPlaneSource} planeToTransform * @param {vec3} targetOrigin Center of the plane * @param {vec3} targetNormal Normal to state to the plane * @param {vec3} viewType Vector that enforces view up */ export function transformPlane( planeToTransform, targetCenter, targetNormal, targetViewUp ) { planeToTransform.setNormal(targetNormal); const viewUp = vtkMath.subtract( planeToTransform.getPoint2(), planeToTransform.getOrigin(), [] ); const angle = vtkMath.signedAngleBetweenVectors( viewUp, targetViewUp, targetNormal ); planeToTransform.rotate(angle, targetNormal); planeToTransform.setCenter(targetCenter); } // Get name of the line in the same plane as the input export function getAssociatedLinesName(lineName) { switch (lineName) { case 'AxisXinY': return 'AxisZinY'; case 'AxisXinZ': return 'AxisYinZ'; case 'AxisYinX': return 'AxisZinX'; case 'AxisYinZ': return 'AxisXinZ'; case 'AxisZinX': return 'AxisYinX'; case 'AxisZinY': return 'AxisXinY'; default: return ''; } } /** * Get the line name, constructs from the plane name and where the plane is displayed * Example: planeName='X' rotatedPlaneName='Y', then the return values will be 'AxisXinY' * @param {String} planeName Value between 'X', 'Y' and 'Z' * @param {String} rotatedPlaneName Value between 'X', 'Y' and 'Z' * @returns {String} */ export function getLineNameFromPlaneAndRotatedPlaneName( planeName, rotatedPlaneName ) { return `Axis${planeName}in${rotatedPlaneName}`; } /** * Extract the plane name from the line name * Example: 'AxisXinY' will return 'X' * @param {String} lineName Should be following this template : 'Axis_in_' with _ a character * @returns {String} Value between 'X', 'Y' and 'Z' or null if an error occured */ export function getPlaneNameFromLineName(lineName) { const match = lineName.match('([XYZ])in[XYZ]'); if (match) { return match[1]; } return null; } /** * Get the orthogonal plane name of 'planeName' in a specific 'rotatedPlaneName' * Example: planeName='X' on rotatedPlaneName='Z', then the associated plane name * of 'X' plane is 'Y' * @param {String} planeName * @param {String} rotatedPlaneName */ export function getAssociatedPlaneName(planeName, rotatedPlaneName) { const lineName = getLineNameFromPlaneAndRotatedPlaneName( planeName, rotatedPlaneName ); const associatedLine = getAssociatedLinesName(lineName); return getPlaneNameFromLineName(associatedLine); }