UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

328 lines (301 loc) 12.9 kB
import { m as macro } from '../../macros2.js'; import vtkAbstractPicker from './AbstractPicker.js'; import vtkBoundingBox from '../../Common/DataModel/BoundingBox.js'; import { d as dot, l as normalize, s as subtract, f as distance2BetweenPoints } from '../../Common/Core/Math/index.js'; import { vec3, mat4, vec4 } from 'gl-matrix'; const { vtkErrorMacro } = macro; const { vtkWarningMacro } = macro; // ---------------------------------------------------------------------------- // vtkPicker methods // ---------------------------------------------------------------------------- function vtkPicker(publicAPI, model) { // Set our className model.classHierarchy.push('vtkPicker'); const superClass = { ...publicAPI }; function initialize() { superClass.initialize(); model.actors = []; model.pickedPositions = []; model.mapperPosition[0] = 0.0; model.mapperPosition[1] = 0.0; model.mapperPosition[2] = 0.0; model.mapper = null; model.dataSet = null; model.globalTMin = Number.MAX_VALUE; } /** * Compute the tolerance in world coordinates. * Do this by determining the world coordinates of the diagonal points of the * window, computing the width of the window in world coordinates, and * multiplying by the tolerance. * @param {Number} selectionZ * @param {Number} aspect * @param {vtkRenderer} renderer * @returns {Number} the computed tolerance */ function computeTolerance(selectionZ, aspect, renderer) { let tolerance = 0.0; const view = renderer.getRenderWindow().getViews()[0]; const viewport = renderer.getViewport(); const winSize = view.getSize(); let x = winSize[0] * viewport[0]; let y = winSize[1] * viewport[1]; const normalizedLeftDisplay = view.displayToNormalizedDisplay(x, y, selectionZ); const windowLowerLeft = renderer.normalizedDisplayToWorld(normalizedLeftDisplay[0], normalizedLeftDisplay[1], normalizedLeftDisplay[2], aspect); x = winSize[0] * viewport[2]; y = winSize[1] * viewport[3]; const normalizedRightDisplay = view.displayToNormalizedDisplay(x, y, selectionZ); const windowUpperRight = renderer.normalizedDisplayToWorld(normalizedRightDisplay[0], normalizedRightDisplay[1], normalizedRightDisplay[2], aspect); for (let i = 0; i < 3; i++) { tolerance += (windowUpperRight[i] - windowLowerLeft[i]) * (windowUpperRight[i] - windowLowerLeft[i]); } return Math.sqrt(tolerance); } /** * Perform picking on the given renderer, given a ray defined in world coordinates. * @param {*} renderer * @param {*} tolerance * @param {*} p1World * @param {*} p2World * @returns true if we picked something else false */ function pick3DInternal(renderer, tolerance, p1World, p2World) { const p1Mapper = new Float64Array(4); const p2Mapper = new Float64Array(4); const ray = []; const hitPosition = []; const props = model.pickFromList ? model.pickList : renderer.getActors(); // pre-allocate some arrays. const transformScale = new Float64Array(3); const pickedPosition = new Float64Array(3); // Loop over props. // Transform ray (defined from position of camera to selection point) into coordinates of mapper (not // transformed to actors coordinates! Reduces overall computation!!!). // Note that only vtkProp3D's can be picked by vtkPicker. props.forEach(prop => { const mapper = prop.getMapper(); const propIsFullyTranslucent = prop.getProperty?.().getOpacity?.() === 0.0; const pickable = prop.getNestedPickable() && prop.getNestedVisibility() && !propIsFullyTranslucent; if (!pickable) { // prop cannot be picked return; } // The prop is candidate for picking: // - get its composite matrix and invert it // - use the inverted matrix to transform the ray points into mapper coordinates model.transformMatrix = prop.getMatrix().slice(0); mat4.transpose(model.transformMatrix, model.transformMatrix); mat4.invert(model.transformMatrix, model.transformMatrix); vec4.transformMat4(p1Mapper, p1World, model.transformMatrix); vec4.transformMat4(p2Mapper, p2World, model.transformMatrix); vec3.scale(p1Mapper, p1Mapper, 1 / p1Mapper[3]); vec3.scale(p2Mapper, p2Mapper, 1 / p2Mapper[3]); subtract(p2Mapper, p1Mapper, ray); // We now have the ray endpoints in mapper coordinates. // Compare it with the mapper bounds to check if intersection is possible. // Get the bounding box of the mapper. // Note that the tolerance is added to the bounding box to make sure things on the edge of the // bounding box are picked correctly. const bounds = mapper ? vtkBoundingBox.inflate(mapper.getBounds(), tolerance) : [...vtkBoundingBox.INIT_BOUNDS]; if (vtkBoundingBox.intersectBox(bounds, p1Mapper, ray, hitPosition, [])) { mat4.getScaling(transformScale, model.transformMatrix); const t = model.intersectWithLine(p1Mapper, p2Mapper, tolerance * 0.333 * (transformScale[0] + transformScale[1] + transformScale[2]), prop, mapper); if (t < Number.MAX_VALUE) { pickedPosition[0] = (1.0 - t) * p1World[0] + t * p2World[0]; pickedPosition[1] = (1.0 - t) * p1World[1] + t * p2World[1]; pickedPosition[2] = (1.0 - t) * p1World[2] + t * p2World[2]; const actorIndex = model.actors.indexOf(prop); if (actorIndex !== -1) { // If already in list, compare the previous picked position with the new one. // Store the new one if it is closer from the ray endpoint. const previousPickedPosition = model.pickedPositions[actorIndex]; if (distance2BetweenPoints(p1World, pickedPosition) < distance2BetweenPoints(p1World, previousPickedPosition)) { model.pickedPositions[actorIndex] = pickedPosition.slice(0); } } else { model.actors.push(prop); model.pickedPositions.push(pickedPosition.slice(0)); } } } }); // sort array by distance const tempArray = []; for (let i = 0; i < model.pickedPositions.length; i++) { tempArray.push({ actor: model.actors[i], pickedPosition: model.pickedPositions[i], distance2: distance2BetweenPoints(p1World, model.pickedPositions[i]) }); } tempArray.sort((a, b) => { const keyA = a.distance2; const keyB = b.distance2; // order the actors based on the distance2 attribute, so the near actors comes // first in the list if (keyA < keyB) return -1; if (keyA > keyB) return 1; return 0; }); model.pickedPositions = []; model.actors = []; tempArray.forEach(obj => { model.pickedPositions.push(obj.pickedPosition); model.actors.push(obj.actor); }); } // Intersect data with specified ray. // Project the center point of the mapper onto the ray and determine its parametric value model.intersectWithLine = (p1, p2, tolerance, prop, mapper) => { if (!mapper) { return Number.MAX_VALUE; } const center = mapper.getCenter(); const ray = vec3.subtract(new Float64Array(3), p2, p1); const rayFactor = dot(ray, ray); if (rayFactor === 0.0) { return 2.0; } // Project the center point onto the ray and determine its parametric value const t = (ray[0] * (center[0] - p1[0]) + ray[1] * (center[1] - p1[1]) + ray[2] * (center[2] - p1[2])) / rayFactor; return t; }; // To be overridden in subclasses publicAPI.pick = (selection, renderer) => { if (selection.length !== 3) { vtkWarningMacro('vtkPicker.pick - selection needs three components'); } if (!renderer) { vtkErrorMacro('vtkPicker.pick - renderer cannot be null'); throw new Error('renderer cannot be null'); } initialize(); const selectionX = selection[0]; const selectionY = selection[1]; let selectionZ = selection[2]; model.renderer = renderer; model.selectionPoint[0] = selectionX; model.selectionPoint[1] = selectionY; model.selectionPoint[2] = selectionZ; const p1World = new Float64Array(4); const p2World = new Float64Array(4); // Get camera focal point and position. Convert to display (screen) // coordinates. We need a depth value for z-buffer. const camera = renderer.getActiveCamera(); const cameraPos = camera.getPosition(); const cameraFP = camera.getFocalPoint(); const view = renderer.getRenderWindow().getViews()[0]; const dims = view.getViewportSize(renderer); if (dims[1] === 0) { vtkWarningMacro('vtkPicker.pick - viewport area is 0'); return; } const aspect = dims[0] / dims[1]; let displayCoords = []; displayCoords = renderer.worldToNormalizedDisplay(cameraFP[0], cameraFP[1], cameraFP[2], aspect); displayCoords = view.normalizedDisplayToDisplay(displayCoords[0], displayCoords[1], displayCoords[2]); selectionZ = displayCoords[2]; // Convert the selection point into world coordinates. const normalizedDisplay = view.displayToNormalizedDisplay(selectionX, selectionY, selectionZ); const worldCoords = renderer.normalizedDisplayToWorld(normalizedDisplay[0], normalizedDisplay[1], normalizedDisplay[2], aspect); for (let i = 0; i < 3; i++) { model.pickPosition[i] = worldCoords[i]; } // Compute the ray endpoints. The ray is along the line running from // the camera position to the selection point, starting where this line // intersects the front clipping plane, and terminating where this // line intersects the back clipping plane. const ray = []; for (let i = 0; i < 3; i++) { ray[i] = model.pickPosition[i] - cameraPos[i]; } const cameraDOP = []; for (let i = 0; i < 3; i++) { cameraDOP[i] = cameraFP[i] - cameraPos[i]; } normalize(cameraDOP); const rayLength = dot(cameraDOP, ray); if (rayLength === 0.0) { vtkWarningMacro('Picker::Pick Cannot process points'); return; } const clipRange = camera.getClippingRange(); let tF; let tB; if (camera.getParallelProjection()) { tF = clipRange[0] - rayLength; tB = clipRange[1] - rayLength; for (let i = 0; i < 3; i++) { p1World[i] = model.pickPosition[i] + tF * cameraDOP[i]; p2World[i] = model.pickPosition[i] + tB * cameraDOP[i]; } } else { tF = clipRange[0] / rayLength; tB = clipRange[1] / rayLength; for (let i = 0; i < 3; i++) { p1World[i] = cameraPos[i] + tF * ray[i]; p2World[i] = cameraPos[i] + tB * ray[i]; } } p1World[3] = 1.0; p2World[3] = 1.0; const tolerance = computeTolerance(selectionZ, aspect, renderer) * model.tolerance; pick3DInternal(model.renderer, tolerance, p1World, p2World); }; publicAPI.pick3DPoint = (selectionPoint, focalPoint, renderer) => { if (!renderer) { throw new Error('renderer cannot be null'); } initialize(); model.renderer = renderer; vec3.copy(model.selectionPoint, selectionPoint); const view = renderer.getRenderWindow().getViews()[0]; const dims = view.getViewportSize(renderer); if (dims[1] === 0) { vtkWarningMacro('vtkPicker.pick3DPoint - viewport area is 0'); return; } const aspect = dims[0] / dims[1]; const tolerance = computeTolerance(model.selectionPoint[2], aspect, renderer) * model.tolerance; pick3DInternal(renderer, tolerance, selectionPoint, focalPoint); }; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { tolerance: 0.025, mapperPosition: [0.0, 0.0, 0.0], mapper: null, dataSet: null, actors: [], pickedPositions: [], transformMatrix: null, globalTMin: Number.MAX_VALUE }; // ---------------------------------------------------------------------------- function extend(publicAPI, model) { let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; Object.assign(model, DEFAULT_VALUES, initialValues); // Inheritance vtkAbstractPicker.extend(publicAPI, model, initialValues); macro.setGet(publicAPI, model, ['tolerance']); macro.setGetArray(publicAPI, model, ['mapperPosition'], 3); macro.get(publicAPI, model, ['mapper', 'dataSet', 'actors', 'pickedPositions']); macro.event(publicAPI, model, 'pickChange'); vtkPicker(publicAPI, model); } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkPicker'); // ---------------------------------------------------------------------------- var vtkPicker$1 = { newInstance, extend }; export { vtkPicker$1 as default, extend, newInstance };