UNPKG

molstar

Version:

A comprehensive macromolecular library.

336 lines (335 loc) 15.5 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 { Vec3 } from '../../../mol-math/linear-algebra.js'; import { Lines } from '../../../mol-geo/geometry/lines/lines.js'; import { LinesBuilder, StripLinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder.js'; import { Mesh } from '../../../mol-geo/geometry/mesh/mesh.js'; import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder.js'; import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube.js'; import { Volume, Grid } from '../../../mol-model/volume.js'; import { VolumeRepresentation, VolumeRepresentationProvider } from '../../../mol-repr/volume/representation.js'; import { Representation } from '../../../mol-repr/representation.js'; import { CommonStreamlinesParams, createStreamlinesLocationIterator, eachStreamlines, getStreamlinesLoci, getStreamlinesVisualLoci, streamlinePassesFilter } from './shared.js'; import { Sphere3D } from '../../../mol-math/geometry.js'; import { StreamlinesProvider } from '../streamlines.js'; import { BaseGeometry } from '../../../mol-geo/geometry/base.js'; import { computeFrenetFrames } from '../../../mol-math/linear-algebra/3d/frenet-frames.js'; import { VolumeVisual } from '../../../mol-repr/volume/visual.js'; import { PositionLocation } from '../../../mol-geo/util/location-iterator.js'; // avoiding namespace lookup improved performance in Chrome (Aug 2020) const v3transformMat4 = Vec3.transformMat4; const v3transformMat4Offset = Vec3.transformMat4Offset; const v3toArray = Vec3.toArray; export const StreamlinesLinesParams = { ...CommonStreamlinesParams, ...Lines.Params, useLineStrips: PD.Boolean(true), }; export function VolumeStreamlinesLinesVisual(materialId) { return VolumeVisual({ defaultProps: PD.getDefaultValues(StreamlinesLinesParams), createGeometry: createVolumeStreamlinesLines, createLocationIterator: createStreamlinesLocationIterator, getLoci: getStreamlinesLoci, eachLocation: eachStreamlines, setUpdateState: (state, volume, newProps, currentProps) => { const streamlinesHash = StreamlinesProvider.get(volume).version; if (state.info.streamlinesHash !== streamlinesHash) { if (state.info.streamlinesHash !== undefined) { state.createGeometry = true; state.updateLocation = true; } state.info.streamlinesHash = streamlinesHash; } if (newProps.anchorEnabled !== currentProps.anchorEnabled || !Vec3.equals(newProps.anchorCenter, currentProps.anchorCenter) || newProps.anchorRadius !== currentProps.anchorRadius) { state.createGeometry = true; } if (newProps.dashEnabled !== currentProps.dashEnabled || newProps.dashPoints !== currentProps.dashPoints || newProps.dashShift !== currentProps.dashShift) { state.createGeometry = true; } if (newProps.useLineStrips !== currentProps.useLineStrips) { state.createGeometry = true; } }, initUpdateState: (state, volume, newProps, newTheme) => { const streamlinesHash = StreamlinesProvider.get(volume).version; state.info.streamlinesHash = streamlinesHash; }, geometryUtils: Lines.Utils, }, materialId); } function streamlinePointCount(streamlines) { let count = 0; for (const streamline of streamlines) { count += streamline.length; } return count; } function createVolumeStreamlinesLines(ctx, volume, _key, _theme, props, lines) { const { cells: { space } } = volume.grid; const gridDimension = space.dimensions; const gridToCartn = Grid.getGridToCartesianTransform(volume.grid); const streamlines = StreamlinesProvider.get(volume).value; const pointCount = streamlinePointCount(streamlines); const { dashEnabled, dashPoints, dashShift } = props; const cycleLength = dashPoints * 2; let builder; if (props.useLineStrips) { const _builder = StripLinesBuilder.create(pointCount, Math.ceil(pointCount / 10), lines); builder = _builder; const b = Vec3(); for (let s = 0, sl = streamlines.length; s < sl; ++s) { const l = streamlines[s]; if (!streamlinePassesFilter(l, gridToCartn, props)) continue; if (dashEnabled) { let inDash = false; for (let i = 0, il = l.length; i < il; ++i) { const inCycle = i % cycleLength; const shouldDraw = dashShift ? (inCycle >= dashPoints) : (inCycle < dashPoints); if (shouldDraw) { if (!inDash) { _builder.start(s); inDash = true; } v3transformMat4(b, l[i], gridToCartn); _builder.addVec(b); } else if (inDash) { v3transformMat4(b, l[i], gridToCartn); _builder.addVec(b); _builder.end(); inDash = false; } } if (inDash) _builder.end(); } else { _builder.start(s); for (let i = 0, il = l.length; i < il; ++i) { v3transformMat4(b, l[i], gridToCartn); _builder.addVec(b); } _builder.end(); } } } else { const _builder = LinesBuilder.create(pointCount, Math.ceil(pointCount / 10), lines); builder = _builder; const a = Vec3(), b = Vec3(); for (let s = 0, sl = streamlines.length; s < sl; ++s) { const l = streamlines[s]; if (!streamlinePassesFilter(l, gridToCartn, props)) continue; if (dashEnabled) { Vec3.transformMat4(a, l[0], gridToCartn); for (let i = 1, il = l.length; i < il; ++i) { Vec3.transformMat4(b, l[i], gridToCartn); const inCycle = (i - 1) % (dashPoints * 2); if (dashShift ? (inCycle >= dashPoints) : (inCycle < dashPoints)) { _builder.addVec(a, b, s); } Vec3.copy(a, b); } } else { Vec3.transformMat4(a, l[0], gridToCartn); for (let i = 1, il = l.length; i < il; ++i) { Vec3.transformMat4(b, l[i], gridToCartn); _builder.addVec(a, b, s); Vec3.copy(a, b); } } } } const result = builder.getLines(); result.setBoundingSphere(Sphere3D.fromDimensionsAndTransform(Sphere3D(), gridDimension, gridToCartn)); return result; } // export const StreamlinesTubeMeshParams = { ...CommonStreamlinesParams, ...Mesh.Params, tubeSizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }), radialSegments: PD.Numeric(8, { min: 2, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo), }; export function VolumeStreamlinesTubeMeshVisual(materialId) { return VolumeVisual({ defaultProps: PD.getDefaultValues(StreamlinesTubeMeshParams), createGeometry: createVolumeStreamlinesTubeMesh, createLocationIterator: createStreamlinesLocationIterator, getLoci: getStreamlinesLoci, eachLocation: eachStreamlines, setUpdateState: (state, volume, newProps, currentProps) => { const streamlinesHash = StreamlinesProvider.get(volume).version; if (state.info.streamlinesHash !== streamlinesHash) { if (state.info.streamlinesHash !== undefined) { state.createGeometry = true; state.updateLocation = true; } state.info.streamlinesHash = streamlinesHash; } if (newProps.tubeSizeFactor !== currentProps.tubeSizeFactor || newProps.radialSegments !== currentProps.radialSegments) { state.createGeometry = true; } if (newProps.anchorEnabled !== currentProps.anchorEnabled || !Vec3.equals(newProps.anchorCenter, currentProps.anchorCenter) || newProps.anchorRadius !== currentProps.anchorRadius) { state.createGeometry = true; } if (newProps.dashEnabled !== currentProps.dashEnabled || newProps.dashPoints !== currentProps.dashPoints || newProps.dashShift !== currentProps.dashShift) { state.createGeometry = true; } }, initUpdateState: (state, volume, newProps, newTheme) => { const streamlinesHash = StreamlinesProvider.get(volume).version; state.info.streamlinesHash = streamlinesHash; }, geometryUtils: Mesh.Utils, }, materialId); } function createVolumeStreamlinesTubeMesh(ctx, volume, _key, theme, props, mesh) { const { cells: { space } } = volume.grid; const gridDimension = space.dimensions; const gridToCartn = Grid.getGridToCartesianTransform(volume.grid); const streamlines = StreamlinesProvider.get(volume).value; const { tubeSizeFactor, radialSegments, dashEnabled, dashPoints, dashShift } = props; // Estimate vertex count const pointCount = streamlinePointCount(streamlines); const vertexCount = pointCount * radialSegments * 2; const builderState = MeshBuilder.createState(vertexCount, Math.ceil(vertexCount / 10), mesh); for (let s = 0, sl = streamlines.length; s < sl; ++s) { const l = streamlines[s]; if (!streamlinePassesFilter(l, gridToCartn, props)) continue; builderState.currentGroup = s; if (dashEnabled) { addDashedStreamlineTube(builderState, l, gridToCartn, radialSegments, tubeSizeFactor, dashPoints, dashShift, theme); } else { addStreamlineTube(builderState, l, gridToCartn, radialSegments, tubeSizeFactor, theme); } } const m = MeshBuilder.getMesh(builderState); m.setBoundingSphere(Sphere3D.fromDimensionsAndTransform(Sphere3D(), gridDimension, gridToCartn)); return m; } const positionLocation = PositionLocation(); /** * Add a tube along a streamline path */ function addStreamlineTube(state, streamline, gridToCartn, radialSegments, tubeSizeFactor, theme) { const n = streamline.length; if (n < 2) return; const linearSegments = n - 1; const curvePoints = new Float32Array(n * 3); const normalVectors = new Float32Array(n * 3); const binormalVectors = new Float32Array(n * 3); const widthValues = new Float32Array(n); const heightValues = new Float32Array(n); for (let i = 0; i < n; ++i) { const p = streamline[i]; v3transformMat4Offset(curvePoints, p, gridToCartn, i * 3, 0, 0); Vec3.fromArray(positionLocation.position, curvePoints, i * 3); const tubeSize = theme.size.size(positionLocation) * tubeSizeFactor; widthValues[i] = tubeSize; heightValues[i] = tubeSize; } computeFrenetFrames(curvePoints, normalVectors, binormalVectors, n); addTube(state, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, widthValues, heightValues, true, true, 'elliptical'); } /** * Add dashed tube segments along a streamline path. * Uses point count to determine dash/gap boundaries. */ function addDashedStreamlineTube(state, streamline, gridToCartn, radialSegments, tubeSizeFactor, dashPoints, dashShift, theme) { const n = streamline.length; if (n < 2) return; const allPoints = []; const allSizes = []; for (let i = 0; i < n; ++i) { const p = v3transformMat4(Vec3(), streamline[i], gridToCartn); allPoints.push(p); Vec3.copy(positionLocation.position, p); allSizes.push(theme.size.size(positionLocation) * tubeSizeFactor); } const cycleLength = dashPoints * 2; let i = dashShift ? dashPoints : 0; while (i < n - 1) { const dashStart = i; const dashEnd = Math.min(i + dashPoints, n - 1); if (dashEnd > dashStart) { emitTubeSegment(state, allPoints, allSizes, dashStart, dashEnd, radialSegments); } i += cycleLength; } } /** * Emit a tube segment from startIdx to endIdx (inclusive) with caps on both ends. */ function emitTubeSegment(state, points, sizes, startIdx, endIdx, radialSegments) { const segmentLength = endIdx - startIdx + 1; if (segmentLength < 2) return; const curvePoints = new Float32Array(segmentLength * 3); const normalVectors = new Float32Array(segmentLength * 3); const binormalVectors = new Float32Array(segmentLength * 3); const widthValues = new Float32Array(segmentLength); const heightValues = new Float32Array(segmentLength); for (let i = 0; i < segmentLength; ++i) { v3toArray(points[startIdx + i], curvePoints, i * 3); widthValues[i] = sizes[startIdx + i]; heightValues[i] = sizes[startIdx + i]; } computeFrenetFrames(curvePoints, normalVectors, binormalVectors, segmentLength); addTube(state, curvePoints, normalVectors, binormalVectors, segmentLength - 1, radialSegments, widthValues, heightValues, true, true, 'elliptical'); } // const StreamlinesVisuals = { 'lines': (ctx, getParams) => VolumeRepresentation('Streamlines lines', ctx, getParams, VolumeStreamlinesLinesVisual, getStreamlinesVisualLoci), 'tube-mesh': (ctx, getParams) => VolumeRepresentation('Streamlines tube-mesh', ctx, getParams, VolumeStreamlinesTubeMeshVisual, getStreamlinesVisualLoci), }; export const StreamlinesParams = { ...StreamlinesLinesParams, ...StreamlinesTubeMeshParams, visuals: PD.MultiSelect(['lines'], PD.objectToOptions(StreamlinesVisuals)), density: PD.Numeric(0.1, { min: 0, max: 1, step: 0.01 }, BaseGeometry.ShadingCategory), }; export function getStreamlinesParams(ctx, volume) { const p = PD.clone(StreamlinesParams); return p; } export function StreamlinesRepresentation(ctx, getParams) { return Representation.createMulti('Streamlines', ctx, getParams, Representation.StateBuilder, StreamlinesVisuals); } export const StreamlinesRepresentationProvider = VolumeRepresentationProvider({ name: 'streamlines', label: 'Streamlines', description: 'Displays streamlines.', factory: StreamlinesRepresentation, getParams: getStreamlinesParams, defaultValues: PD.getDefaultValues(StreamlinesParams), defaultColorTheme: { name: 'uniform' }, defaultSizeTheme: { name: 'uniform' }, isApplicable: (volume) => !Volume.isEmpty(volume) && !Volume.Segmentation.get(volume), ensureCustomProperties: { attach: (ctx, volume) => StreamlinesProvider.attach(ctx, volume, void 0, true), detach: (data) => StreamlinesProvider.ref(data, false) }, });