UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

579 lines (530 loc) 23 kB
import { m as macro } from '../../macros2.js'; import vtkBoundingBox from '../../Common/DataModel/BoundingBox.js'; import vtkDataArray from '../../Common/Core/DataArray.js'; import { b as vtkMath } from '../../Common/Core/Math/index.js'; import { AttributeTypes } from '../../Common/DataModel/DataSetAttributes/Constants.js'; import vtkPoints from '../../Common/Core/Points.js'; import vtkPolyData from '../../Common/DataModel/PolyData.js'; import vtkTriangle from '../../Common/DataModel/Triangle.js'; const VertexType = { VTK_SIMPLE_VERTEX: 0, VTK_FIXED_VERTEX: 1, VTK_FEATURE_EDGE_VERTEX: 2, VTK_BOUNDARY_EDGE_VERTEX: 3 }; // ---------------------------------------------------------------------------- // vtkWindowedSincPolyDataFilter methods // ---------------------------------------------------------------------------- function vtkWindowedSincPolyDataFilter(publicAPI, model) { // Set our className model.classHierarchy.push('vtkWindowedSincPolyDataFilter'); publicAPI.vtkWindowedSincPolyDataFilterExecute = (inPts, inputPolyData, output) => { if (!inPts || model.numberOfIterations <= 0) { return inPts; } const inPtsData = inPts.getData(); const inVerts = inputPolyData.getVerts().getData(); const inLines = inputPolyData.getLines().getData(); const inPolys = inputPolyData.getPolys().getData(); const inStrips = inputPolyData.getStrips().getData(); const cosFeatureAngle = Math.cos(vtkMath.radiansFromDegrees(model.featureAngle)); const cosEdgeAngle = Math.cos(vtkMath.radiansFromDegrees(model.edgeAngle)); const numPts = inPts.getNumberOfPoints(); // Perform topological analysis. What we're going to do is build a connectivity // array of connected vertices. The outcome will be one of three // classifications for a vertex: VTK_SIMPLE_VERTEX, VTK_FIXED_VERTEX. or // VTK_EDGE_VERTEX. Simple vertices are smoothed using all connected // vertices. FIXED vertices are never smoothed. Edge vertices are smoothed // using a subset of the attached vertices. const verts = new Array(numPts); for (let i = 0; i < numPts; ++i) { verts[i] = { type: VertexType.VTK_SIMPLE_VERTEX, edges: null }; } // check vertices first. Vertices are never smoothed_-------------- let npts = 0; for (let i = 0; i < inVerts.length; i += npts + 1) { npts = inVerts[i]; const pts = inVerts.slice(i + 1, i + 1 + npts); for (let j = 0; j < pts.length; ++j) { verts[pts[j]].type = VertexType.VTK_FIXED_VERTEX; } } // now check lines. Only manifold lines can be smoothed------------ for (let i = 0; i < inLines.length; i += npts + 1) { npts = inLines[i]; const pts = inLines.slice(i + 1, i + 1 + npts); // Check for closed loop which are treated specially. Basically the // last point is ignored (set to fixed). const closedLoop = pts[0] === pts[npts - 1] && npts > 3; for (let j = 0; j < npts; ++j) { if (verts[pts[j]].type === VertexType.VTK_SIMPLE_VERTEX) { // First point if (j === 0) { if (!closedLoop) { verts[pts[0]].type = VertexType.VTK_FIXED_VERTEX; } else { verts[pts[0]].type = VertexType.VTK_FEATURE_EDGE_VERTEX; verts[pts[0]].edges = [pts[npts - 2], pts[1]]; } } // Last point else if (j === npts - 1 && !closedLoop) { verts[pts[j]].type = VertexType.VTK_FIXED_VERTEX; } // In between point // is edge vertex (unless already edge vertex!) else { verts[pts[j]].type = VertexType.VTK_FEATURE_EDGE_VERTEX; verts[pts[j]].edges = [pts[j - 1], pts[closedLoop && j === npts - 2 ? 0 : j + 1]]; } } // if simple vertex // Vertex has been visited before, need to fix it. Special case // when working on closed loop. else if (verts[pts[j]].type === VertexType.VTK_FEATURE_EDGE_VERTEX && !(closedLoop && j === npts - 1)) { verts[pts[j]].type = VertexType.VTK_FIXED_VERTEX; verts[pts[j]].edges = null; } } // for all points in this line } // for all lines // now polygons and triangle strips------------------------------- const numPolys = inPolys.length; const numStrips = inStrips.length; if (numPolys > 0 || numStrips > 0) { const inMesh = vtkPolyData.newInstance(); inMesh.setPoints(inputPolyData.getPoints()); inMesh.setPolys(inputPolyData.getPolys()); const mesh = inMesh; let neighbors = []; let nei = 0; // const numNeiPts = 0; const normal = []; const neiNormal = []; /* TODO: Add vtkTriangleFilter if ( (numStrips = inputPolyData.getStrips().GetNumberOfCells()) > 0 ) { // convert data to triangles inMesh.setStrips(inputPolyData.getStrips()); const toTris = vtkTriangleFilter.newInstance(); toTris.setInputData(inMesh); toTris.update(); mesh = toTris.getOutput(); } */ mesh.buildLinks(); // to do neighborhood searching const polys = mesh.getPolys().getData(); let cellId = 0; for (let c = 0; c < polys.length; c += npts + 1, ++cellId) { npts = polys[c]; const pts = polys.slice(c + 1, c + 1 + npts); for (let i = 0; i < npts; ++i) { const p1 = pts[i]; const p2 = pts[(i + 1) % npts]; if (verts[p1].edges === null) { verts[p1].edges = []; } if (verts[p2].edges == null) { verts[p2].edges = []; } neighbors = mesh.getCellEdgeNeighbors(cellId, p1, p2); const numNei = neighbors.length; // neighbors->GetNumberOfIds(); let edge = VertexType.VTK_SIMPLE_VERTEX; if (numNei === 0) { edge = VertexType.VTK_BOUNDARY_EDGE_VERTEX; } else if (numNei >= 2) { // non-manifold case, check nonmanifold smoothing state if (!model.nonManifoldSmoothing) { // check to make sure that this edge hasn't been marked already let j = 0; for (; j < numNei; ++j) { if (neighbors[j] < cellId) { break; } } if (j >= numNei) { edge = VertexType.VTK_FEATURE_EDGE_VERTEX; } } /* eslint-disable no-cond-assign */ } else if (numNei === 1 && (nei = neighbors[0]) > cellId) { if (model.featureEdgeSmoothing) { // TODO: support polygons // vtkPolygon::ComputeNormal(inPts,npts,pts,normal); vtkTriangle.computeNormal(inPts.getPoint(pts[0]), inPts.getPoint(pts[1]), inPts.getPoint(pts[2]), normal); const { cellPointIds } = mesh.getCellPoints(nei); // vtkPolygon::ComputeNormal(inPts,numNeiPts,neiPts,neiNormal); vtkTriangle.computeNormal(inPts.getPoint(cellPointIds[0]), inPts.getPoint(cellPointIds[1]), inPts.getPoint(cellPointIds[2]), neiNormal); if (vtkMath.dot(normal, neiNormal) <= cosFeatureAngle) { edge = VertexType.VTK_FEATURE_EDGE_VERTEX; } } } // a visited edge; skip rest of analysis else { /* eslint-disable no-continue */ continue; } if (edge && verts[p1].type === VertexType.VTK_SIMPLE_VERTEX) { verts[p1].edges = [p2]; verts[p1].type = edge; } else if (edge && verts[p1].type === VertexType.VTK_BOUNDARY_EDGE_VERTEX || edge && verts[p1].type === VertexType.VTK_FEATURE_EDGE_VERTEX || !edge && verts[p1].type === VertexType.VTK_SIMPLE_VERTEX) { verts[p1].edges.push(p2); if (verts[p1].type && edge === VertexType.VTK_BOUNDARY_EDGE_VERTEX) { verts[p1].type = VertexType.VTK_BOUNDARY_EDGE_VERTEX; } } if (edge && verts[p2].type === VertexType.VTK_SIMPLE_VERTEX) { verts[p2].edges = [p1]; verts[p2].type = edge; } else if (edge && verts[p2].type === VertexType.VTK_BOUNDARY_EDGE_VERTEX || edge && verts[p2].type === VertexType.VTK_FEATURE_EDGE_VERTEX || !edge && verts[p2].type === VertexType.VTK_SIMPLE_VERTEX) { verts[p2].edges.push(p1); if (verts[p2].type && edge === VertexType.VTK_BOUNDARY_EDGE_VERTEX) { verts[p2].type = VertexType.VTK_BOUNDARY_EDGE_VERTEX; } } } } } // if strips or polys for (let i = 0; i < numPts; ++i) { if (verts[i].type === VertexType.VTK_SIMPLE_VERTEX) ; else if (verts[i].type === VertexType.VTK_FIXED_VERTEX) ; else if (verts[i].type === VertexType.VTK_FEATURE_EDGE_VERTEX || verts[i].type === VertexType.VTK_BOUNDARY_EDGE_VERTEX) { // see how many edges; if two, what the angle is if (!model.boundarySmoothing && verts[i].type === VertexType.VTK_BOUNDARY_EDGE_VERTEX) { verts[i].type = VertexType.VTK_FIXED_VERTEX; } else if ((npts = verts[i].edges.length) !== 2) { // can only smooth edges on 2-manifold surfaces verts[i].type = VertexType.VTK_FIXED_VERTEX; } // check angle between edges else { const x1 = inPts.getPoint(verts[i].edges[0]); const x2 = inPts.getPoint(i); const x3 = inPts.getPoint(verts[i].edges[1]); const l1 = [0, 0, 0]; const l2 = [0, 0, 0]; for (let k = 0; k < 3; ++k) { l1[k] = x2[k] - x1[k]; l2[k] = x3[k] - x2[k]; } if (vtkMath.normalize(l1) >= 0.0 && vtkMath.normalize(l2) >= 0.0 && vtkMath.dot(l1, l2) < cosEdgeAngle) { verts[i].type = VertexType.VTK_FIXED_VERTEX; } else if (verts[i].type === VertexType.VTK_FEATURE_EDGE_VERTEX) ; else ; } // if along edge } // if edge vertex } // for all points // Perform Windowed Sinc function interpolation // // console.log('Beginning smoothing iterations...'); // need 4 vectors of points let zero = 0; let one = 1; let two = 2; const three = 3; const newPts = []; newPts.push(vtkPoints.newInstance()); newPts[zero].setNumberOfPoints(numPts); newPts.push(vtkPoints.newInstance()); newPts[one].setNumberOfPoints(numPts); newPts.push(vtkPoints.newInstance()); newPts[two].setNumberOfPoints(numPts); newPts.push(vtkPoints.newInstance()); newPts[three].setNumberOfPoints(numPts); // Get the center and length of the input dataset const inCenter = vtkBoundingBox.getCenter(inputPolyData.getBounds()); const inLength = vtkBoundingBox.getDiagonalLength(inputPolyData.getBounds()); if (!model.normalizeCoordinates) { // initialize to old coordinates // for (let i = 0; i < numPts; ++i) { // newPts[zero].setPoint(i, inPts.subarray(i)); // } const copy = macro.newTypedArray(newPts[zero].getDataType(), inPtsData); newPts[zero].setData(copy, 3); } else { // center the data and scale to be within unit cube [-1, 1] // initialize to old coordinates const normalizedPoint = [0, 0, 0]; for (let i = 0; i < numPts; ++i) { inPts.getPoint(i, normalizedPoint); normalizedPoint[0] = (normalizedPoint[0] - inCenter[0]) / inLength; normalizedPoint[1] = (normalizedPoint[1] - inCenter[1]) / inLength; normalizedPoint[2] = (normalizedPoint[2] - inCenter[2]) / inLength; newPts[zero].setPoint(i, ...normalizedPoint); } } // Smooth with a low pass filter defined as a windowed sinc function. // Taubin describes this methodology is the IBM tech report RC-20404 // (#90237, dated 3/12/96) "Optimal Surface Smoothing as Filter Design" // G. Taubin, T. Zhang and G. Golub. (Zhang and Golub are at Stanford // University) // The formulas here follow the notation of Taubin's TR, i.e. // newPts[zero], newPts[one], etc. // calculate weights and filter coefficients const kPb = model.passBand; // reasonable default for kPb in [0, 2] is 0.1 const thetaPb = Math.acos(1.0 - 0.5 * kPb); // thetaPb in [0, M_PI/2] // vtkDebugMacro(<< "thetaPb = " << thetaPb); const w = new Array(model.numberOfIterations + 1); const c = new Array(model.numberOfIterations + 1); const cprime = new Array(model.numberOfIterations + 1); const zerovector = [0, 0, 0]; // Calculate the weights and the Chebychev coefficients c. // // Windowed sinc function weights. This is for a Hamming window. Other // windowing function could be implemented here. for (let i = 0; i <= model.numberOfIterations; ++i) { w[i] = 0.54 + 0.46 * Math.cos(i * Math.PI / (model.numberOfIterations + 1)); } // Calculate the optimal sigma (offset or fudge factor for the filter). // This is a Newton-Raphson Search. let fKpb = 0; let fPrimeKpb = 0; let done = false; let sigma = 0.0; for (let j = 0; !done && j < 500; ++j) { // Chebyshev coefficients c[0] = w[0] * (thetaPb + sigma) / Math.PI; for (let i = 1; i <= model.numberOfIterations; ++i) { c[i] = 2.0 * w[i] * Math.sin(i * (thetaPb + sigma)) / (i * Math.PI); } // calculate the Chebyshev coefficients for the derivative of the filter cprime[model.numberOfIterations] = 0.0; cprime[model.numberOfIterations - 1] = 0.0; if (model.numberOfIterations > 1) { cprime[model.numberOfIterations - 2] = 2.0 * (model.numberOfIterations - 1) * c[model.numberOfIterations - 1]; } for (let i = model.numberOfIterations - 3; i >= 0; --i) { cprime[i] = cprime[i + 2] + 2.0 * (i + 1) * c[i + 1]; } // Evaluate the filter and its derivative at kPb (note the discrepancy // of calculating the c's based on thetaPb + sigma and evaluating the // filter at kPb (which is equivalent to thetaPb) fKpb = 0.0; fPrimeKpb = 0.0; fKpb += c[0]; fPrimeKpb += cprime[0]; for (let i = 1; i <= model.numberOfIterations; ++i) { if (i === 1) { fKpb += c[i] * (1.0 - 0.5 * kPb); fPrimeKpb += cprime[i] * (1.0 - 0.5 * kPb); } else { fKpb += c[i] * Math.cos(i * Math.acos(1.0 - 0.5 * kPb)); fPrimeKpb += cprime[i] * Math.cos(i * Math.acos(1.0 - 0.5 * kPb)); } } // if fKpb is not close enough to 1.0, then adjust sigma if (model.numberOfIterations > 1) { if (Math.abs(fKpb - 1.0) >= 1e-3) { sigma -= (fKpb - 1.0) / fPrimeKpb; // Newton-Rhapson (want f=1) } else { done = true; } } else { // Order of Chebyshev is 1. Can't use Newton-Raphson to find an // optimal sigma. Object will most likely shrink. done = true; sigma = 0.0; } } if (Math.abs(fKpb - 1.0) >= 1e-3) { console.log('An optimal offset for the smoothing filter could not be found. Unpredictable smoothing/shrinkage may result.'); } const x = [0, 0, 0]; const y = [0, 0, 0]; const deltaX = [0, 0, 0]; const xNew = [0, 0, 0]; const x1 = [0, 0, 0]; const x2 = [0, 0, 0]; // first iteration for (let i = 0; i < numPts; ++i) { if (verts[i].edges != null && (npts = verts[i].edges.length) > 0) { // point is allowed to move newPts[zero].getPoint(i, x); // use current points deltaX[0] = 0.0; deltaX[1] = 0.0; deltaX[2] = 0.0; // calculate the negative of the laplacian // for all connected points for (let j = 0; j < npts; ++j) { newPts[zero].getPoint(verts[i].edges[j], y); for (let k = 0; k < 3; ++k) { deltaX[k] += (x[k] - y[k]) / npts; } } // newPts[one] = newPts[zero] - 0.5 newPts[one] for (let k = 0; k < 3; ++k) { deltaX[k] = x[k] - 0.5 * deltaX[k]; } newPts[one].setPoint(i, ...deltaX); if (verts[i].type === VertexType.VTK_FIXED_VERTEX) { newPts[zero].getPoint(i, deltaX); } else { // calculate newPts[three] = c0 newPts[zero] + c1 newPts[one] for (let k = 0; k < 3; ++k) { deltaX[k] = c[0] * x[k] + c[1] * deltaX[k]; } } newPts[three].setPoint(i, ...deltaX); } // if can move point else { // point is not allowed to move, just use the old point... // (zero out the Laplacian) newPts[one].setPoint(i, ...zerovector); newPts[zero].getPoint(i, deltaX); newPts[three].setPoint(i, ...deltaX); } } // for all points // for the rest of the iterations const pX0 = [0, 0, 0]; const pX1 = [0, 0, 0]; const pX3 = [0, 0, 0]; let iterationNumber = 2; for (; iterationNumber <= model.numberOfIterations; iterationNumber++) { for (let i = 0; i < numPts; ++i) { npts = verts[i].edges != null ? verts[i].edges.length : 0; if (npts > 0) { // point is allowed to move newPts[zero].getPoint(i, pX0); // use current points newPts[one].getPoint(i, pX1); deltaX[0] = 0.0; deltaX[1] = 0.0; deltaX[2] = 0.0; // calculate the negative laplacian of x1 for (let j = 0; j < npts; ++j) { newPts[one].getPoint(verts[i].edges[j], y); for (let k = 0; k < 3; ++k) { deltaX[k] += (pX1[k] - y[k]) / npts; } } // for all connected points // Taubin: x2 = (x1 - x0) + (x1 - x2) for (let k = 0; k < 3; ++k) { deltaX[k] = pX1[k] - pX0[k] + pX1[k] - deltaX[k]; } newPts[two].setPoint(i, ...deltaX); // smooth the vertex (x3 = x3 + cj x2) newPts[three].getPoint(i, pX3); for (let k = 0; k < 3; ++k) { xNew[k] = pX3[k] + c[iterationNumber] * deltaX[k]; } if (verts[i].type !== VertexType.VTK_FIXED_VERTEX) { newPts[three].setPoint(i, ...xNew); } } // if can move point else { // point is not allowed to move, just use the old point... // (zero out the Laplacian) newPts[one].setPoint(i, ...zerovector); newPts[two].setPoint(i, ...zerovector); } } // for all points // update the pointers. three is always three. all other pointers // shift by one and wrap. zero = (1 + zero) % 3; one = (1 + one) % 3; two = (1 + two) % 3; } // for all iterations or until converge // move the iteration count back down so that it matches the // actual number of iterations executed --iterationNumber; // set zero to three so the correct set of positions is outputted zero = three; // console.log('Performed', iterationNumber, 'smoothing passes'); // if we scaled the data down to the unit cube, then scale data back // up to the original space if (model.normalizeCoordinates) { // Re-position the coordinated const repositionedPoint = [0, 0, 0]; for (let i = 0; i < numPts; ++i) { newPts[zero].getPoint(i, repositionedPoint); for (let j = 0; j < 3; ++j) { repositionedPoint[j] = repositionedPoint[j] * inLength + inCenter[j]; } newPts[zero].setPoint(i, ...repositionedPoint); } } if (model.generateErrorScalars) { const newScalars = new Float32Array(numPts); for (let i = 0; i < numPts; ++i) { inPts.getPoint(i, x1); newPts[zero].getPoint(i, x2); newScalars[i] = Math.sqrt(Math.distance2BetweenPoints(x1, x2)); } const newScalarsArray = vtkDataArray.newInstance({ numberOfComponents: 1, values: newScalars }); const idx = output.getPointData().addArray(newScalarsArray); output.getPointData().setActiveAttribute(idx, AttributeTypes.SCALARS); } if (model.generateErrorVectors) { const newVectors = new Float32Array(3 * numPts); for (let i = 0; i < numPts; ++i) { inPts.getPoint(i, x1); newPts[zero].getPoint(i, x2); for (let j = 0; j < 3; ++j) { newVectors[3 * i + j] = x2[j] - x1[j]; } } const newVectorsArray = vtkDataArray.newInstance({ numberOfComponents: 3, values: newVectors }); output.getPointData().setVectors(newVectorsArray); } return newPts[zero]; }; publicAPI.requestData = (inData, outData) => { const numberOfInputs = publicAPI.getNumberOfInputPorts(); if (!numberOfInputs) { return; } const input = inData[0]; if (!input) { return; } const output = outData[0]?.initialize() || vtkPolyData.newInstance(); const outputPoints = publicAPI.vtkWindowedSincPolyDataFilterExecute(input.getPoints(), input, output); output.setPointData(input.getPointData()); output.setCellData(input.getCellData()); output.setFieldData(input.getFieldData()); output.setPoints(outputPoints); output.setVerts(input.getVerts()); output.setLines(input.getLines()); output.setPolys(input.getPolys()); output.setStrips(input.getStrips()); outData[0] = output; }; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { numberOfIterations: 20, passBand: 0.1, featureAngle: 45.0, edgeAngle: 15.0, featureEdgeSmoothing: 0, boundarySmoothing: 1, nonManifoldSmoothing: 0, generateErrorScalars: 0, generateErrorVectors: 0, normalizeCoordinates: 0 }; // ---------------------------------------------------------------------------- function extend(publicAPI, model) { let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; Object.assign(model, DEFAULT_VALUES, initialValues); /* Make this a VTK object */ macro.obj(publicAPI, model); /* Also make it an algorithm with one input and one output */ macro.algo(publicAPI, model, 1, 1); /* Setters */ macro.setGet(publicAPI, model, ['numberOfIterations', 'passBand', 'featureAngle', 'edgeAngle', 'featureEdgeSmoothing', 'boundarySmoothing', 'nonManifoldSmoothing', 'generateErrorScalars', 'generateErrorVectors', 'normalizeCoordinates']); /* Object specific methods */ vtkWindowedSincPolyDataFilter(publicAPI, model); } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkWindowedSincPolyDataFilter'); // ---------------------------------------------------------------------------- var vtkWindowedSincPolyDataFilter$1 = { newInstance, extend }; export { vtkWindowedSincPolyDataFilter$1 as default, extend, newInstance };