UNPKG

molstar

Version:

A comprehensive macromolecular library.

290 lines (289 loc) 12.9 kB
/** * 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) });