UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

243 lines (228 loc) 10.6 kB
import vtkBoundingBox, { STATIC } from '../../../Common/DataModel/BoundingBox.js'; import vtkCubeSource from '../../../Filters/Sources/CubeSource.js'; import vtkCutter from '../../../Filters/Core/Cutter.js'; import vtkPlane from '../../../Common/DataModel/Plane.js'; import { s as subtract, l as normalize, j as cross, w as multiplyScalar, m as multiplyAccumulate, X as signedAngleBetweenVectors } from '../../../Common/Core/Math/index.js'; import vtkMatrixBuilder from '../../../Common/Core/MatrixBuilder.js'; import { viewTypeToPlaneName, planeNameToViewType, planeNames } from './Constants.js'; const EPSILON = 10e-7; /** * 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 * @return {Boolean} false if no bounds have been found, else true */ function boundPlane(bounds, origin, p1, p2) { const v1 = []; subtract(p1, origin, v1); normalize(v1); const v2 = []; subtract(p2, origin, v2); normalize(v2); const n = [0, 0, 1]; cross(v1, v2, n); normalize(n); // Inflate bounds in order to avoid precision error when cutting cubesource const inflatedBounds = [...bounds]; const eps = [...n]; multiplyScalar(eps, EPSILON); vtkBoundingBox.addBounds(inflatedBounds, bounds[0] + eps[0], bounds[1] + eps[0], bounds[2] + eps[1], bounds[3] + eps[1], bounds[4] + eps[2], bounds[5] + eps[2]); vtkBoundingBox.addBounds(inflatedBounds, bounds[0] - eps[0], bounds[1] - eps[0], bounds[2] - eps[1], bounds[3] - eps[1], bounds[4] - eps[2], bounds[5] - eps[2]); const plane = vtkPlane.newInstance(); plane.setOrigin(...origin); plane.setNormal(...n); const cubeSource = vtkCubeSource.newInstance(); cubeSource.setBounds(inflatedBounds); const cutter = vtkCutter.newInstance(); cutter.setCutFunction(plane); cutter.setInputConnection(cubeSource.getOutputPort()); const cutBounds = cutter.getOutputData(); if (cutBounds.getNumberOfPoints() === 0) { return false; } 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]; } return true; } // Project point (inPoint) to the bounds of the image according to a plane // defined by two vectors (v1, v2) 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) { multiplyAccumulate(outPoint, v1, o1, outPoint); } if (o2 !== 0.0) { multiplyAccumulate(outPoint, v2, o2, outPoint); } return outPoint; } // Compute the intersection between p1 and p2 on bounds function boundPointOnPlane(p1, p2, bounds) { const dir12 = [0, 0, 0]; subtract(p2, p1, dir12); const out = [0, 0, 0]; const tolerance = [0, 0, 0]; vtkBoundingBox.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 */ function rotateVector(vectorToBeRotated, axis, angle) { const rotatedVector = [...vectorToBeRotated]; vtkMatrixBuilder.buildFromRadian().rotate(angle, axis).apply(rotatedVector); return rotatedVector; } /** * Return ['X'] if there are only 1 plane defined in the widget state. * Return ['X', 'Y'] if there are only 2 planes defined in the widget state. * Return ['X', 'Y', 'Z'] if there are 3 planes defined in the widget state. * @param {object} widgetState the state of the widget * @returns An array of plane names */ function getPlaneNames(widgetState) { return Object.keys(widgetState.getPlanes()).map(viewType => viewTypeToPlaneName[viewType]); } /** * Return X if lineName == XinY|XinZ, Y if lineName == YinX|YinZ and Z otherwise * @param {string} lineName name of the line (YinX, ZinX, XinY, ZinY, XinZ, YinZ) */ function getLinePlaneName(lineName) { return lineName[0]; } /** * Return X if lineName == YinX|ZinX, Y if lineName == XinY|ZinY and Z otherwise * @param {string} lineName name of the line (YinX, ZinX, XinY, ZinY, XinZ, YinZ) */ function getLineInPlaneName(lineName) { return lineName[3]; } /** * Returns ['XinY', 'YinX'] if planes == ['X', 'Y'] * ['XinY', 'XinZ', 'YinX', 'YinZ', 'ZinX', 'ZinY'] if planes == ['X', 'Y', 'Z'] * @param {string} planes name of the planes (e.g. ['X', 'Y']) */ function getPlanesLineNames() { let planes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : planeNames; const lines = []; planes.forEach(plane => { planes.forEach(inPlane => { if (plane !== inPlane) { lines.push(`${plane}in${inPlane}`); } }); }); return lines; } function getLineNames(widgetState) { const planes = Object.keys(widgetState.getPlanes()).map(viewType => viewTypeToPlaneName[viewType]); return getPlanesLineNames(planes); } /** * Return ZinX if lineName == YinX, YinX if lineName == ZinX, ZinY if lineName == XinY... * @param {string} lineName name of the line (YinX, ZinX, XinY, ZinY, XinZ, YinZ) */ function getOtherLineName(widgetState, lineName) { const linePlaneName = getLinePlaneName(lineName); const lineInPlaneName = getLineInPlaneName(lineName); const otherLineName = getPlaneNames(widgetState).find(planeName => planeName !== linePlaneName && planeName !== lineInPlaneName); return `${otherLineName}in${lineInPlaneName}`; } // Compute the offset of the rotation handle origin function computeRotationHandleOriginOffset(axis, rotationHandlePosition, volumeDiagonalLength, scaleInPixels) { // FIXME: p1 and p2 could be placed on the exact boundaries of the volume. return multiplyScalar([...axis], rotationHandlePosition * (scaleInPixels ? 1 : volumeDiagonalLength) / 2); } // Update the reslice cursor state according to the three planes normals and the origin function updateState(widgetState, scaleInPixels, rotationHandlePosition) { const planes = Object.keys(widgetState.getPlanes()).map(viewType => viewTypeToPlaneName[viewType]); // Generates an object as such: // axes = {'XY': cross(X, Y), 'YX': cross(X, Y), 'YZ': cross(Y, Z)...} const axes = planes.reduce((res, plane) => { planes.filter(otherPlane => plane !== otherPlane).forEach(otherPlane => { const cross$1 = cross(widgetState.getPlanes()[planeNameToViewType[plane]].normal, widgetState.getPlanes()[planeNameToViewType[otherPlane]].normal, []); res[`${plane}${otherPlane}`] = cross$1; res[`${otherPlane}${plane}`] = cross$1; }); return res; }, {}); const bounds = widgetState.getImage().getBounds(); const center = widgetState.getCenter(); // Length of the principal diagonal. const pdLength = vtkBoundingBox.getDiagonalLength(bounds); widgetState.getCenterHandle().setOrigin(center); getPlanesLineNames(planes).forEach(lineName => { const planeName = getLinePlaneName(lineName); const inPlaneName = getLineInPlaneName(lineName); const direction = axes[`${planeName}${inPlaneName}`]; widgetState[`getRotationHandle${lineName}0`]().setOrigin(center); widgetState[`getRotationHandle${lineName}0`]().getManipulator()?.setHandleOrigin(center); widgetState[`getRotationHandle${lineName}0`]().getManipulator()?.setHandleNormal(widgetState.getPlanes()[planeNameToViewType[planeName]].normal); widgetState[`getRotationHandle${lineName}0`]().setOffset(computeRotationHandleOriginOffset(direction, rotationHandlePosition, pdLength, scaleInPixels)); widgetState[`getRotationHandle${lineName}1`]().setOrigin(center); widgetState[`getRotationHandle${lineName}1`]().getManipulator()?.setHandleOrigin(center); widgetState[`getRotationHandle${lineName}1`]().getManipulator()?.setHandleNormal(widgetState.getPlanes()[planeNameToViewType[planeName]].normal); widgetState[`getRotationHandle${lineName}1`]().setOffset(computeRotationHandleOriginOffset(direction, -rotationHandlePosition, pdLength, scaleInPixels)); const lineHandle = widgetState[`getAxis${lineName}`](); lineHandle.setOrigin(center); lineHandle.getManipulator()?.setHandleOrigin(center); lineHandle.getManipulator()?.setHandleNormal(widgetState.getPlanes()[planeNameToViewType[planeName]].normal); normalize(direction); const right = widgetState.getPlanes()[planeNameToViewType[inPlaneName]].normal; const up = cross(direction, right, []); lineHandle.setRight(right); lineHandle.setUp(up); lineHandle.setDirection(direction); }); } /** * 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 */ function transformPlane(planeToTransform, targetCenter, targetNormal, targetViewUp) { planeToTransform.setNormal(targetNormal); const viewUp = subtract(planeToTransform.getPoint2(), planeToTransform.getOrigin(), []); const angle = signedAngleBetweenVectors(viewUp, targetViewUp, targetNormal); planeToTransform.rotate(angle, targetNormal); planeToTransform.setCenter(targetCenter); } export { boundPlane, boundPoint, boundPointOnPlane, getLineInPlaneName, getLineNames, getLinePlaneName, getOtherLineName, getPlaneNames, getPlanesLineNames, rotateVector, transformPlane, updateState };