import macro from 'vtk.js/Sources/macros'; import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; import vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkPoints from 'vtk.js/Sources/Common/Core/Points'; import vtkAbstractPointLocator from 'vtk.js/Sources/Common/DataModel/AbstractPointLocator'; import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox';
const { vtkErrorMacro } = macro;
function vtkPointLocator(publicAPI, model) { model.classHierarchy.push('vtkPointLocator');
function distance2ToBucket(x, nei) { const bounds = [ nei[0] * model.HX + model.BX, (nei[0] + 1) * model.HX + model.BX, nei[1] * model.HY + model.BY, (nei[1] + 1) * model.HY + model.BY, nei[2] * model.HZ + model.BZ, (nei[2] + 1) * model.HZ + model.BZ, ]; return vtkBoundingBox.distance2ToBounds(x, bounds); }
function getBucketNeighbors(ijk, ndivs, level) { const buckets = []; if (level === 0) { buckets.push([...ijk]); return buckets; } const minLevel = []; const maxLevel = []; for (let i = 0; i < 3; i++) { const min = ijk[i] - level; const max = ijk[i] + level; minLevel[i] = min > 0 ? min : 0; maxLevel[i] = max < ndivs[i] - 1 ? max : ndivs[i] - 1; } for (let i = minLevel[0]; i <= maxLevel[0]; i++) { for (let j = minLevel[1]; j <= maxLevel[1]; j++) { for (let k = minLevel[2]; k <= maxLevel[2]; k++) { if ( i === ijk[0] + level || i === ijk[0] - level || j === ijk[1] + level || j === ijk[1] - level || k === ijk[2] + level || k === ijk[2] - level ) { buckets.push([i, j, k]); } } } } return buckets; }
function getOverlappingBuckets(x, ijk, dist, level) { const buckets = [];
const xBounds = [x[0], x[0], x[1], x[1], x[2], x[2]]; const bbox = vtkBoundingBox.newInstance(); bbox.setBounds(xBounds); bbox.inflate(dist);
const ijkBounds = [ijk[0], ijk[0], ijk[1], ijk[1], ijk[2], ijk[2]]; const ijkBox = vtkBoundingBox.newInstance(); ijkBox.setBounds(ijkBounds); ijkBox.inflate(level);
const minLevel = publicAPI.getBucketIndices([ bbox.getBounds()[0], bbox.getBounds()[2], bbox.getBounds()[4], ]); const maxLevel = publicAPI.getBucketIndices([ bbox.getBounds()[1], bbox.getBounds()[3], bbox.getBounds()[5], ]);
for (let i = minLevel[0]; i <= maxLevel[0]; i++) { for (let j = minLevel[1]; j <= maxLevel[1]; j++) { for (let k = minLevel[2]; k <= maxLevel[2]; k++) { if (!ijkBox.containsPoint(i, j, k)) { buckets.push([i, j, k]); } } } }
return buckets; }
function getBucketIndex(ijk) { return ijk[0] + ijk[1] * model.XD + ijk[2] * model.sliceSize; }
publicAPI.getBucketIndices = (point) => { const ix = Math.floor((point[0] - model.BX) * model.FX); const iy = Math.floor((point[1] - model.BY) * model.FY); const iz = Math.floor((point[2] - model.BZ) * model.FZ); const ijk = []; ijk[0] = Math.max(0, Math.min(ix, model.XD - 1)); ijk[1] = Math.max(0, Math.min(iy, model.YD - 1)); ijk[2] = Math.max(0, Math.min(iz, model.ZD - 1)); return ijk; };
publicAPI.getBucketIndex = (point) => { const ijk = publicAPI.getBucketIndices(point); return getBucketIndex(ijk); };
publicAPI.buildLocator = () => { model.level = 1; const bounds = model.dataSet.getBounds(); const numPts = model.dataSet.getNumberOfPoints();
let numBuckets = Math.ceil(numPts / model.numberOfPointsPerBucket);
const ndivs = [0, 0, 0]; const bbox = vtkBoundingBox.newInstance(); bbox.setBounds(bounds);
if (model.automatic) { bbox.computeDivisions(numBuckets, ndivs, model.bounds); } else { model.bounds = bbox.inflate(); for (let i = 0; i < 3; i++) { ndivs[i] = Math.max(1, model.divisions[i]); } }
model.divisions = ndivs; numBuckets = ndivs[0] * ndivs[1] * ndivs[2]; model.numberOfBuckets = numBuckets;
for (let i = 0; i < 3; ++i) { model.H[i] = (model.bounds[2 * i + 1] - model.bounds[2 * i]) / ndivs[i]; }
model.hashTable.clear();
publicAPI.computePerformanceFactors();
for (let i = 0; i < numPts; ++i) { const pt = model.dataSet.getPoints().getPoint(i); const key = publicAPI.getBucketIndex(pt); if (!model.hashTable.has(key)) { model.hashTable.set(key, []); } const bucket = model.hashTable.get(key); bucket.push(i); } };
publicAPI.initialize = () => { model.points = null; publicAPI.freeSearchStructure(); };
publicAPI.initPointInsertion = (points, bounds, estNumPts = 0) => { if (points == null) { vtkErrorMacro('A valid vtkPoints object is required for point insertion'); return false; }
if (!bounds || bounds.length !== 6) { vtkErrorMacro('A valid bounds array of length 6 is required'); return false; }
if (!points) { vtkErrorMacro('A valid vtkPoints is required for point insertion'); return false; }
publicAPI.freeSearchStructure(); model.insertionPointId = 0; model.points = points; model.points.setNumberOfComponents(3); model.points.initialize();
let numBuckets = 0; const ndivs = [0, 0, 0]; const bbox = vtkBoundingBox.newInstance(); bbox.setBounds(bounds);
if (model.automatic && estNumPts > 0) { numBuckets = Math.ceil(estNumPts / model.numberOfPointsPerBucket); bbox.computeDivisions(numBuckets, ndivs, model.bounds); } else { model.bounds = bbox.inflate(); for (let i = 0; i < 3; i++) { ndivs[i] = Math.max(1, model.divisions[i]); } }
model.divisions = ndivs; numBuckets = ndivs[0] * ndivs[1] * ndivs[2]; model.numberOfBuckets = numBuckets;
for (let i = 0; i < 3; ++i) { model.H[i] = (model.bounds[2 * i + 1] - model.bounds[2 * i]) / ndivs[i]; }
model.insertionTol2 = model.tolerance * model.tolerance;
let maxDivs = 0; let hmin = Number.MAX_VALUE; for (let i = 0; i < 3; i++) { hmin = model.H[i] < hmin ? model.H[i] : hmin; maxDivs = maxDivs > model.divisions[i] ? maxDivs : model.divisions[i]; } model.insertionLevel = Math.ceil(model.tolerance / hmin); model.insertionLevel = model.insertionLevel > maxDivs ? maxDivs : model.insertionLevel;
publicAPI.computePerformanceFactors();
return true; };
publicAPI.insertPoint = (ptId, x) => { const key = publicAPI.getBucketIndex(x); if (!model.hashTable.has(key)) { model.hashTable.set(key, []); } const bucket = model.hashTable.get(key); bucket.push(ptId); model.points.insertPoint(ptId, x); return { inserted: true, id: ptId }; };
publicAPI.insertNextPoint = (x) => { const key = publicAPI.getBucketIndex(x); if (!model.hashTable.has(key)) { model.hashTable.set(key, []); } const bucket = model.hashTable.get(key); bucket.push(model.insertionPointId); model.points.insertPoint(model.insertionPointId, x); return { inserted: true, id: model.insertionPointId++ }; };
publicAPI.insertUniquePoint = (x) => { const ptId = publicAPI.isInsertedPoint(x);
if (ptId > -1) { return { inserted: false, id: ptId }; } const ret = publicAPI.insertNextPoint(x); return ret; };
publicAPI.isInsertedPoint = (x) => { const ijk = publicAPI.getBucketIndices(x);
const insertionLevel = model.insertionLevel ?? 1;
const numDivs = model.divisions;
for (let lvtk = 0; lvtk <= insertionLevel; lvtk++) { const buckets = getBucketNeighbors(ijk, numDivs, lvtk); for (let i = 0; i < buckets.length; i++) { const nei = buckets[i]; const key = getBucketIndex(nei); const bucket = model.hashTable.get(key); if (bucket) { for (let j = 0; j < bucket.length; j++) { const ptId = bucket[j]; const pt = model.points.getPoint(ptId); if (vtkMath.distance2BetweenPoints(x, pt) <= model.insertionTol2) { return ptId; } } } } } return -1; };
publicAPI.findClosestPoint = (x) => { publicAPI.buildLocator(); const ijk = publicAPI.getBucketIndices(x); const numDivs = model.divisions; let minDist2 = Number.MAX_VALUE; let closest = -1; const maxLevel = Math.max(...numDivs);
for (let level = 0; level < maxLevel && closest === -1; level++) { const neighbors = getBucketNeighbors(ijk, numDivs, level); for (let n = 0; n < neighbors.length; n++) { const key = getBucketIndex(neighbors[n]); const bucket = model.hashTable.get(key); if (bucket) { for (let b = 0; b < bucket.length; b++) { const ptId = bucket[b]; const pt = model.dataSet.getPoints().getPoint(ptId); const dist2 = vtkMath.distance2BetweenPoints(x, pt); if (dist2 < minDist2) { minDist2 = dist2; closest = ptId; } } } } } return closest; };
publicAPI.findClosestPointWithinRadius = (radius, x, inputDataLength = 0) => { publicAPI.buildLocator(); let closest = -1; const radius2 = radius * radius; let minDist2 = 1.01 * radius2; let dist2 = -1.0; const ijk = publicAPI.getBucketIndices(x); const key = getBucketIndex(ijk); const bucket = model.hashTable.get(key);
if (bucket) { for (let j = 0; j < bucket.length; j++) { const ptId = bucket[j]; const pt = model.dataSet.getPoints().getPoint(ptId); const d2 = vtkMath.distance2BetweenPoints(x, pt); if (d2 < minDist2) { closest = ptId; minDist2 = d2; dist2 = d2; } } }
let refinedRadius; let refinedRadius2; if (minDist2 < radius2) { refinedRadius = Math.sqrt(dist2); refinedRadius2 = dist2; } else { refinedRadius = radius; refinedRadius2 = radius2; }
if (inputDataLength !== 0.0) { const distance2ToDataBounds = vtkBoundingBox.distance2ToBounds( x, model.bounds ); const maxDistance = Math.sqrt(distance2ToDataBounds) + inputDataLength; if (refinedRadius > maxDistance) { refinedRadius = maxDistance; refinedRadius2 = maxDistance * maxDistance; } }
const radiusLevels = [0, 0, 0]; for (let i = 0; i < 3; i++) { radiusLevels[i] = Math.floor(refinedRadius / model.H[i]); if (radiusLevels[i] > model.divisions[i] / 2) { radiusLevels[i] = Math.floor(model.divisions[i] / 2); } } let radiusLevel = Math.max(...radiusLevels); if (radiusLevel === 0) { radiusLevel = 1; }
for (let ii = radiusLevel; ii >= 1; ii--) { const currentRadius = refinedRadius;
const buckets = getOverlappingBuckets(x, ijk, refinedRadius / ii, ii);
for (let i = 0; i < buckets.length; i++) { const nei = buckets[i]; const d2ToBucket = distance2ToBucket(x, nei); if (d2ToBucket < refinedRadius2) { const key1 = getBucketIndex(nei); const bucket1 = model.hashTable.get(key1); if (bucket1) { for (let j = 0; j < bucket1.length; j++) { const ptId = bucket1[j]; const pt = model.dataSet.getPoints().getPoint(ptId); if (pt) { const d2 = vtkMath.distance2BetweenPoints(x, pt); if (d2 < minDist2) { closest = ptId; minDist2 = d2; refinedRadius = Math.sqrt(minDist2); refinedRadius2 = minDist2; dist2 = d2; } } } } } }
if (refinedRadius < currentRadius && ii > 2) { ii = Math.floor(ii * (refinedRadius / currentRadius)) + 1; if (ii < 2) { ii = 2; } } }
if (closest !== -1 && minDist2 <= radius * radius) { dist2 = minDist2; } else { closest = -1; }
return { id: closest, dist2 }; };
publicAPI.findClosestInsertedPoint = (x) => { for (let i = 0; i < 3; i++) { if (x[i] < model.bounds[2 * i] || x[i] > model.bounds[2 * i + 1]) { return -1; } }
const ijk = publicAPI.getBucketIndices(x); const numDivs = model.divisions; let closest = -1; let minDist2 = Number.MAX_VALUE; let level = 0; const maxLevel = Math.max(numDivs[0], numDivs[1], numDivs[2]); const points = model.points;
for (; closest === -1 && level < maxLevel; level++) { const neighbors = getBucketNeighbors(ijk, numDivs, level); for (let i = 0; i < neighbors.length; i++) { const nei = neighbors[i]; const cno = nei[0] + nei[1] * model.XD + nei[2] * model.sliceSize; const bucket = model.hashTable.get(cno); if (bucket) { for (let j = 0; j < bucket.length; j++) { const ptId = bucket[j]; const pt = points.getPoint(ptId); const dist2 = vtkMath.distance2BetweenPoints(x, pt); if (dist2 < minDist2) { closest = ptId; minDist2 = dist2; } } } } }
const refineNeighbors = getBucketNeighbors(ijk, numDivs, level); for (let i = 0; i < refineNeighbors.length; i++) { const nei = refineNeighbors[i]; let dist2 = 0; for (let j = 0; j < 3; j++) { if (ijk[j] !== nei[j]) { const MULTIPLES = ijk[j] > nei[j] ? nei[j] + 1 : nei[j]; const diff = model.bounds[2 * j] + MULTIPLES * model.H[j] - x[j]; dist2 += diff * diff; } } if (dist2 < minDist2) { const cno = nei[0] + nei[1] * model.XD + nei[2] * model.sliceSize; const bucket = model.hashTable.get(cno); if (bucket) { for (let j = 0; j < bucket.length; j++) { const ptId = bucket[j]; const pt = points.getPoint(ptId); const d2 = vtkMath.distance2BetweenPoints(x, pt); if (d2 < minDist2) { closest = ptId; minDist2 = d2; } } } } }
return closest; };
publicAPI.getPointsInBucket = (x) => { publicAPI.buildLocator(); const key = publicAPI.getBucketIndex(x); const bucket = model.hashTable.get(key);
if (!bucket) return []; return bucket; };
publicAPI.freeSearchStructure = () => { model.hashTable.clear(); model.points = vtkPoints.newInstance(); model.divisions = [50, 50, 50]; vtkMath.uninitializeBounds(model.bounds); };
publicAPI.computePerformanceFactors = () => { model.HX = model.H[0]; model.HY = model.H[1]; model.HZ = model.H[2]; model.FX = 1.0 / model.H[0]; model.FY = 1.0 / model.H[1]; model.FZ = 1.0 / model.H[2]; model.BX = model.bounds[0]; model.BY = model.bounds[2]; model.BZ = model.bounds[4]; model.XD = model.divisions[0]; model.YD = model.divisions[1]; model.ZD = model.divisions[2]; model.sliceSize = model.divisions[0] * model.divisions[1]; };
publicAPI.generateRepresentation = (polydata) => { if (!model.hashTable || model.hashTable.size === 0) { vtkErrorMacro("Can't build representation, no data provided!"); return; }
const facePts = []; facePts.length = 4;
function generateFace(face, i, j, k, pts, polys) { const x0 = model.bounds[0] + i * model.HX; const y0 = model.bounds[2] + j * model.HY; const z0 = model.bounds[4] + k * model.HZ; const x1 = x0 + model.HX; const y1 = y0 + model.HY; const z1 = z0 + model.HZ;
if (face === 0) { facePts[0] = [x0, y0, z0]; facePts[1] = [x0, y1, z0]; facePts[2] = [x0, y1, z1]; facePts[3] = [x0, y0, z1]; } else if (face === 1) { facePts[0] = [x0, y0, z0]; facePts[1] = [x1, y0, z0]; facePts[2] = [x1, y0, z1]; facePts[3] = [x0, y0, z1]; } else if (face === 2) { facePts[0] = [x0, y0, z0]; facePts[1] = [x1, y0, z0]; facePts[2] = [x1, y1, z0]; facePts[3] = [x0, y1, z0]; }
const ptIds = facePts.map((pt) => pts.insertNextPoint(...pt)); polys.insertNextCell([ptIds[0], ptIds[1], ptIds[2], ptIds[3]]); }
const pts = vtkPoints.newInstance(); pts.allocate(5000); const polys = vtkCellArray.newInstance(); polys.allocate(2048); const divisions = model.divisions; const sliceSize = divisions[0] * divisions[1];
function hasBucket(i, j, k) { if ( i < 0 || i >= divisions[0] || j < 0 || j >= divisions[1] || k < 0 || k >= divisions[2] ) { return false; } const idx = i + j * divisions[0] + k * sliceSize; return model.hashTable.has(idx); }
for (let k = 0; k < divisions[2]; k++) { for (let j = 0; j < divisions[1]; j++) { for (let i = 0; i < divisions[0]; i++) { const idx = i + j * divisions[0] + k * sliceSize; const inside = model.hashTable.has(idx);
for (let axis = 0; axis < 3; axis++) { let ni = i; let nj = j; let nk = k; if (axis === 0) ni = i - 1; if (axis === 1) nj = j - 1; if (axis === 2) nk = k - 1;
const neighborInside = hasBucket(ni, nj, nk);
if (ni < 0 || nj < 0 || nk < 0) { if (inside) { generateFace(axis, i, j, k, pts, polys); } } else if ( (neighborInside && !inside) || (!neighborInside && inside) ) { generateFace(axis, i, j, k, pts, polys); } }
if (i + 1 >= divisions[0] && inside) { generateFace(0, i + 1, j, k, pts, polys); } if (j + 1 >= divisions[1] && inside) { generateFace(1, i, j + 1, k, pts, polys); } if (k + 1 >= divisions[2] && inside) { generateFace(2, i, j, k + 1, pts, polys); } } } }
polydata.setPoints(pts); polydata.setPolys(polys); }; }
function defaultValues(initialValues) { return { divisions: [50, 50, 50], numberOfPointsPerBucket: 3, bounds: [0, 0, 0, 0, 0, 0], tolerance: 0.001, automatic: true, ...initialValues, }; }
export function extend(publicAPI, model, initialValues = {}) { vtkAbstractPointLocator.extend( publicAPI, model, defaultValues(initialValues) ); macro.setGet(publicAPI, model, ['numberOfPointsPerBucket', 'points']);
macro.setGetArray(publicAPI, model, ['divisions'], 3); vtkMath.uninitializeBounds(model.bounds); model.points = model.points || vtkPoints.newInstance(); model.hashTable = new Map(); model.H = [0, 0, 0]; model.insertionPointId = 0; model.insertionTol2 = 0.0001; model.insertionLevel = 0; vtkPointLocator(publicAPI, model); }
export const newInstance = macro.newInstance(extend, 'vtkPointLocator');
export default { newInstance, extend };
|