molstar
Version:
A comprehensive macromolecular library.
343 lines (342 loc) • 17.1 kB
JavaScript
"use strict";
/**
* 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>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.StreamlinesRepresentationProvider = exports.StreamlinesParams = exports.StreamlinesTubeMeshParams = exports.StreamlinesLinesParams = void 0;
exports.VolumeStreamlinesLinesVisual = VolumeStreamlinesLinesVisual;
exports.VolumeStreamlinesTubeMeshVisual = VolumeStreamlinesTubeMeshVisual;
exports.getStreamlinesParams = getStreamlinesParams;
exports.StreamlinesRepresentation = StreamlinesRepresentation;
const param_definition_1 = require("../../../mol-util/param-definition.js");
const linear_algebra_1 = require("../../../mol-math/linear-algebra.js");
const lines_1 = require("../../../mol-geo/geometry/lines/lines.js");
const lines_builder_1 = require("../../../mol-geo/geometry/lines/lines-builder.js");
const mesh_1 = require("../../../mol-geo/geometry/mesh/mesh.js");
const mesh_builder_1 = require("../../../mol-geo/geometry/mesh/mesh-builder.js");
const tube_1 = require("../../../mol-geo/geometry/mesh/builder/tube.js");
const volume_1 = require("../../../mol-model/volume.js");
const representation_1 = require("../../../mol-repr/volume/representation.js");
const representation_2 = require("../../../mol-repr/representation.js");
const shared_1 = require("./shared.js");
const geometry_1 = require("../../../mol-math/geometry.js");
const streamlines_1 = require("../streamlines.js");
const base_1 = require("../../../mol-geo/geometry/base.js");
const frenet_frames_1 = require("../../../mol-math/linear-algebra/3d/frenet-frames.js");
const visual_1 = require("../../../mol-repr/volume/visual.js");
const location_iterator_1 = require("../../../mol-geo/util/location-iterator.js");
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3transformMat4 = linear_algebra_1.Vec3.transformMat4;
const v3transformMat4Offset = linear_algebra_1.Vec3.transformMat4Offset;
const v3toArray = linear_algebra_1.Vec3.toArray;
exports.StreamlinesLinesParams = {
...shared_1.CommonStreamlinesParams,
...lines_1.Lines.Params,
useLineStrips: param_definition_1.ParamDefinition.Boolean(true),
};
function VolumeStreamlinesLinesVisual(materialId) {
return (0, visual_1.VolumeVisual)({
defaultProps: param_definition_1.ParamDefinition.getDefaultValues(exports.StreamlinesLinesParams),
createGeometry: createVolumeStreamlinesLines,
createLocationIterator: shared_1.createStreamlinesLocationIterator,
getLoci: shared_1.getStreamlinesLoci,
eachLocation: shared_1.eachStreamlines,
setUpdateState: (state, volume, newProps, currentProps) => {
const streamlinesHash = streamlines_1.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 ||
!linear_algebra_1.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 = streamlines_1.StreamlinesProvider.get(volume).version;
state.info.streamlinesHash = streamlinesHash;
},
geometryUtils: lines_1.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 = volume_1.Grid.getGridToCartesianTransform(volume.grid);
const streamlines = streamlines_1.StreamlinesProvider.get(volume).value;
const pointCount = streamlinePointCount(streamlines);
const { dashEnabled, dashPoints, dashShift } = props;
const cycleLength = dashPoints * 2;
let builder;
if (props.useLineStrips) {
const _builder = lines_builder_1.StripLinesBuilder.create(pointCount, Math.ceil(pointCount / 10), lines);
builder = _builder;
const b = (0, linear_algebra_1.Vec3)();
for (let s = 0, sl = streamlines.length; s < sl; ++s) {
const l = streamlines[s];
if (!(0, shared_1.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 = lines_builder_1.LinesBuilder.create(pointCount, Math.ceil(pointCount / 10), lines);
builder = _builder;
const a = (0, linear_algebra_1.Vec3)(), b = (0, linear_algebra_1.Vec3)();
for (let s = 0, sl = streamlines.length; s < sl; ++s) {
const l = streamlines[s];
if (!(0, shared_1.streamlinePassesFilter)(l, gridToCartn, props))
continue;
if (dashEnabled) {
linear_algebra_1.Vec3.transformMat4(a, l[0], gridToCartn);
for (let i = 1, il = l.length; i < il; ++i) {
linear_algebra_1.Vec3.transformMat4(b, l[i], gridToCartn);
const inCycle = (i - 1) % (dashPoints * 2);
if (dashShift ? (inCycle >= dashPoints) : (inCycle < dashPoints)) {
_builder.addVec(a, b, s);
}
linear_algebra_1.Vec3.copy(a, b);
}
}
else {
linear_algebra_1.Vec3.transformMat4(a, l[0], gridToCartn);
for (let i = 1, il = l.length; i < il; ++i) {
linear_algebra_1.Vec3.transformMat4(b, l[i], gridToCartn);
_builder.addVec(a, b, s);
linear_algebra_1.Vec3.copy(a, b);
}
}
}
}
const result = builder.getLines();
result.setBoundingSphere(geometry_1.Sphere3D.fromDimensionsAndTransform((0, geometry_1.Sphere3D)(), gridDimension, gridToCartn));
return result;
}
//
exports.StreamlinesTubeMeshParams = {
...shared_1.CommonStreamlinesParams,
...mesh_1.Mesh.Params,
tubeSizeFactor: param_definition_1.ParamDefinition.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
radialSegments: param_definition_1.ParamDefinition.Numeric(8, { min: 2, max: 56, step: 2 }, base_1.BaseGeometry.CustomQualityParamInfo),
};
function VolumeStreamlinesTubeMeshVisual(materialId) {
return (0, visual_1.VolumeVisual)({
defaultProps: param_definition_1.ParamDefinition.getDefaultValues(exports.StreamlinesTubeMeshParams),
createGeometry: createVolumeStreamlinesTubeMesh,
createLocationIterator: shared_1.createStreamlinesLocationIterator,
getLoci: shared_1.getStreamlinesLoci,
eachLocation: shared_1.eachStreamlines,
setUpdateState: (state, volume, newProps, currentProps) => {
const streamlinesHash = streamlines_1.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 ||
!linear_algebra_1.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 = streamlines_1.StreamlinesProvider.get(volume).version;
state.info.streamlinesHash = streamlinesHash;
},
geometryUtils: mesh_1.Mesh.Utils,
}, materialId);
}
function createVolumeStreamlinesTubeMesh(ctx, volume, _key, theme, props, mesh) {
const { cells: { space } } = volume.grid;
const gridDimension = space.dimensions;
const gridToCartn = volume_1.Grid.getGridToCartesianTransform(volume.grid);
const streamlines = streamlines_1.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 = mesh_builder_1.MeshBuilder.createState(vertexCount, Math.ceil(vertexCount / 10), mesh);
for (let s = 0, sl = streamlines.length; s < sl; ++s) {
const l = streamlines[s];
if (!(0, shared_1.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 = mesh_builder_1.MeshBuilder.getMesh(builderState);
m.setBoundingSphere(geometry_1.Sphere3D.fromDimensionsAndTransform((0, geometry_1.Sphere3D)(), gridDimension, gridToCartn));
return m;
}
const positionLocation = (0, location_iterator_1.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);
linear_algebra_1.Vec3.fromArray(positionLocation.position, curvePoints, i * 3);
const tubeSize = theme.size.size(positionLocation) * tubeSizeFactor;
widthValues[i] = tubeSize;
heightValues[i] = tubeSize;
}
(0, frenet_frames_1.computeFrenetFrames)(curvePoints, normalVectors, binormalVectors, n);
(0, tube_1.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((0, linear_algebra_1.Vec3)(), streamline[i], gridToCartn);
allPoints.push(p);
linear_algebra_1.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];
}
(0, frenet_frames_1.computeFrenetFrames)(curvePoints, normalVectors, binormalVectors, segmentLength);
(0, tube_1.addTube)(state, curvePoints, normalVectors, binormalVectors, segmentLength - 1, radialSegments, widthValues, heightValues, true, true, 'elliptical');
}
//
const StreamlinesVisuals = {
'lines': (ctx, getParams) => (0, representation_1.VolumeRepresentation)('Streamlines lines', ctx, getParams, VolumeStreamlinesLinesVisual, shared_1.getStreamlinesVisualLoci),
'tube-mesh': (ctx, getParams) => (0, representation_1.VolumeRepresentation)('Streamlines tube-mesh', ctx, getParams, VolumeStreamlinesTubeMeshVisual, shared_1.getStreamlinesVisualLoci),
};
exports.StreamlinesParams = {
...exports.StreamlinesLinesParams,
...exports.StreamlinesTubeMeshParams,
visuals: param_definition_1.ParamDefinition.MultiSelect(['lines'], param_definition_1.ParamDefinition.objectToOptions(StreamlinesVisuals)),
density: param_definition_1.ParamDefinition.Numeric(0.1, { min: 0, max: 1, step: 0.01 }, base_1.BaseGeometry.ShadingCategory),
};
function getStreamlinesParams(ctx, volume) {
const p = param_definition_1.ParamDefinition.clone(exports.StreamlinesParams);
return p;
}
function StreamlinesRepresentation(ctx, getParams) {
return representation_2.Representation.createMulti('Streamlines', ctx, getParams, representation_2.Representation.StateBuilder, StreamlinesVisuals);
}
exports.StreamlinesRepresentationProvider = (0, representation_1.VolumeRepresentationProvider)({
name: 'streamlines',
label: 'Streamlines',
description: 'Displays streamlines.',
factory: StreamlinesRepresentation,
getParams: getStreamlinesParams,
defaultValues: param_definition_1.ParamDefinition.getDefaultValues(exports.StreamlinesParams),
defaultColorTheme: { name: 'uniform' },
defaultSizeTheme: { name: 'uniform' },
isApplicable: (volume) => !volume_1.Volume.isEmpty(volume) && !volume_1.Volume.Segmentation.get(volume),
ensureCustomProperties: {
attach: (ctx, volume) => streamlines_1.StreamlinesProvider.attach(ctx, volume, void 0, true),
detach: (data) => streamlines_1.StreamlinesProvider.ref(data, false)
},
});