grid.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { Result, Lookup3D } from './common'
  8. import { Box3D } from '../primitives/box3d';
  9. import { Sphere3D } from '../primitives/sphere3d';
  10. import { PositionData } from '../common';
  11. import { Vec3 } from '../../linear-algebra';
  12. import { OrderedSet } from 'mol-data/int';
  13. interface GridLookup3D<T = number> extends Lookup3D<T> {
  14. readonly buckets: { readonly offset: ArrayLike<number>, readonly count: ArrayLike<number>, readonly array: ArrayLike<number> }
  15. }
  16. function GridLookup3D(data: PositionData, cellSize?: Vec3): GridLookup3D {
  17. return new GridLookup3DImpl(data, cellSize);
  18. }
  19. export { GridLookup3D }
  20. class GridLookup3DImpl implements GridLookup3D<number> {
  21. private ctx: QueryContext;
  22. boundary: Lookup3D['boundary'];
  23. buckets: GridLookup3D['buckets'];
  24. find(x: number, y: number, z: number, radius: number): Result<number> {
  25. this.ctx.x = x;
  26. this.ctx.y = y;
  27. this.ctx.z = z;
  28. this.ctx.radius = radius;
  29. this.ctx.isCheck = false;
  30. query(this.ctx);
  31. return this.ctx.result;
  32. }
  33. check(x: number, y: number, z: number, radius: number): boolean {
  34. this.ctx.x = x;
  35. this.ctx.y = y;
  36. this.ctx.z = z;
  37. this.ctx.radius = radius;
  38. this.ctx.isCheck = true;
  39. return query(this.ctx);
  40. }
  41. constructor(data: PositionData, cellSize?: Vec3) {
  42. const structure = build(data, cellSize);
  43. this.ctx = createContext(structure);
  44. this.boundary = { box: structure.boundingBox, sphere: structure.boundingSphere };
  45. this.buckets = { offset: structure.bucketOffset, count: structure.bucketCounts, array: structure.bucketArray };
  46. }
  47. }
  48. /**
  49. * Adapted from https://github.com/arose/ngl
  50. * MIT License Copyright (C) 2014+ Alexander Rose
  51. */
  52. interface InputData {
  53. x: ArrayLike<number>,
  54. y: ArrayLike<number>,
  55. z: ArrayLike<number>,
  56. indices: OrderedSet,
  57. radius?: ArrayLike<number>,
  58. }
  59. interface Grid3D {
  60. size: number[],
  61. min: number[],
  62. grid: Uint32Array,
  63. delta: number[],
  64. bucketOffset: Uint32Array,
  65. bucketCounts: Int32Array,
  66. bucketArray: Int32Array,
  67. data: InputData,
  68. maxRadius: number,
  69. expandedBox: Box3D,
  70. boundingBox: Box3D,
  71. boundingSphere: Sphere3D
  72. }
  73. interface BuildState {
  74. size: number[],
  75. delta: number[],
  76. data: InputData,
  77. expandedBox: Box3D,
  78. boundingBox: Box3D,
  79. boundingSphere: Sphere3D,
  80. elementCount: number
  81. }
  82. function _build(state: BuildState): Grid3D {
  83. const { expandedBox, size: [sX, sY, sZ], data: { x: px, y: py, z: pz, radius, indices }, elementCount, delta } = state;
  84. const n = sX * sY * sZ;
  85. const { min: [minX, minY, minZ] } = expandedBox;
  86. let maxRadius = 0;
  87. let bucketCount = 0;
  88. const grid = new Uint32Array(n);
  89. const bucketIndex = new Int32Array(elementCount);
  90. for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
  91. const i = OrderedSet.getAt(indices, t);
  92. const x = Math.floor((px[i] - minX) / delta[0]);
  93. const y = Math.floor((py[i] - minY) / delta[1]);
  94. const z = Math.floor((pz[i] - minZ) / delta[2]);
  95. const idx = (((x * sY) + y) * sZ) + z;
  96. if ((grid[idx] += 1) === 1) {
  97. bucketCount += 1
  98. }
  99. bucketIndex[t] = idx;
  100. }
  101. if (radius) {
  102. for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
  103. const i = OrderedSet.getAt(indices, t);
  104. if (radius[i] > maxRadius) maxRadius = radius[i];
  105. }
  106. }
  107. const bucketCounts = new Int32Array(bucketCount);
  108. for (let i = 0, j = 0; i < n; i++) {
  109. const c = grid[i];
  110. if (c > 0) {
  111. grid[i] = j + 1;
  112. bucketCounts[j] = c;
  113. j += 1;
  114. }
  115. }
  116. const bucketOffset = new Uint32Array(bucketCount);
  117. for (let i = 1; i < bucketCount; ++i) {
  118. bucketOffset[i] += bucketOffset[i - 1] + bucketCounts[i - 1];
  119. }
  120. const bucketFill = new Int32Array(bucketCount);
  121. const bucketArray = new Int32Array(elementCount);
  122. for (let i = 0; i < elementCount; i++) {
  123. const bucketIdx = grid[bucketIndex[i]]
  124. if (bucketIdx > 0) {
  125. const k = bucketIdx - 1;
  126. bucketArray[bucketOffset[k] + bucketFill[k]] = i;
  127. bucketFill[k] += 1;
  128. }
  129. }
  130. return {
  131. size: state.size,
  132. bucketArray,
  133. bucketCounts,
  134. bucketOffset,
  135. grid,
  136. delta,
  137. min: state.expandedBox.min,
  138. data: state.data,
  139. maxRadius,
  140. expandedBox: state.expandedBox,
  141. boundingBox: state.boundingBox,
  142. boundingSphere: state.boundingSphere
  143. }
  144. }
  145. function build(data: PositionData, cellSize?: Vec3) {
  146. const boundingBox = Box3D.computeBounding(data);
  147. // need to expand the grid bounds to avoid rounding errors
  148. const expandedBox = Box3D.expand(Box3D.empty(), boundingBox, Vec3.create(0.5, 0.5, 0.5));
  149. const boundingSphere = Sphere3D.computeBounding(data);
  150. const { indices } = data;
  151. const S = Vec3.sub(Vec3.zero(), expandedBox.max, expandedBox.min);
  152. let delta, size;
  153. const elementCount = OrderedSet.size(indices);
  154. if (cellSize) {
  155. size = [Math.ceil(S[0] / cellSize[0]), Math.ceil(S[1] / cellSize[1]), Math.ceil(S[2] / cellSize[2])];
  156. delta = cellSize;
  157. } else if (elementCount > 0) {
  158. // size of the box
  159. // required "grid volume" so that each cell contains on average 32 elements.
  160. const V = Math.ceil(elementCount / 32);
  161. const f = Math.pow(V / (S[0] * S[1] * S[2]), 1 / 3);
  162. size = [Math.ceil(S[0] * f), Math.ceil(S[1] * f), Math.ceil(S[2] * f)];
  163. delta = [S[0] / size[0], S[1] / size[1], S[2] / size[2]];
  164. } else {
  165. delta = S;
  166. size = [1, 1, 1];
  167. }
  168. const inputData: InputData = {
  169. x: data.x,
  170. y: data.y,
  171. z: data.z,
  172. indices,
  173. radius: data.radius
  174. };
  175. const state: BuildState = {
  176. size,
  177. data: inputData,
  178. expandedBox,
  179. boundingBox,
  180. boundingSphere,
  181. elementCount: elementCount,
  182. delta
  183. }
  184. return _build(state);
  185. }
  186. interface QueryContext {
  187. grid: Grid3D,
  188. x: number,
  189. y: number,
  190. z: number,
  191. radius: number,
  192. result: Result<number>,
  193. isCheck: boolean
  194. }
  195. function createContext(grid: Grid3D): QueryContext {
  196. return { grid, x: 0.1, y: 0.1, z: 0.1, radius: 0.1, result: Result.create(), isCheck: false }
  197. }
  198. function query(ctx: QueryContext): boolean {
  199. const { min, size: [sX, sY, sZ], bucketOffset, bucketCounts, bucketArray, grid, data: { x: px, y: py, z: pz, indices, radius }, delta, maxRadius } = ctx.grid;
  200. const { radius: inputRadius, isCheck, x, y, z, result } = ctx;
  201. const r = inputRadius + maxRadius;
  202. const rSq = r * r;
  203. Result.reset(result);
  204. const loX = Math.max(0, Math.floor((x - r - min[0]) / delta[0]));
  205. const loY = Math.max(0, Math.floor((y - r - min[1]) / delta[1]));
  206. const loZ = Math.max(0, Math.floor((z - r - min[2]) / delta[2]));
  207. const hiX = Math.min(sX - 1, Math.floor((x + r - min[0]) / delta[0]));
  208. const hiY = Math.min(sY - 1, Math.floor((y + r - min[1]) / delta[1]));
  209. const hiZ = Math.min(sZ - 1, Math.floor((z + r - min[2]) / delta[2]));
  210. if (loX > hiX || loY > hiY || loZ > hiZ) return false;
  211. for (let ix = loX; ix <= hiX; ix++) {
  212. for (let iy = loY; iy <= hiY; iy++) {
  213. for (let iz = loZ; iz <= hiZ; iz++) {
  214. const bucketIdx = grid[(((ix * sY) + iy) * sZ) + iz];
  215. if (bucketIdx === 0) continue;
  216. const k = bucketIdx - 1;
  217. const offset = bucketOffset[k];
  218. const count = bucketCounts[k];
  219. const end = offset + count;
  220. for (let i = offset; i < end; i++) {
  221. const idx = OrderedSet.getAt(indices, bucketArray[i]);
  222. const dx = px[idx] - x;
  223. const dy = py[idx] - y;
  224. const dz = pz[idx] - z;
  225. const distSq = dx * dx + dy * dy + dz * dz;
  226. if (distSq <= rSq) {
  227. if (maxRadius > 0 && Math.sqrt(distSq) - radius![idx] > inputRadius) continue;
  228. if (isCheck) return true;
  229. Result.add(result, bucketArray[i], distSq);
  230. }
  231. }
  232. }
  233. }
  234. }
  235. return result.count > 0;
  236. }