mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 13:30:24 +08:00
346 lines
12 KiB
TypeScript
346 lines
12 KiB
TypeScript
/**
|
|
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
|
*
|
|
* @author David Sehnal <david.sehnal@gmail.com>
|
|
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
|
*/
|
|
|
|
import { SpacegroupCell, Box3D, Sphere3D } from '../../mol-math/geometry';
|
|
import { Tensor, Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
|
import { Histogram, calculateHistogram } from '../../mol-math/histogram';
|
|
import { lerp } from '../../mol-math/interpolate';
|
|
|
|
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
|
const v3transformMat4 = Vec3.transformMat4;
|
|
const v3lerp = Vec3.lerp;
|
|
|
|
/** The basic unit cell that contains the grid data. */
|
|
interface Grid {
|
|
readonly transform: Grid.Transform,
|
|
readonly cells: Tensor,
|
|
readonly stats: Readonly<{
|
|
min: number,
|
|
max: number,
|
|
mean: number,
|
|
sigma: number
|
|
}>,
|
|
readonly periodicity?: 'none' | 'xyz'
|
|
}
|
|
|
|
namespace Grid {
|
|
export const One: Grid = {
|
|
transform: { kind: 'matrix', matrix: Mat4.identity() },
|
|
cells: Tensor.create(Tensor.Space([1, 1, 1], [0, 1, 2]), Tensor.Data1([0])),
|
|
stats: { min: 0, max: 0, mean: 0, sigma: 0 },
|
|
};
|
|
|
|
export type Transform = { kind: 'spacegroup', cell: SpacegroupCell, fractionalBox: Box3D } | { kind: 'matrix', matrix: Mat4 }
|
|
|
|
const _scale = Mat4(), _translate = Mat4();
|
|
export function getGridToCartesianTransform(grid: Grid) {
|
|
if (grid.transform.kind === 'matrix') {
|
|
return Mat4.copy(Mat4(), grid.transform.matrix);
|
|
}
|
|
|
|
if (grid.transform.kind === 'spacegroup') {
|
|
const { cells: { space } } = grid;
|
|
const scale = Mat4.fromScaling(_scale, Vec3.div(Vec3(), Box3D.size(Vec3(), grid.transform.fractionalBox), Vec3.ofArray(space.dimensions)));
|
|
const translate = Mat4.fromTranslation(_translate, grid.transform.fractionalBox.min);
|
|
return Mat4.mul3(Mat4(), grid.transform.cell.fromFractional, translate, scale);
|
|
}
|
|
|
|
return Mat4.identity();
|
|
}
|
|
|
|
export function areEquivalent(gridA: Grid, gridB: Grid) {
|
|
return gridA === gridB;
|
|
}
|
|
|
|
export function isEmpty(grid: Grid) {
|
|
return grid.cells.data.length === 0;
|
|
}
|
|
|
|
export function getBoundingSphere(grid: Grid, boundingSphere?: Sphere3D) {
|
|
if (!boundingSphere) boundingSphere = Sphere3D();
|
|
|
|
const dimensions = grid.cells.space.dimensions as Vec3;
|
|
const transform = Grid.getGridToCartesianTransform(grid);
|
|
return Sphere3D.fromDimensionsAndTransform(boundingSphere, dimensions, transform);
|
|
}
|
|
|
|
const _isoBbox = Box3D();
|
|
export function getIsosurfaceBoundingSphere(grid: Grid, isoValue: number, boundingSphere?: Sphere3D) {
|
|
const neg = isoValue < 0;
|
|
|
|
const c = [0, 0, 0];
|
|
const getCoords = grid.cells.space.getCoords;
|
|
const d = grid.cells.data;
|
|
const [xn, yn, zn] = grid.cells.space.dimensions;
|
|
|
|
let minx = xn - 1, miny = yn - 1, minz = zn - 1;
|
|
let maxx = 0, maxy = 0, maxz = 0;
|
|
for (let i = 0, il = d.length; i < il; ++i) {
|
|
if ((neg && d[i] <= isoValue) || (!neg && d[i] >= isoValue)) {
|
|
getCoords(i, c);
|
|
if (c[0] < minx) minx = c[0];
|
|
if (c[1] < miny) miny = c[1];
|
|
if (c[2] < minz) minz = c[2];
|
|
if (c[0] > maxx) maxx = c[0];
|
|
if (c[1] > maxy) maxy = c[1];
|
|
if (c[2] > maxz) maxz = c[2];
|
|
}
|
|
}
|
|
|
|
Vec3.set(_isoBbox.min, minx - 1, miny - 1, minz - 1);
|
|
Vec3.set(_isoBbox.max, maxx + 1, maxy + 1, maxz + 1);
|
|
const transform = Grid.getGridToCartesianTransform(grid);
|
|
Box3D.transform(_isoBbox, _isoBbox, transform);
|
|
return Sphere3D.fromBox3D(boundingSphere || Sphere3D(), _isoBbox);
|
|
}
|
|
|
|
/**
|
|
* Compute histogram with given bin count.
|
|
* Cached on the Grid object.
|
|
*/
|
|
export function getHistogram(grid: Grid, binCount: number) {
|
|
let histograms = (grid as any)._historams as { [binCount: number]: Histogram };
|
|
if (!histograms) {
|
|
histograms = (grid as any)._historams = { };
|
|
}
|
|
if (!histograms[binCount]) {
|
|
histograms[binCount] = calculateHistogram(grid.cells.data, binCount, { min: grid.stats.min, max: grid.stats.max });
|
|
}
|
|
return histograms[binCount];
|
|
}
|
|
|
|
export function makeGetTrilinearlyInterpolated(grid: Grid, transform: 'none' | 'relative') {
|
|
const cartnToGrid = Grid.getGridToCartesianTransform(grid);
|
|
Mat4.invert(cartnToGrid, cartnToGrid);
|
|
const gridCoords = Vec3();
|
|
|
|
const { stats } = grid;
|
|
const { dimensions, get } = grid.cells.space;
|
|
const data = grid.cells.data;
|
|
|
|
const [mi, mj, mk] = dimensions;
|
|
const getValue = (i: number, j: number, k: number) => get(data, i, j, k);
|
|
const isPeriodic = grid.periodicity === 'xyz';
|
|
|
|
return function getTrilinearlyInterpolated(position: Vec3): number {
|
|
v3transformMat4(gridCoords, position, cartnToGrid);
|
|
|
|
const value = trilinearlyInterpolate(gridCoords, mi, mj, mk, isPeriodic, getValue);
|
|
if (Number.isNaN(value)) return value;
|
|
|
|
if (transform === 'relative') {
|
|
return (value - stats.mean) / stats.sigma;
|
|
} else {
|
|
return value;
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Core trilinear interpolation function.
|
|
* @param gridCoords - Position in grid coordinates (fractional indices)
|
|
* @param mi, mj, mk - Grid dimensions
|
|
* @param getValue - Function to get value at integer grid coordinates
|
|
* @returns Interpolated value or NaN if out of bounds
|
|
*/
|
|
export function trilinearlyInterpolate(
|
|
gridCoords: Vec3,
|
|
mi: number, mj: number, mk: number,
|
|
isPeriodic: boolean,
|
|
getValue: (i: number, j: number, k: number) => number
|
|
): number {
|
|
const i = Math.trunc(gridCoords[0]);
|
|
const j = Math.trunc(gridCoords[1]);
|
|
const k = Math.trunc(gridCoords[2]);
|
|
|
|
if (!isPeriodic && (i < 0 || i >= mi || j < 0 || j >= mj || k < 0 || k >= mk)) {
|
|
return Number.NaN;
|
|
}
|
|
|
|
const u = gridCoords[0] - i;
|
|
const v = gridCoords[1] - j;
|
|
const w = gridCoords[2] - k;
|
|
|
|
const ii = Math.min(i + 1, mi - 1);
|
|
const jj = Math.min(j + 1, mj - 1);
|
|
const kk = Math.min(k + 1, mk - 1);
|
|
|
|
let a = getValue(i, j, k);
|
|
let b = getValue(ii, j, k);
|
|
let c = getValue(i, jj, k);
|
|
let d = getValue(ii, jj, k);
|
|
const x = lerp(lerp(a, b, u), lerp(c, d, u), v);
|
|
|
|
a = getValue(i, j, kk);
|
|
b = getValue(ii, j, kk);
|
|
c = getValue(i, jj, kk);
|
|
d = getValue(ii, jj, kk);
|
|
const y = lerp(lerp(a, b, u), lerp(c, d, u), v);
|
|
|
|
return lerp(x, y, w);
|
|
}
|
|
|
|
const _a = Vec3(), _b = Vec3(), _c = Vec3(), _d = Vec3();
|
|
const _ab = Vec3(), _cd = Vec3(), _x = Vec3(), _y = Vec3();
|
|
|
|
/**
|
|
* Core trilinear interpolation function for Vec3 values.
|
|
* More efficient for interleaved data like gradients since getValue is called once per grid point.
|
|
* @param gridCoords - Position in grid coordinates (fractional indices)
|
|
* @param mi, mj, mk - Grid dimensions
|
|
* @param getValue - Function to get Vec3 value at integer grid coordinates
|
|
* @param out - Output Vec3
|
|
* @returns true if interpolation succeeded, false if out of bounds
|
|
*/
|
|
export function trilinearlyInterpolateVec3(
|
|
gridCoords: Vec3,
|
|
mi: number, mj: number, mk: number,
|
|
getValue: (i: number, j: number, k: number, out: Vec3) => void,
|
|
out: Vec3
|
|
): boolean {
|
|
const i = Math.trunc(gridCoords[0]);
|
|
const j = Math.trunc(gridCoords[1]);
|
|
const k = Math.trunc(gridCoords[2]);
|
|
|
|
if (i < 0 || i >= mi || j < 0 || j >= mj || k < 0 || k >= mk) {
|
|
return false;
|
|
}
|
|
|
|
const u = gridCoords[0] - i;
|
|
const v = gridCoords[1] - j;
|
|
const w = gridCoords[2] - k;
|
|
|
|
const ii = Math.min(i + 1, mi - 1);
|
|
const jj = Math.min(j + 1, mj - 1);
|
|
const kk = Math.min(k + 1, mk - 1);
|
|
|
|
// Interpolate in the k plane
|
|
getValue(i, j, k, _a);
|
|
getValue(ii, j, k, _b);
|
|
getValue(i, jj, k, _c);
|
|
getValue(ii, jj, k, _d);
|
|
v3lerp(_ab, _a, _b, u);
|
|
v3lerp(_cd, _c, _d, u);
|
|
v3lerp(_x, _ab, _cd, v);
|
|
|
|
// Interpolate in the k+1 plane
|
|
getValue(i, j, kk, _a);
|
|
getValue(ii, j, kk, _b);
|
|
getValue(i, jj, kk, _c);
|
|
getValue(ii, jj, kk, _d);
|
|
v3lerp(_ab, _a, _b, u);
|
|
v3lerp(_cd, _c, _d, u);
|
|
v3lerp(_y, _ab, _cd, v);
|
|
|
|
// Final interpolation between planes
|
|
v3lerp(out, _x, _y, w);
|
|
return true;
|
|
}
|
|
|
|
type Gradients = {
|
|
values: Float32Array,
|
|
magnitude: {
|
|
min: number,
|
|
max: number,
|
|
mean: number,
|
|
sigma: number
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Pre-compute gradients at each grid cell using central differences.
|
|
* Returns a single Float32Array with interleaved xyz components (x1, y1, z1, x2, y2, z2, ...).
|
|
* Cached on the Grid object.
|
|
*/
|
|
export function getGradients(grid: Grid): Gradients {
|
|
if ((grid as any)._gradients) {
|
|
return (grid as any)._gradients as Gradients;
|
|
}
|
|
|
|
const gradients = computeGradients(grid);
|
|
(grid as any)._gradients = gradients;
|
|
return gradients;
|
|
}
|
|
|
|
function computeGradients(grid: Grid): Gradients {
|
|
const { dimensions, get, dataOffset } = grid.cells.space;
|
|
const data = grid.cells.data;
|
|
const [mi, mj, mk] = dimensions;
|
|
|
|
const n = mi * mj * mk;
|
|
const values = new Float32Array(n * 3);
|
|
|
|
let min = Infinity;
|
|
let max = -Infinity;
|
|
let sum = 0;
|
|
let sumSq = 0;
|
|
|
|
for (let k = 0; k < mk; ++k) {
|
|
for (let j = 0; j < mj; ++j) {
|
|
for (let i = 0; i < mi; ++i) {
|
|
const idx = dataOffset(i, j, k) * 3;
|
|
|
|
// Use central differences where possible, forward/backward at boundaries
|
|
const im = Math.max(0, i - 1);
|
|
const ip = Math.min(mi - 1, i + 1);
|
|
const jm = Math.max(0, j - 1);
|
|
const jp = Math.min(mj - 1, j + 1);
|
|
const km = Math.max(0, k - 1);
|
|
const kp = Math.min(mk - 1, k + 1);
|
|
|
|
// Gradient components (using central differences with proper divisor)
|
|
const gx = (get(data, ip, j, k) - get(data, im, j, k)) / (ip - im || 1);
|
|
const gy = (get(data, i, jp, k) - get(data, i, jm, k)) / (jp - jm || 1);
|
|
const gz = (get(data, i, j, kp) - get(data, i, j, km)) / (kp - km || 1);
|
|
|
|
values[idx] = gx;
|
|
values[idx + 1] = gy;
|
|
values[idx + 2] = gz;
|
|
|
|
const mag = gx * gx + gy * gy + gz * gz;
|
|
if (mag < min) min = mag;
|
|
if (mag > max) max = mag;
|
|
sum += mag;
|
|
sumSq += mag * mag;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (min === Infinity) min = 0;
|
|
if (max === -Infinity) max = 1;
|
|
min = Math.sqrt(min);
|
|
max = Math.sqrt(max);
|
|
|
|
const mean = sum / n;
|
|
const sigma = Math.sqrt(sumSq / n);
|
|
|
|
return { values, magnitude: { min, max, mean, sigma } };
|
|
}
|
|
|
|
/**
|
|
* Create a function that returns trilinearly interpolated gradient at a grid position.
|
|
* The gradient is pre-computed at grid cells and interpolated for smooth results.
|
|
*/
|
|
export function makeGetInterpolatedGradient(grid: Grid) {
|
|
const { values: g } = getGradients(grid);
|
|
const { dimensions, dataOffset } = grid.cells.space;
|
|
const [mi, mj, mk] = dimensions;
|
|
|
|
const getGradientVec3 = (i: number, j: number, k: number, out: Vec3) => {
|
|
const idx = dataOffset(i, j, k) * 3;
|
|
out[0] = g[idx];
|
|
out[1] = g[idx + 1];
|
|
out[2] = g[idx + 2];
|
|
};
|
|
|
|
return function getInterpolatedGradient(gridCoords: Vec3, out: Vec3): boolean {
|
|
return trilinearlyInterpolateVec3(gridCoords, mi, mj, mk, getGradientVec3, out);
|
|
};
|
|
}
|
|
}
|
|
|
|
export { Grid }; |