molstar
Version:
A comprehensive macromolecular library.
234 lines (233 loc) • 10.1 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 Ludovic Autin <autin@scripps.edu>
*/
import { Interval } from '../../../mol-data/int/interval.js';
import { OrderedSet } from '../../../mol-data/int/ordered-set.js';
import { LocationIterator } from '../../../mol-geo/util/location-iterator.js';
import { DataLocation } from '../../../mol-model/location.js';
import { DataLoci, EmptyLoci } from '../../../mol-model/loci.js';
import { Grid } from '../../../mol-model/volume/grid.js';
import { Volume } from '../../../mol-model/volume/volume.js';
import { StreamlinesProvider } from '../streamlines.js';
import { ParamDefinition as PD } from '../../../mol-util/param-definition.js';
import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper.js';
import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3.js';
export function StreamlinesLocation(streamlines, volume, index, instance) {
return DataLocation('streamlines', { volume, streamlines }, { index: index, instance: instance });
}
export function isStreamlinesLocation(x) {
return !!x && x.kind === 'data-location' && x.tag === 'streamlines';
}
export function areStreamlinesLocationsEqual(locA, locB) {
return (locA.data.volume === locB.data.volume &&
locA.data.streamlines === locB.data.streamlines &&
locA.element.index === locB.element.index &&
locA.element.instance === locB.element.instance);
}
export function streamlinesLocationLabel(streamlines, volume, index, instance) {
const label = [
`${volume.label || 'Volume'}`,
`Streamline #${index}`
];
if (volume.instances.length > 1) {
label.push(`Instance #${instance}`);
}
return label.join(' | ');
}
export function streamlinesLociLabel(streamlines, volume, elements) {
const size = getStreamlinesLociSize(elements);
const label = [
`${volume.label || 'Volume'}`
];
if (size === 0) {
label.push('No Streamlines');
}
else if (size === 1) {
const index = OrderedSet.start(elements[0].indices);
label.push(`Streamline #${index}`);
if (volume.instances.length > 1) {
const instance = OrderedSet.start(elements[0].instances);
label.push(`Instance #${instance}`);
}
}
else {
label.push(`${size} Streamlines`);
}
return label.join(' | ');
}
export function StreamlinesLoci(streamlines, volume, elements) {
return DataLoci('streamlines', { streamlines, volume }, elements, (boundingSphere) => getStreamlinesLociBoundingSphere(streamlines, volume, elements, boundingSphere), () => streamlinesLociLabel(streamlines, volume, elements));
}
export function isStreamlinesLoci(x) {
return !!x && x.kind === 'data-loci' && x.tag === 'streamlines';
}
export function getStreamlinesLociSize(elements) {
let size = 0;
for (const e of elements) {
size += OrderedSet.size(e.indices) * OrderedSet.size(e.instances);
}
return size;
}
const boundaryHelper = new BoundaryHelper('98');
const tmpBoundaryPos = Vec3();
const tmpBoundaryPos2 = Vec3();
export function getStreamlinesLociBoundingSphere(streamlines, volume, elements, boundingSphere) {
boundaryHelper.reset();
const transform = Grid.getGridToCartesianTransform(volume.grid);
for (const { indices, instances } of elements) {
for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
const o = OrderedSet.getAt(indices, i);
for (const p of streamlines[o]) {
Vec3.transformMat4(tmpBoundaryPos, p, transform);
for (let j = 0, _j = OrderedSet.size(instances); j < _j; j++) {
const instance = volume.instances[OrderedSet.getAt(instances, j)];
Vec3.transformMat4(tmpBoundaryPos2, tmpBoundaryPos, instance.transform);
boundaryHelper.includePosition(tmpBoundaryPos2);
}
}
}
}
boundaryHelper.finishedIncludeStep();
for (const { indices, instances } of elements) {
for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
const o = OrderedSet.getAt(indices, i);
for (const p of streamlines[o]) {
Vec3.transformMat4(tmpBoundaryPos, p, transform);
for (let j = 0, _j = OrderedSet.size(instances); j < _j; j++) {
const instance = volume.instances[OrderedSet.getAt(instances, j)];
Vec3.transformMat4(tmpBoundaryPos2, tmpBoundaryPos, instance.transform);
boundaryHelper.radiusPosition(tmpBoundaryPos2);
}
}
}
}
return boundaryHelper.getSphere(boundingSphere);
}
export function areStreamlinesLociEqual(a, b) {
if (a.data.volume !== b.data.volume || a.elements.length !== b.elements.length)
return false;
for (let i = 0, il = a.elements.length; i < il; ++i) {
const ae = a.elements[i], be = b.elements[i];
if (!OrderedSet.areEqual(ae.instances, be.instances) ||
!OrderedSet.areEqual(ae.indices, be.indices))
return false;
}
return true;
}
export function isStreamlinesLociEmpty(loci) {
for (const { indices, instances } of loci.elements) {
if (!OrderedSet.isEmpty(indices) || !OrderedSet.isEmpty(instances))
return false;
}
return true;
}
//
export const CommonStreamlinesParams = {
anchorEnabled: PD.Boolean(false, { label: 'Anchor' }),
anchorCenter: PD.Vec3(Vec3(), undefined, { hideIf: p => !p.anchorEnabled }),
anchorRadius: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }, { hideIf: p => !p.anchorEnabled }),
dashEnabled: PD.Boolean(false, { label: 'Dash' }),
dashPoints: PD.Numeric(5, { min: 1, max: 25, step: 1 }, { description: 'Number of streamline points per dash/gap', hideIf: p => !p.dashEnabled }),
dashShift: PD.Boolean(false, { description: 'Shift dashes so dashes become gaps and vice versa', hideIf: p => !p.dashEnabled }),
};
const tmpFilterVec = Vec3();
/**
* Check if a streamline passes the sphere filter.
* A streamline passes if its start or end point is within the filter sphere.
*/
export function streamlinePassesFilter(streamline, gridToCartn, props) {
if (streamline.length < 2)
return false;
if (!props.anchorEnabled)
return true;
// Check start point
const start = streamline[0];
Vec3.transformMat4(tmpFilterVec, start, gridToCartn);
if (Vec3.distance(tmpFilterVec, props.anchorCenter) <= props.anchorRadius)
return true;
// Check end point
const end = streamline[streamline.length - 1];
Vec3.transformMat4(tmpFilterVec, end, gridToCartn);
if (Vec3.distance(tmpFilterVec, props.anchorCenter) <= props.anchorRadius)
return true;
return false;
}
export function getStreamlinesVisualLoci(volume, _props) {
const streamlines = StreamlinesProvider.get(volume).value;
const indices = Interval.ofLength(streamlines.length);
const instances = Interval.ofLength(volume.instances.length);
return StreamlinesLoci(streamlines, volume, [{ indices, instances }]);
}
export function getStreamlinesLoci(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);
const streamlines = StreamlinesProvider.get(volume).value;
const indices = OrderedSet.ofSingleton(groupId);
return StreamlinesLoci(streamlines, volume, [{ indices, instances }]);
}
return EmptyLoci;
}
export function eachStreamlines(loci, volume, _key, _props, apply) {
let changed = false;
const streamlines = StreamlinesProvider.get(volume).value;
const count = streamlines.length;
if (Volume.isLoci(loci)) {
if (!Volume.areEquivalent(loci.volume, volume))
return false;
if (Interval.is(loci.instances)) {
const start = Interval.start(loci.instances) * count;
const end = Interval.end(loci.instances) * count;
if (apply(Interval.ofBounds(start, end)))
changed = true;
}
else {
for (let i = 0, il = loci.instances.length; i < il; ++i) {
const offset = loci.instances[i] * count;
if (apply(Interval.ofBounds(offset, offset + count)))
changed = true;
}
}
}
else if (isStreamlinesLoci(loci)) {
if (!Volume.areEquivalent(loci.data.volume, volume))
return false;
for (const { indices, instances } of loci.elements) {
if (Interval.is(indices)) {
OrderedSet.forEach(instances, j => {
const offset = j * count;
if (apply(Interval.offset(indices, offset)))
changed = true;
});
}
else {
OrderedSet.forEach(indices, v => {
OrderedSet.forEach(instances, j => {
const offset = j * count;
if (apply(Interval.ofSingleton(offset + v)))
changed = true;
});
});
}
}
}
return changed;
}
export function createStreamlinesLocationIterator(volume) {
const streamlines = StreamlinesProvider.get(volume).value;
const groupCount = streamlines.length;
const instanceCount = volume.instances.length;
const l = StreamlinesLocation(streamlines, volume);
const getLocation = (groupIndex, instanceIndex) => {
l.element.index = groupIndex;
l.element.instance = instanceIndex;
return l;
};
return LocationIterator(groupCount, instanceCount, 1, getLocation);
}