UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

483 lines (449 loc) 17 kB
import { m as macro } from '../../macros2.js'; import vtkCellTypes from '../../Common/DataModel/CellTypes.js'; import vtkLine from '../../Common/DataModel/Line.js'; import vtkPicker from './Picker.js'; import vtkPolyLine from '../../Common/DataModel/PolyLine.js'; import vtkTriangle from '../../Common/DataModel/Triangle.js'; import vtkQuad from '../../Common/DataModel/Quad.js'; import { l as normalize, f as distance2BetweenPoints } from '../../Common/Core/Math/index.js'; import { CellType } from '../../Common/DataModel/CellTypes/Constants.js'; import { vec3, vec4 } from 'gl-matrix'; import vtkBox from '../../Common/DataModel/Box.js'; // ---------------------------------------------------------------------------- // Global methods // ---------------------------------------------------------------------------- function createCellMap() { return { [CellType.VTK_LINE]: vtkLine.newInstance(), [CellType.VTK_POLY_LINE]: vtkPolyLine.newInstance(), [CellType.VTK_TRIANGLE]: vtkTriangle.newInstance(), [CellType.VTK_QUAD]: vtkQuad.newInstance() }; } function clipLineWithPlane(mapper, matrix, p1, p2) { const outObj = { planeId: -1, t1: 0.0, t2: 1.0, intersect: 0 }; const nbClippingPlanes = mapper.getNumberOfClippingPlanes(); const plane = []; for (let i = 0; i < nbClippingPlanes; i++) { mapper.getClippingPlaneInDataCoords(matrix, i, plane); const d1 = plane[0] * p1[0] + plane[1] * p1[1] + plane[2] * p1[2] + plane[3]; const d2 = plane[0] * p2[0] + plane[1] * p2[1] + plane[2] * p2[2] + plane[3]; // If both distances are negative, both points are outside if (d1 < 0 && d2 < 0) { return 0; } if (d1 < 0 || d2 < 0) { // If only one of the distances is negative, the line crosses the plane // Compute fractional distance "t" of the crossing between p1 & p2 let t = 0.0; // The "if" here just avoids an expensive division when possible if (d1 !== 0) { // We will never have d1==d2 since they have different signs t = d1 / (d1 - d2); } // If point p1 was clipped, adjust t1 if (d1 < 0) { if (t >= outObj.t1) { outObj.t1 = t; outObj.planeId = i; } } else if (t <= outObj.t2) { // else point p2 was clipped, so adjust t2 outObj.t2 = t; } // If this happens, there's no line left if (outObj.t1 > outObj.t2) { outObj.intersect = 0; return outObj; } } } outObj.intersect = 1; return outObj; } // ---------------------------------------------------------------------------- // Static API // ---------------------------------------------------------------------------- const STATIC = { clipLineWithPlane }; // ---------------------------------------------------------------------------- // vtkCellPicker methods // ---------------------------------------------------------------------------- function vtkCellPicker(publicAPI, model) { // Set our className model.classHierarchy.push('vtkCellPicker'); const superClass = { ...publicAPI }; function resetCellPickerInfo() { model.cellId = -1; model.pCoords[0] = 0.0; model.pCoords[1] = 0.0; model.pCoords[2] = 0.0; model.cellIJK[0] = 0.0; model.cellIJK[1] = 0.0; model.cellIJK[2] = 0.0; model.mapperNormal[0] = 0.0; model.mapperNormal[1] = 0.0; model.mapperNormal[2] = 1.0; model.pickNormal[0] = 0.0; model.pickNormal[1] = 0.0; model.pickNormal[2] = 1.0; } function resetPickInfo() { model.dataSet = null; model.mapper = null; resetCellPickerInfo(); } publicAPI.initialize = () => { resetPickInfo(); superClass.initialize(); }; publicAPI.computeSurfaceNormal = (data, cell, weights, normal) => { const normals = data.getPointData().getNormals(); if (normals) { normal[0] = 0.0; normal[1] = 0.0; normal[2] = 0.0; const pointNormal = []; for (let i = 0; i < 3; i++) { normals.getTuple(cell.getPointsIds()[i], pointNormal); normal[0] += pointNormal[0] * weights[i]; normal[1] += pointNormal[1] * weights[i]; normal[2] += pointNormal[2] * weights[i]; } normalize(normal); } else { return 0; } return 1; }; publicAPI.pick = (selection, renderer) => { publicAPI.initialize(); const pickResult = superClass.pick(selection, renderer); if (pickResult) { const camera = renderer.getActiveCamera(); const cameraPos = []; camera.getPosition(cameraPos); if (camera.getParallelProjection()) { // For parallel projection, use -ve direction of projection const cameraFocus = []; camera.getFocalPoint(cameraFocus); model.pickNormal[0] = cameraPos[0] - cameraFocus[0]; model.pickNormal[1] = cameraPos[1] - cameraFocus[1]; model.pickNormal[2] = cameraPos[2] - cameraFocus[2]; } else { // Get the vector from pick position to the camera model.pickNormal[0] = cameraPos[0] - model.pickPosition[0]; model.pickNormal[1] = cameraPos[1] - model.pickPosition[1]; model.pickNormal[2] = cameraPos[2] - model.pickPosition[2]; } normalize(model.pickNormal); } return pickResult; }; model.intersectWithLine = (p1, p2, tolerance, prop, mapper) => { let tMin = Number.MAX_VALUE; let t1 = 0.0; let t2 = 1.0; const vtkCellPickerPlaneTol = 1e-14; const clipLine = clipLineWithPlane(mapper, model.transformMatrix, p1, p2); if (mapper && !clipLine.intersect) { return Number.MAX_VALUE; } if (mapper.isA('vtkImageMapper') || mapper.isA('vtkImageArrayMapper')) { const pickData = mapper.intersectWithLineForCellPicking(p1, p2); if (pickData) { tMin = pickData.t; model.cellIJK = pickData.ijk; model.pCoords = pickData.pCoords; } } else if (mapper.isA('vtkVolumeMapper')) { // we calculate here the parametric intercept points between the ray and the bounding box, so // if the application defines for some reason a too large ray length (1e6), it restrict the calculation // to the vtkVolume prop bounding box const interceptionObject = vtkBox.intersectWithLine(mapper.getBounds(), p1, p2); t1 = interceptionObject?.t1 > clipLine.t1 ? interceptionObject.t1 : clipLine.t1; t2 = interceptionObject?.t2 < clipLine.t2 ? interceptionObject.t2 : clipLine.t2; tMin = model.intersectVolumeWithLine(p1, p2, t1, t2, tolerance, prop); } else if (mapper.isA('vtkMapper')) { tMin = model.intersectActorWithLine(p1, p2, t1, t2, tolerance, mapper); } if (tMin < model.globalTMin) { model.globalTMin = tMin; if (Math.abs(tMin - t1) < vtkCellPickerPlaneTol && clipLine.clippingPlaneId >= 0) { model.mapperPosition[0] = p1[0] * (1 - t1) + p2[0] * t1; model.mapperPosition[1] = p1[1] * (1 - t1) + p2[1] * t1; model.mapperPosition[2] = p1[2] * (1 - t1) + p2[2] * t1; const plane = []; mapper.getClippingPlaneInDataCoords(model.transformMatrix, clipLine.clippingPlaneId, plane); normalize(plane); // Want normal outward from the planes, not inward model.mapperNormal[0] = -plane[0]; model.mapperNormal[1] = -plane[1]; model.mapperNormal[2] = -plane[2]; } vec3.transformMat4(model.pickPosition, model.mapperPosition, model.transformMatrix); // Transform vector const mat = model.transformMatrix; model.mapperNormal[0] = mat[0] * model.pickNormal[0] + mat[4] * model.pickNormal[1] + mat[8] * model.pickNormal[2]; model.mapperNormal[1] = mat[1] * model.pickNormal[0] + mat[5] * model.pickNormal[1] + mat[9] * model.pickNormal[2]; model.mapperNormal[2] = mat[2] * model.pickNormal[0] + mat[6] * model.pickNormal[1] + mat[10] * model.pickNormal[2]; } return tMin; }; model.intersectVolumeWithLine = (p1, p2, t1, t2, tolerance, volume) => { let tMin = Number.MAX_VALUE; const mapper = volume.getMapper(); const imageData = mapper.getInputData(); const dims = imageData.getDimensions(); const scalars = imageData.getPointData().getScalars().getData(); const extent = imageData.getExtent(); // get the world to index transform to correctly transform from world to volume index const imageTransform = imageData.getWorldToIndex(); // calculate opacity table const numIComps = 1; let oWidth = mapper.getOpacityTextureWidth(); if (oWidth <= 0) { oWidth = 1024; } const tmpTable = new Float32Array(oWidth); const opacityArray = new Float32Array(oWidth); let ofun; let oRange; const sampleDist = volume.getMapper().getSampleDistance(); for (let c = 0; c < numIComps; ++c) { ofun = volume.getProperty().getScalarOpacity(c); oRange = ofun.getRange(); ofun.getTable(oRange[0], oRange[1], oWidth, tmpTable, 1); const opacityFactor = sampleDist / volume.getProperty().getScalarOpacityUnitDistance(c); // adjust for sample distance etc for (let i = 0; i < oWidth; ++i) { opacityArray[i] = 1.0 - (1.0 - tmpTable[i]) ** opacityFactor; } } const scale = oWidth / (oRange[1] - oRange[0] + 1); // Make a new p1 and p2 using the clipped t1 and t2 const q1 = [0, 0, 0, 1]; const q2 = [0, 0, 0, 1]; q1[0] = p1[0]; q1[1] = p1[1]; q1[2] = p1[2]; q2[0] = p2[0]; q2[1] = p2[1]; q2[2] = p2[2]; if (t1 !== 0.0 || t2 !== 1.0) { for (let j = 0; j < 3; j++) { q1[j] = p1[j] * (1.0 - t1) + p2[j] * t1; q2[j] = p1[j] * (1.0 - t2) + p2[j] * t2; } } // convert q1, q2 world coordinates to x1, x2 volume index coordinates const x1 = [0, 0, 0, 0]; const x2 = [0, 0, 0, 0]; vec4.transformMat4(x1, q1, imageTransform); vec4.transformMat4(x2, q2, imageTransform); const x = [0, 0, 0]; const xi = [0, 0, 0]; const sliceSize = dims[1] * dims[0]; const rowSize = dims[0]; // here the step is the 1 over the distance between volume index location x1 and x2 const step = 1 / Math.sqrt(distance2BetweenPoints(x1, x2)); let insideVolume; // here we reinterpret the t value as the distance between x1 and x2 // When calculating the tMin, we weight t between t1 and t2 values for (let t = 0; t < 1; t += step) { // calculate the location of the point insideVolume = true; for (let j = 0; j < 3; j++) { // "t" is the fractional distance between endpoints x1 and x2 x[j] = x1[j] * (1.0 - t) + x2[j] * t; } for (let j = 0; j < 3; j++) { // Bounds check if (x[j] < extent[2 * j]) { x[j] = extent[2 * j]; insideVolume = false; } else if (x[j] > extent[2 * j + 1]) { x[j] = extent[2 * j + 1]; insideVolume = false; } xi[j] = Math.round(x[j]); } if (insideVolume) { const index = xi[2] * sliceSize + xi[1] * rowSize + xi[0]; let value = scalars[index]; if (value < oRange[0]) { value = oRange[0]; } else if (value > oRange[1]) { value = oRange[1]; } value = Math.floor((value - oRange[0]) * scale); const opacity = tmpTable[value]; if (opacity > model.opacityThreshold) { // returning the tMin to the original scale, if t1 > 0 or t2 < 1 tMin = t1 * (1.0 - t) + t2 * t; break; } } } return tMin; }; model.intersectActorWithLine = (p1, p2, t1, t2, tolerance, mapper) => { let tMin = Number.MAX_VALUE; const minXYZ = [0, 0, 0]; let pDistMin = Number.MAX_VALUE; const minPCoords = [0, 0, 0]; let minCellId = null; let minCell = null; let minCellType = null; let subId = null; const x = []; const data = mapper.getInputData(); // Make a new p1 and p2 using the clipped t1 and t2 const q1 = [0, 0, 0]; const q2 = [0, 0, 0]; q1[0] = p1[0]; q1[1] = p1[1]; q1[2] = p1[2]; q2[0] = p2[0]; q2[1] = p2[1]; q2[2] = p2[2]; if (t1 !== 0.0 || t2 !== 1.0) { for (let j = 0; j < 3; j++) { q1[j] = p1[j] * (1.0 - t1) + p2[j] * t1; q2[j] = p1[j] * (1.0 - t2) + p2[j] * t2; } } if (data.getCells) { if (!data.getCells()) { data.buildLinks(); } const tempCellMap = createCellMap(); const minCellMap = createCellMap(); const numberOfCells = data.getNumberOfCells(); /* eslint-disable no-continue */ for (let cellId = 0; cellId < numberOfCells; cellId++) { const pCoords = [0, 0, 0]; minCellType = data.getCellType(cellId); // Skip cells that are marked as empty if (minCellType === CellType.VTK_EMPTY_CELL) { continue; } const cell = tempCellMap[minCellType]; if (cell == null) { continue; } minCell = minCellMap[minCellType]; data.getCell(cellId, cell); let cellPicked; { if (vtkCellTypes.hasSubCells(minCellType)) { cellPicked = cell.intersectWithLine(t1, t2, p1, p2, tolerance, x, pCoords); } else { cellPicked = cell.intersectWithLine(p1, p2, tolerance, x, pCoords); } } if (cellPicked.intersect === 1 && cellPicked.t <= tMin + model.tolerance && cellPicked.t >= t1 && cellPicked.t <= t2) { const pDist = cell.getParametricDistance(pCoords); if (pDist < pDistMin || pDist === pDistMin && cellPicked.t < tMin) { tMin = cellPicked.t; pDistMin = pDist; subId = cellPicked.subId; minCellId = cellId; cell.deepCopy(minCell); for (let k = 0; k < 3; k++) { minXYZ[k] = x[k]; minPCoords[k] = pCoords[k]; } } } } /* eslint-enable no-continue */ } if (minCellId >= 0 && tMin < model.globalTMin) { resetPickInfo(); const nbPointsInCell = minCell.getNumberOfPoints(); const weights = new Array(nbPointsInCell); for (let i = 0; i < nbPointsInCell; i++) { weights[i] = 0.0; } const point = []; if (vtkCellTypes.hasSubCells(minCellType)) { minCell.evaluateLocation(subId, minPCoords, point, weights); } else { minCell.evaluateLocation(minPCoords, point, weights); } // Return the polydata to the user model.dataSet = data; model.cellId = minCellId; model.pCoords[0] = minPCoords[0]; model.pCoords[1] = minPCoords[1]; model.pCoords[2] = minPCoords[2]; // Find the point with the maximum weight let maxWeight = 0; let iMaxWeight = -1; for (let i = 0; i < nbPointsInCell; i++) { if (weights[i] > maxWeight) { iMaxWeight = i; maxWeight = weights[i]; } } // If maximum weight is found, use it to get the PointId if (iMaxWeight !== -1) { model.pointId = minCell.getPointsIds()[iMaxWeight]; } // Set the mapper position model.mapperPosition[0] = minXYZ[0]; model.mapperPosition[1] = minXYZ[1]; model.mapperPosition[2] = minXYZ[2]; // Compute the normal if (!publicAPI.computeSurfaceNormal(data, minCell, weights, model.mapperNormal)) { // By default, the normal points back along view ray model.mapperNormal[0] = p1[0] - p2[0]; model.mapperNormal[1] = p1[1] - p2[1]; model.mapperNormal[2] = p1[2] - p2[2]; normalize(model.mapperNormal); } } return tMin; }; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { cellId: -1, pCoords: [], cellIJK: [], pickNormal: [], mapperNormal: [], opacityThreshold: 0.2 }; // ---------------------------------------------------------------------------- function extend(publicAPI, model) { let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; Object.assign(model, DEFAULT_VALUES, initialValues); // Inheritance vtkPicker.extend(publicAPI, model, initialValues); macro.getArray(publicAPI, model, ['pickNormal', 'mapperNormal', 'pCoords', 'cellIJK']); macro.setGet(publicAPI, model, ['opacityThreshold']); macro.get(publicAPI, model, ['cellId']); // Object methods vtkCellPicker(publicAPI, model); } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkCellPicker'); // ---------------------------------------------------------------------------- var vtkCellPicker$1 = { newInstance, extend, ...STATIC }; export { STATIC, vtkCellPicker$1 as default, extend, newInstance };