UNPKG

molstar

Version:

A comprehensive macromolecular library.

167 lines (166 loc) 6.79 kB
/** * Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Ludovic Autin <autin@scripps.edu> * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { ParamDefinition as PD } from '../../../mol-util/param-definition.js'; import { PCG } from '../../../mol-data/util/hash-functions.js'; import { Vec3 } from '../../../mol-math/linear-algebra.js'; import { Grid } from '../../../mol-model/volume/grid.js'; // avoiding namespace lookup improved performance in Chrome (Aug 2020) const v3fromArray = Vec3.fromArray; const v3distance = Vec3.distance; const v3setMagnitude = Vec3.setMagnitude; const v3create = Vec3.create; const v3copy = Vec3.copy; const v3add = Vec3.add; export const BasicStreamlineCalculationParams = { seedDensity: PD.Numeric(10, { min: 1, max: 30, step: 1 }, { description: 'Percentage of cells with seed.' }), stepSize: PD.Numeric(0.35, { min: 0.01, max: 1, step: 0.01 }, { description: 'Step size in grid space.' }), minSeparation: PD.Numeric(0, { min: 0, max: 5, step: 0.5 }, { description: 'Minimum separation between streamlines in grid space. 0 to disable.' }), }; const d = Vec3(); const p = Vec3(); const g = Vec3(); const prev = Vec3(); /** * Basic tracer with fixed step size in grid space */ function traceOneDirection(out, grid, getGradient, seed, stepSize, dir, occupancy) { const { space } = grid.cells; const [nx, ny, nz] = space.dimensions; const o = space.dataOffset; const step = dir * stepSize; const maxSteps = Math.max(nx, ny, nz) * 5 / stepSize; const t = stepSize / 1.1; // tolerance for writing points const seenVoxel = new Map(); const maxVoxelVisits = Math.ceil(2 / stepSize); v3copy(p, seed); let written = 0; for (let i = 0; i < maxSteps; ++i) { // boundary check if (p[0] < 1 || p[0] > nx - 2 || p[1] < 1 || p[1] > ny - 2 || p[2] < 1 || p[2] > nz - 2) break; if (!getGradient(p, g)) break; const key = o(Math.round(p[0]), Math.round(p[1]), Math.round(p[2])); // stop if entering a cell already claimed by another streamline if (occupancy && occupancy[key]) break; const c = (seenVoxel.get(key) || 0) + 1; seenVoxel.set(key, c); if (c > maxVoxelVisits) break; v3setMagnitude(d, g, step); // write midpoint (keeps lines smooth and avoids tiny box at start) const midX = p[0] + 0.5 * d[0]; const midY = p[1] + 0.5 * d[1]; const midZ = p[2] + 0.5 * d[2]; const m = v3create(midX, midY, midZ); if (v3distance(prev, m) >= t) { out.push(m); v3copy(prev, m); } written++; v3add(p, p, d); // advance } return written; } function traceStreamlineBothDirs(grid, getGradient, seed, stepSize, occupancy) { const line = []; const nBack = traceOneDirection(line, grid, getGradient, seed, stepSize, -1, occupancy); if (nBack > 1) line.reverse(); traceOneDirection(line, grid, getGradient, seed, stepSize, +1, occupancy); return line; } async function computeBasicStreamlines(ctx, grid, props) { const { space } = grid.cells; const [nx, ny, nz] = space.dimensions; const o = space.dataOffset; const { seedDensity, stepSize, minSeparation } = props; const seedStep = Math.max(1, Math.floor(Math.min(nx, ny, nz) / seedDensity)); // bounds avoiding edges const xStart = 1, xEnd = nx - 2 - seedStep; const yStart = 1, yEnd = ny - 2 - seedStep; const zStart = 1, zEnd = nz - 2 - seedStep; const pcg = new PCG(); const seeds = []; for (let z = zStart; z <= zEnd; z += seedStep) { for (let y = yStart; y <= yEnd; y += seedStep) { for (let x = xStart; x <= xEnd; x += seedStep) { seeds.push(x + pcg.float() * seedStep, y + pcg.float() * seedStep, z + pcg.float() * seedStep); } } } // shuffle in-place by triplets [x,y,z] for (let i = (seeds.length / 3) - 1; i > 0; i--) { const j = Math.floor(pcg.float() * (i + 1)); const ia = i * 3, ja = j * 3; // swap 3 numbers at once const tx = seeds[ia], ty = seeds[ia + 1], tz = seeds[ia + 2]; seeds[ia] = seeds[ja]; seeds[ia + 1] = seeds[ja + 1]; seeds[ia + 2] = seeds[ja + 2]; seeds[ja] = tx; seeds[ja + 1] = ty; seeds[ja + 2] = tz; } await ctx.update({ isIndeterminate: false, current: 0, max: seeds.length / 3 }); const getGradient = Grid.makeGetInterpolatedGradient(grid); // occupancy grid for minimum separation between streamlines const useSeparation = minSeparation > 0; const occupancy = useSeparation ? new Uint8Array(nx * ny * nz) : undefined; // precompute sphere offsets for marking occupancy const sphereOffsets = []; if (useSeparation) { const r = Math.ceil(minSeparation); const r2 = minSeparation * minSeparation; for (let dz = -r; dz <= r; ++dz) { for (let dy = -r; dy <= r; ++dy) { for (let dx = -r; dx <= r; ++dx) { if (dx * dx + dy * dy + dz * dz <= r2) { sphereOffsets.push([dx, dy, dz]); } } } } } const lines = []; const pos = Vec3(); for (let i = 0; i < seeds.length; i += 3) { if (ctx.shouldUpdate) await ctx.update({ current: i / 3 + 1 }); v3fromArray(pos, seeds, i); const line = traceStreamlineBothDirs(grid, getGradient, pos, stepSize, occupancy); if (line.length * stepSize >= 3) { lines.push(line); // mark occupancy around accepted streamline points if (occupancy) { for (let j = 0, jl = line.length; j < jl; ++j) { const pt = line[j]; const cx = Math.round(pt[0]); const cy = Math.round(pt[1]); const cz = Math.round(pt[2]); for (let k = 0, kl = sphereOffsets.length; k < kl; ++k) { const off = sphereOffsets[k]; const ox = cx + off[0]; const oy = cy + off[1]; const oz = cz + off[2]; if (ox >= 0 && ox < nx && oy >= 0 && oy < ny && oz >= 0 && oz < nz) { occupancy[o(ox, oy, oz)] = 1; } } } } } } return lines; } // export async function calculateBasicStreamlines(ctx, volume, props) { return computeBasicStreamlines(ctx.runtime, volume.grid, props); }