@kitware/vtk.js
Version:
Visualization Toolkit for the Web
243 lines (228 loc) • 10.6 kB
JavaScript
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 };