molstar
Version:
A comprehensive macromolecular library.
290 lines (289 loc) • 12.9 kB
JavaScript
/**
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { ParamDefinition as PD } from '../../mol-util/param-definition.js';
import { Grid, Volume } from '../../mol-model/volume.js';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh.js';
import { VolumeRepresentation, VolumeRepresentationProvider } from './representation.js';
import { Representation } from '../representation.js';
import { PickingId } from '../../mol-geo/geometry/picking.js';
import { EmptyLoci } from '../../mol-model/loci.js';
import { createVolumeCellLocationIterator, eachVolumeLoci } from './util.js';
import { BaseGeometry } from '../../mol-geo/geometry/base.js';
import { Spheres } from '../../mol-geo/geometry/spheres/spheres.js';
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder.js';
import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder.js';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3.js';
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere.js';
import { sphereVertexCount } from '../../mol-geo/primitive/sphere.js';
import { Points } from '../../mol-geo/geometry/points/points.js';
import { PointsBuilder } from '../../mol-geo/geometry/points/points-builder.js';
import { Mat4 } from '../../mol-math/linear-algebra.js';
import { Interval } from '../../mol-data/int/interval.js';
import { OrderedSet } from '../../mol-data/int/ordered-set.js';
import { PCG } from '../../mol-data/util/hash-functions.js';
import { VolumeVisual } from './visual.js';
export const VolumeDotParams = {
isoValue: Volume.IsoValueParam,
perturbPositions: PD.Boolean(false)
};
//
export const VolumeSphereParams = {
...Spheres.Params,
...Mesh.Params,
...VolumeDotParams,
tryUseImpostor: PD.Boolean(true),
detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }, BaseGeometry.CustomQualityParamInfo),
};
export function VolumeSphereVisual(materialId, volume, key, props, webgl) {
return props.tryUseImpostor && webgl && webgl.extensions.fragDepth && webgl.extensions.textureFloat
? VolumeSphereImpostorVisual(materialId)
: VolumeSphereMeshVisual(materialId);
}
export function VolumeSphereImpostorVisual(materialId) {
return VolumeVisual({
defaultProps: PD.getDefaultValues(VolumeSphereParams),
createGeometry: createVolumeSphereImpostor,
createLocationIterator: createVolumeCellLocationIterator,
getLoci: getDotLoci,
eachLocation: eachDot,
setUpdateState: (state, volume, newProps, currentProps, newTheme, currentTheme) => {
state.createGeometry = (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats) ||
newProps.perturbPositions !== currentProps.perturbPositions);
},
geometryUtils: Spheres.Utils,
mustRecreate: (_volumekey, props, webgl) => {
return !props.tryUseImpostor || !webgl;
}
}, materialId);
}
export function VolumeSphereMeshVisual(materialId) {
return VolumeVisual({
defaultProps: PD.getDefaultValues(VolumeSphereParams),
createGeometry: createVolumeSphereMesh,
createLocationIterator: createVolumeCellLocationIterator,
getLoci: getDotLoci,
eachLocation: eachDot,
setUpdateState: (state, volume, newProps, currentProps, newTheme, currentTheme) => {
state.createGeometry = (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats) ||
newProps.perturbPositions !== currentProps.perturbPositions ||
newProps.sizeFactor !== currentProps.sizeFactor ||
newProps.detail !== currentProps.detail);
},
geometryUtils: Mesh.Utils,
mustRecreate: (volumekey, props, webgl) => {
return props.tryUseImpostor && !!webgl;
}
}, materialId);
}
function getBasis(m) {
return {
...Mat4.extractBasis(m),
maxScale: Mat4.getMaxScaleOnAxis(m)
};
}
const pcg = new PCG();
const offset = Vec3();
function getRandomOffsetFromBasis({ x, y, z, maxScale }) {
const rx = (pcg.float() - 0.5) * maxScale;
const ry = (pcg.float() - 0.5) * maxScale;
const rz = (pcg.float() - 0.5) * maxScale;
Vec3.scale(offset, x, rx);
Vec3.scaleAndAdd(offset, offset, y, ry);
Vec3.scaleAndAdd(offset, offset, z, rz);
return offset;
}
export function createVolumeSphereImpostor(ctx, volume, key, theme, props, spheres) {
const { cells: { space, data }, stats } = volume.grid;
const gridToCartn = Grid.getGridToCartesianTransform(volume.grid);
const isoVal = Volume.IsoValue.toAbsolute(props.isoValue, stats).absoluteValue;
const p = Vec3();
const [xn, yn, zn] = space.dimensions;
const count = Math.ceil((xn * yn * zn) / 10);
const builder = SpheresBuilder.create(count, Math.ceil(count / 2), spheres);
const invert = isoVal < 0;
// Precompute basis vectors and largest cell axis length
const basis = props.perturbPositions ? getBasis(gridToCartn) : undefined;
for (let z = 0; z < zn; ++z) {
for (let y = 0; y < yn; ++y) {
for (let x = 0; x < xn; ++x) {
const value = space.get(data, x, y, z);
if (!invert && value < isoVal || invert && value > isoVal)
continue;
const cellIdx = space.dataOffset(x, y, z);
if (basis) {
Vec3.set(p, x, y, z);
Vec3.transformMat4(p, p, gridToCartn);
const offset = getRandomOffsetFromBasis(basis);
Vec3.add(p, p, offset);
}
else {
Vec3.set(p, x, y, z);
Vec3.transformMat4(p, p, gridToCartn);
}
builder.add(p[0], p[1], p[2], cellIdx);
}
}
}
const s = builder.getSpheres();
s.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
return s;
}
export function createVolumeSphereMesh(ctx, volume, key, theme, props, mesh) {
const { detail, sizeFactor } = props;
const { cells: { space, data }, stats } = volume.grid;
const gridToCartn = Grid.getGridToCartesianTransform(volume.grid);
const isoVal = Volume.IsoValue.toAbsolute(props.isoValue, stats).absoluteValue;
const p = Vec3();
const [xn, yn, zn] = space.dimensions;
const count = Math.ceil((xn * yn * zn) / 10);
const vertexCount = count * sphereVertexCount(detail);
const builderState = MeshBuilder.createState(vertexCount, Math.ceil(vertexCount / 2), mesh);
const l = Volume.Cell.Location(volume);
const themeSize = theme.size.size;
const invert = isoVal < 0;
// Precompute basis vectors and largest cell axis length
const basis = props.perturbPositions ? getBasis(gridToCartn) : undefined;
for (let z = 0; z < zn; ++z) {
for (let y = 0; y < yn; ++y) {
for (let x = 0; x < xn; ++x) {
const value = space.get(data, x, y, z);
if (!invert && value < isoVal || invert && value > isoVal)
continue;
const cellIdx = space.dataOffset(x, y, z);
l.cell = cellIdx;
const size = themeSize(l) * sizeFactor;
if (basis) {
Vec3.set(p, x, y, z);
Vec3.transformMat4(p, p, gridToCartn);
const offset = getRandomOffsetFromBasis(basis);
Vec3.add(p, p, offset);
}
else {
Vec3.set(p, x, y, z);
Vec3.transformMat4(p, p, gridToCartn);
}
builderState.currentGroup = cellIdx;
addSphere(builderState, p, size, detail);
}
}
}
const m = MeshBuilder.getMesh(builderState);
m.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
return m;
}
//
export const VolumePointParams = {
...Points.Params,
...VolumeDotParams,
};
export function VolumePointVisual(materialId) {
return VolumeVisual({
defaultProps: PD.getDefaultValues(VolumePointParams),
createGeometry: createVolumePoint,
createLocationIterator: createVolumeCellLocationIterator,
getLoci: getDotLoci,
eachLocation: eachDot,
setUpdateState: (state, volume, newProps, currentProps, newTheme, currentTheme) => {
state.createGeometry = (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats) ||
newProps.perturbPositions !== currentProps.perturbPositions);
},
geometryUtils: Points.Utils,
}, materialId);
}
export function createVolumePoint(ctx, volume, key, theme, props, points) {
const { cells: { space, data }, stats } = volume.grid;
const gridToCartn = Grid.getGridToCartesianTransform(volume.grid);
const isoVal = Volume.IsoValue.toAbsolute(props.isoValue, stats).absoluteValue;
const p = Vec3();
const [xn, yn, zn] = space.dimensions;
const count = Math.ceil((xn * yn * zn) / 10);
const builder = PointsBuilder.create(count, Math.ceil(count / 2), points);
const invert = isoVal < 0;
// Precompute basis vectors and largest cell axis length
const basis = props.perturbPositions ? getBasis(gridToCartn) : undefined;
for (let z = 0; z < zn; ++z) {
for (let y = 0; y < yn; ++y) {
for (let x = 0; x < xn; ++x) {
const value = space.get(data, x, y, z);
if (!invert && value < isoVal || invert && value > isoVal)
continue;
const cellIdx = space.dataOffset(x, y, z);
if (basis) {
Vec3.set(p, x, y, z);
Vec3.transformMat4(p, p, gridToCartn);
const offset = getRandomOffsetFromBasis(basis);
Vec3.add(p, p, offset);
}
else {
Vec3.set(p, x, y, z);
Vec3.transformMat4(p, p, gridToCartn);
}
builder.add(p[0], p[1], p[2], cellIdx);
}
}
}
const pt = builder.getPoints();
pt.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
return pt;
}
//
function getLoci(volume, props) {
const instances = Interval.ofLength(volume.instances.length);
return Volume.Isosurface.Loci(volume, props.isoValue, instances);
}
function getDotLoci(pickingId, volume, key, props, id) {
const { objectId, groupId, instanceId } = pickingId;
if (id === objectId) {
const granularity = Volume.PickingGranularity.get(volume);
const instances = OrderedSet.ofSingleton(instanceId);
if (granularity === 'volume') {
return Volume.Loci(volume, instances);
}
else if (granularity === 'object' || groupId === PickingId.Null) {
return Volume.Isosurface.Loci(volume, props.isoValue, instances);
}
else {
const indices = Interval.ofSingleton(groupId);
return Volume.Cell.Loci(volume, [{ indices, instances }]);
}
}
return EmptyLoci;
}
function eachDot(loci, volume, key, props, apply) {
return eachVolumeLoci(loci, volume, { isoValue: props.isoValue }, apply);
}
//
const DotVisuals = {
'sphere': (ctx, getParams) => VolumeRepresentation('Dot sphere', ctx, getParams, VolumeSphereVisual, getLoci),
'point': (ctx, getParams) => VolumeRepresentation('Dot point', ctx, getParams, VolumePointVisual, getLoci),
};
export const DotParams = {
...VolumeSphereParams,
...VolumePointParams,
visuals: PD.MultiSelect(['sphere'], PD.objectToOptions(DotVisuals)),
bumpFrequency: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
};
export function getDotParams(ctx, volume) {
const p = PD.clone(DotParams);
p.isoValue = Volume.createIsoValueParam(Volume.IsoValue.relative(2), volume.grid.stats);
return p;
}
export function DotRepresentation(ctx, getParams) {
return Representation.createMulti('Dot', ctx, getParams, Representation.StateBuilder, DotVisuals);
}
export const DotRepresentationProvider = VolumeRepresentationProvider({
name: 'dot',
label: 'Dot',
description: 'Displays dots of volumetric data.',
factory: DotRepresentation,
getParams: getDotParams,
defaultValues: PD.getDefaultValues(DotParams),
defaultColorTheme: { name: 'uniform' },
defaultSizeTheme: { name: 'uniform' },
locationKinds: ['cell-location', 'position-location'],
isApplicable: (volume) => !Volume.isEmpty(volume) && !Volume.Segmentation.get(volume)
});