mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 13:30:24 +08:00
Streamlines support
- Add basic calculation method - Add custom-volume-property - Add representation with lines and tube-mesh visuals
This commit is contained in:
@@ -23,6 +23,10 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Support memory efficient line-strips in Lines geometry,
|
||||
- Add `StripLinesBuilder`
|
||||
- Add `computeFrenetFrames` helper
|
||||
- Streamlines support
|
||||
- Add basic calculation method
|
||||
- Add custom-volume-property
|
||||
- Add representation with lines and tube-mesh visuals
|
||||
|
||||
## [v5.6.1] - 2026-01-23
|
||||
- Disable occlusion culling in `ImagePass` (#1758)
|
||||
|
||||
40
src/mol-model-props/volume/streamlines.ts
Normal file
40
src/mol-model-props/volume/streamlines.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { CustomProperty } from '../common/custom-property';
|
||||
import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
|
||||
import { CustomVolumeProperty } from '../common/custom-volume-property';
|
||||
import { Volume } from '../../mol-model/volume/volume';
|
||||
import { calculateBasicStreamlines, BasicStreamlineCalculationParams } from './streamlines/basic';
|
||||
import { Streamlines } from './streamlines/shared';
|
||||
|
||||
export const StreamlinesParams = {
|
||||
type: PD.MappedStatic('basic', {
|
||||
'basic': PD.Group(BasicStreamlineCalculationParams, { isFlat: true }),
|
||||
})
|
||||
};
|
||||
|
||||
export type StreamlinesParams = typeof StreamlinesParams
|
||||
export type StreamlinesProps = PD.Values<StreamlinesParams>
|
||||
export type StreamlinesValue = Streamlines
|
||||
|
||||
export const StreamlinesProvider: CustomVolumeProperty.Provider<StreamlinesParams, StreamlinesValue> = CustomVolumeProperty.createProvider({
|
||||
label: 'Streamlines',
|
||||
descriptor: CustomPropertyDescriptor({
|
||||
name: 'molstar_streamlines',
|
||||
// TODO `cifExport` and `symbol`
|
||||
}),
|
||||
defaultParams: StreamlinesParams,
|
||||
getParams: (data: Volume) => StreamlinesParams,
|
||||
isApplicable: (data: Volume) => !Volume.Segmentation.get(data),
|
||||
obtain: async (ctx: CustomProperty.Context, data: Volume, props: Partial<StreamlinesProps>) => {
|
||||
const p = { ...PD.getDefaultValues(StreamlinesParams), ...props };
|
||||
switch (p.type.name) {
|
||||
case 'basic': return { value: await calculateBasicStreamlines(ctx, data, p.type.params) };
|
||||
}
|
||||
}
|
||||
});
|
||||
153
src/mol-model-props/volume/streamlines/basic.ts
Normal file
153
src/mol-model-props/volume/streamlines/basic.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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';
|
||||
import { PCG } from '../../../mol-data/util/hash-functions';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { Grid } from '../../../mol-model/volume/grid';
|
||||
import { CustomProperty } from '../../common/custom-property';
|
||||
import { Volume } from '../../../mol-model/volume';
|
||||
import { StreamlinePoint, Streamlines } from './shared';
|
||||
import { RuntimeContext } from '../../../mol-task/execution/runtime-context';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3distance = Vec3.distance;
|
||||
const v3setMagnitude = Vec3.setMagnitude;
|
||||
const v3create = Vec3.create;
|
||||
const v3copy = Vec3.copy;
|
||||
const v3add = Vec3.add;
|
||||
|
||||
export const BasicStreamlineCalculationParams = {
|
||||
seedDensity: PD.Numeric(10, { min: 1, max: 30, step: 1 }, { description: 'Percentage of cells with seed.' }),
|
||||
stepSize: PD.Numeric(0.35, { min: 0.01, max: 1, step: 0.01 }, { description: 'Step size in grid space.' }),
|
||||
};
|
||||
export type BasicStreamlineCalculationParams = typeof BasicStreamlineCalculationParams
|
||||
export type BasicStreamlineCalculationProps = PD.Values<BasicStreamlineCalculationParams>
|
||||
|
||||
type GetGradient = (gridCoords: Vec3, out: Vec3) => boolean
|
||||
|
||||
const d = Vec3();
|
||||
const p = Vec3();
|
||||
const g = Vec3();
|
||||
const prev = Vec3();
|
||||
|
||||
/**
|
||||
* Basic tracer with fixed step size in grid space
|
||||
*/
|
||||
function traceOneDirection(out: StreamlinePoint[], grid: Grid, getGradient: GetGradient, seed: Vec3, stepSize: number, dir: 1 | -1): number {
|
||||
const { space } = grid.cells;
|
||||
const [nx, ny, nz] = space.dimensions;
|
||||
const o = space.dataOffset;
|
||||
|
||||
const step = dir * stepSize;
|
||||
const maxSteps = Math.max(nx, ny, nz) * 5 / stepSize;
|
||||
const t = stepSize / 1.1; // tolerance for writing points
|
||||
|
||||
const seenVoxel = new Map<number, number>();
|
||||
const maxVoxelVisits = Math.ceil(2 / stepSize);
|
||||
|
||||
v3copy(p, seed);
|
||||
|
||||
let written = 0;
|
||||
for (let i = 0; i < maxSteps; ++i) {
|
||||
// boundary check
|
||||
if (p[0] < 1 || p[0] > nx - 2 ||
|
||||
p[1] < 1 || p[1] > ny - 2 ||
|
||||
p[2] < 1 || p[2] > nz - 2) break;
|
||||
|
||||
if (!getGradient(p, g)) break;
|
||||
|
||||
const key = o(Math.round(p[0]), Math.round(p[1]), Math.round(p[2]));
|
||||
const c = (seenVoxel.get(key) || 0) + 1;
|
||||
seenVoxel.set(key, c);
|
||||
if (c > maxVoxelVisits) break;
|
||||
|
||||
v3setMagnitude(d, g, step);
|
||||
// write midpoint (keeps lines smooth and avoids tiny box at start)
|
||||
const midX = p[0] + 0.5 * d[0];
|
||||
const midY = p[1] + 0.5 * d[1];
|
||||
const midZ = p[2] + 0.5 * d[2];
|
||||
const m = v3create(midX, midY, midZ);
|
||||
if (v3distance(prev, m) >= t) {
|
||||
out.push(m);
|
||||
v3copy(prev, m);
|
||||
}
|
||||
written++;
|
||||
|
||||
v3add(p, p, d); // advance
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
function traceStreamlineBothDirs(grid: Grid, getGradient: GetGradient, seed: Vec3, stepSize: number): StreamlinePoint[] {
|
||||
const line: StreamlinePoint[] = [];
|
||||
const nBack = traceOneDirection(line, grid, getGradient, seed, stepSize, -1);
|
||||
if (nBack > 1) line.reverse();
|
||||
traceOneDirection(line, grid, getGradient, seed, stepSize, +1);
|
||||
return line;
|
||||
}
|
||||
|
||||
async function computeBasicStreamlines(ctx: RuntimeContext, grid: Grid, props: BasicStreamlineCalculationProps): Promise<Streamlines> {
|
||||
const { space } = grid.cells;
|
||||
const [nx, ny, nz] = space.dimensions;
|
||||
|
||||
const { seedDensity, stepSize } = props;
|
||||
const seedStep = Math.max(1, Math.floor(Math.min(nx, ny, nz) / seedDensity));
|
||||
|
||||
// bounds avoiding edges
|
||||
const xStart = 1, xEnd = nx - 2 - seedStep;
|
||||
const yStart = 1, yEnd = ny - 2 - seedStep;
|
||||
const zStart = 1, zEnd = nz - 2 - seedStep;
|
||||
|
||||
const pcg = new PCG();
|
||||
|
||||
const seeds: number[] = [];
|
||||
for (let z = zStart; z <= zEnd; z += seedStep) {
|
||||
for (let y = yStart; y <= yEnd; y += seedStep) {
|
||||
for (let x = xStart; x <= xEnd; x += seedStep) {
|
||||
seeds.push(
|
||||
x + pcg.float() * seedStep,
|
||||
y + pcg.float() * seedStep,
|
||||
z + pcg.float() * seedStep
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shuffle in-place by triplets [x,y,z]
|
||||
for (let i = (seeds.length / 3) - 1; i > 0; i--) {
|
||||
const j = Math.floor(pcg.float() * (i + 1));
|
||||
const ia = i * 3, ja = j * 3;
|
||||
// swap 3 numbers at once
|
||||
const tx = seeds[ia], ty = seeds[ia + 1], tz = seeds[ia + 2];
|
||||
seeds[ia] = seeds[ja]; seeds[ia + 1] = seeds[ja + 1]; seeds[ia + 2] = seeds[ja + 2];
|
||||
seeds[ja] = tx; seeds[ja + 1] = ty; seeds[ja + 2] = tz;
|
||||
}
|
||||
|
||||
await ctx.update({ isIndeterminate: false, current: 0, max: seeds.length / 3 });
|
||||
|
||||
const getGradient = Grid.makeGetInterpolatedGradient(grid);
|
||||
|
||||
const lines: Streamlines = [];
|
||||
const pos = Vec3();
|
||||
for (let i = 0; i < seeds.length; i += 3) {
|
||||
if (ctx.shouldUpdate) await ctx.update({ current: i / 3 + 1 });
|
||||
|
||||
v3fromArray(pos, seeds, i);
|
||||
const line = traceStreamlineBothDirs(grid, getGradient, pos, stepSize);
|
||||
if (line.length * stepSize >= 3) lines.push(line);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export async function calculateBasicStreamlines(ctx: CustomProperty.Context, volume: Volume, props: BasicStreamlineCalculationProps): Promise<Streamlines> {
|
||||
return computeBasicStreamlines(ctx.runtime, volume.grid, props);
|
||||
}
|
||||
370
src/mol-model-props/volume/streamlines/representation.ts
Normal file
370
src/mol-model-props/volume/streamlines/representation.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* 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';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { Lines } from '../../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder, StripLinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube';
|
||||
import { Volume, Grid } from '../../../mol-model/volume';
|
||||
import { VolumeRepresentation, VolumeRepresentationProvider } from '../../../mol-repr/volume/representation';
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { VisualContext } from '../../../mol-repr/visual';
|
||||
import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
|
||||
import { RepresentationContext, RepresentationParamsGetter, Representation } from '../../../mol-repr/representation';
|
||||
import { CommonStreamlinesParams, createStreamlinesLocationIterator, eachStreamlines, getStreamlinesLoci, getStreamlinesVisualLoci, Streamline, StreamlinesLocation, StreamlinesIndex, Streamlines, streamlinePassesFilter } from './shared';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { StreamlinesProvider } from '../streamlines';
|
||||
import { CustomProperty } from '../../common/custom-property';
|
||||
import { BaseGeometry } from '../../../mol-geo/geometry/base';
|
||||
import { computeFrenetFrames } from '../../../mol-math/linear-algebra/3d/frenet-frames';
|
||||
import { VolumeVisual } from '../../../mol-repr/volume/visual';
|
||||
|
||||
// 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 type StreamlinesLinesParams = typeof StreamlinesLinesParams
|
||||
export type StreamlinesLinesProps = PD.Values<StreamlinesLinesParams>
|
||||
|
||||
export function VolumeStreamlinesLinesVisual(materialId: number): VolumeVisual<StreamlinesLinesParams> {
|
||||
return VolumeVisual<Lines, StreamlinesLinesParams>({
|
||||
defaultProps: PD.getDefaultValues(StreamlinesLinesParams),
|
||||
createGeometry: createVolumeStreamlinesLines,
|
||||
createLocationIterator: createStreamlinesLocationIterator,
|
||||
getLoci: getStreamlinesLoci,
|
||||
eachLocation: eachStreamlines,
|
||||
setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<StreamlinesLinesParams>, currentProps: PD.Values<StreamlinesLinesParams>) => {
|
||||
const streamlinesHash = StreamlinesProvider.get(volume).version;
|
||||
if ((state.info.streamlinesHash as number) !== 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: VisualUpdateState, volume: Volume, newProps: PD.Values<StreamlinesLinesParams>, newTheme: Theme) => {
|
||||
const streamlinesHash = StreamlinesProvider.get(volume).version;
|
||||
state.info.streamlinesHash = streamlinesHash;
|
||||
},
|
||||
geometryUtils: Lines.Utils,
|
||||
}, materialId);
|
||||
}
|
||||
|
||||
function streamlinePointCount(streamlines: Streamlines): number {
|
||||
let count = 0;
|
||||
for (const streamline of streamlines) {
|
||||
count += streamline.length;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function createVolumeStreamlinesLines(ctx: VisualContext, volume: Volume, _key: number, _theme: Theme, props: StreamlinesLinesProps, lines?: Lines): Lines {
|
||||
const { cells: { space } } = volume.grid;
|
||||
const gridDimension = space.dimensions as Vec3;
|
||||
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: LinesBuilder | StripLinesBuilder;
|
||||
|
||||
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 type StreamlinesTubeMeshParams = typeof StreamlinesTubeMeshParams
|
||||
export type StreamlinesTubeMeshProps = PD.Values<StreamlinesTubeMeshParams>
|
||||
|
||||
export function VolumeStreamlinesTubeMeshVisual(materialId: number): VolumeVisual<StreamlinesTubeMeshParams> {
|
||||
return VolumeVisual<Mesh, StreamlinesTubeMeshParams>({
|
||||
defaultProps: PD.getDefaultValues(StreamlinesTubeMeshParams),
|
||||
createGeometry: createVolumeStreamlinesTubeMesh,
|
||||
createLocationIterator: createStreamlinesLocationIterator,
|
||||
getLoci: getStreamlinesLoci,
|
||||
eachLocation: eachStreamlines,
|
||||
setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<StreamlinesTubeMeshParams>, currentProps: PD.Values<StreamlinesTubeMeshParams>) => {
|
||||
const streamlinesHash = StreamlinesProvider.get(volume).version;
|
||||
if ((state.info.streamlinesHash as number) !== 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: VisualUpdateState, volume: Volume, newProps: PD.Values<StreamlinesTubeMeshParams>, newTheme: Theme) => {
|
||||
const streamlinesHash = StreamlinesProvider.get(volume).version;
|
||||
state.info.streamlinesHash = streamlinesHash;
|
||||
},
|
||||
geometryUtils: Mesh.Utils,
|
||||
}, materialId);
|
||||
}
|
||||
|
||||
function createVolumeStreamlinesTubeMesh(ctx: VisualContext, volume: Volume, _key: number, theme: Theme, props: StreamlinesTubeMeshProps, mesh?: Mesh): Mesh {
|
||||
const { cells: { space } } = volume.grid;
|
||||
const gridDimension = space.dimensions as Vec3;
|
||||
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);
|
||||
|
||||
const location = StreamlinesLocation(streamlines, volume);
|
||||
for (let s = 0, sl = streamlines.length; s < sl; ++s) {
|
||||
const l = streamlines[s];
|
||||
if (!streamlinePassesFilter(l, gridToCartn, props)) continue;
|
||||
|
||||
builderState.currentGroup = s;
|
||||
location.element.index = s as StreamlinesIndex;
|
||||
const tubeSize = theme.size.size(location) * tubeSizeFactor;
|
||||
|
||||
if (dashEnabled) {
|
||||
addDashedStreamlineTube(builderState, l, gridToCartn, radialSegments, tubeSize, dashPoints, dashShift);
|
||||
} else {
|
||||
addStreamlineTube(builderState, l, gridToCartn, radialSegments, tubeSize);
|
||||
}
|
||||
}
|
||||
|
||||
const m = MeshBuilder.getMesh(builderState);
|
||||
m.setBoundingSphere(Sphere3D.fromDimensionsAndTransform(Sphere3D(), gridDimension, gridToCartn));
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tube along a streamline path
|
||||
*/
|
||||
function addStreamlineTube(state: MeshBuilder.State, streamline: Streamline, gridToCartn: Mat4, radialSegments: number, tubeSize: number) {
|
||||
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);
|
||||
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: MeshBuilder.State, streamline: Streamline, gridToCartn: Mat4, radialSegments: number, tubeSize: number, dashPoints: number, dashShift: boolean) {
|
||||
const n = streamline.length;
|
||||
if (n < 2) return;
|
||||
|
||||
const allPoints: Vec3[] = [];
|
||||
for (let i = 0; i < n; ++i) {
|
||||
allPoints.push(v3transformMat4(Vec3(), streamline[i], gridToCartn));
|
||||
}
|
||||
|
||||
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, dashStart, dashEnd, radialSegments, tubeSize);
|
||||
}
|
||||
i += cycleLength;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a tube segment from startIdx to endIdx (inclusive) with caps on both ends.
|
||||
*/
|
||||
function emitTubeSegment(state: MeshBuilder.State, points: Vec3[], startIdx: number, endIdx: number, radialSegments: number, tubeSize: number) {
|
||||
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] = tubeSize;
|
||||
heightValues[i] = tubeSize;
|
||||
}
|
||||
|
||||
computeFrenetFrames(curvePoints, normalVectors, binormalVectors, segmentLength);
|
||||
addTube(state, curvePoints, normalVectors, binormalVectors, segmentLength - 1, radialSegments, widthValues, heightValues, true, true, 'elliptical');
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const StreamlinesVisuals = {
|
||||
'lines': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, StreamlinesLinesParams>) => VolumeRepresentation('Streamlines lines', ctx, getParams, VolumeStreamlinesLinesVisual, getStreamlinesVisualLoci),
|
||||
'tube-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, StreamlinesTubeMeshParams>) => 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 type StreamlinesParams = typeof StreamlinesParams;
|
||||
|
||||
export function getStreamlinesParams(ctx: ThemeRegistryContext, volume: Volume) {
|
||||
const p = PD.clone(StreamlinesParams);
|
||||
return p;
|
||||
}
|
||||
|
||||
export type StreamlinesRepresentation = VolumeRepresentation<StreamlinesParams>
|
||||
export function StreamlinesRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, StreamlinesParams>): StreamlinesRepresentation {
|
||||
return Representation.createMulti('Streamlines', ctx, getParams, Representation.StateBuilder, StreamlinesVisuals as unknown as Representation.Def<Volume, StreamlinesParams>);
|
||||
}
|
||||
|
||||
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) => !Volume.isEmpty(volume) && !Volume.Segmentation.get(volume),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, volume: Volume) => StreamlinesProvider.attach(ctx, volume, void 0, true),
|
||||
detach: (data) => StreamlinesProvider.ref(data, false)
|
||||
},
|
||||
});
|
||||
267
src/mol-model-props/volume/streamlines/shared.ts
Normal file
267
src/mol-model-props/volume/streamlines/shared.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 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';
|
||||
import { OrderedSet } from '../../../mol-data/int/ordered-set';
|
||||
import { PickingId } from '../../../mol-geo/geometry/picking';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d';
|
||||
import { DataLocation } from '../../../mol-model/location';
|
||||
import { DataLoci, EmptyLoci, Loci } from '../../../mol-model/loci';
|
||||
import { Grid } from '../../../mol-model/volume/grid';
|
||||
import { Volume } from '../../../mol-model/volume/volume';
|
||||
import { StreamlinesProvider } from '../streamlines';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../../mol-math/linear-algebra/3d/mat4';
|
||||
|
||||
export type StreamlinePoint = Vec3
|
||||
export type Streamline = StreamlinePoint[]
|
||||
export type Streamlines = Streamline[]
|
||||
export type StreamlinesIndex = { readonly '@type': 'streamlines-index' } & number
|
||||
|
||||
//
|
||||
|
||||
type VolumeStreamlines = { readonly volume: Volume, readonly streamlines: Streamlines }
|
||||
|
||||
export interface StreamlinesLocation extends DataLocation<VolumeStreamlines, { index: StreamlinesIndex, instance: Volume.InstanceIndex }> {}
|
||||
|
||||
export function StreamlinesLocation(streamlines: Streamlines, volume: Volume, index?: StreamlinesIndex, instance?: Volume.InstanceIndex): StreamlinesLocation {
|
||||
return DataLocation('streamlines', { volume, streamlines }, { index: index as any, instance: instance as any });
|
||||
}
|
||||
|
||||
export function isStreamlinesLocation(x: any): x is StreamlinesLocation {
|
||||
return !!x && x.kind === 'data-location' && x.tag === 'streamlines';
|
||||
}
|
||||
|
||||
export function areStreamlinesLocationsEqual(locA: StreamlinesLocation, locB: StreamlinesLocation) {
|
||||
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: Streamlines, volume: Volume, index: StreamlinesIndex, instance: Volume.InstanceIndex): string {
|
||||
const label = [
|
||||
`${volume.label || 'Volume'}`,
|
||||
`Streamline #${index}`
|
||||
];
|
||||
if (volume.instances.length > 1) {
|
||||
label.push(`Instance #${instance}`);
|
||||
}
|
||||
return label.join(' | ');
|
||||
}
|
||||
|
||||
export function streamlinesLociLabel(streamlines: Streamlines, volume: Volume, elements: ReadonlyArray<StreamlinesElement>): string {
|
||||
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(' | ');
|
||||
}
|
||||
|
||||
type StreamlinesElement = {
|
||||
readonly indices: OrderedSet<StreamlinesIndex>,
|
||||
readonly instances: OrderedSet<Volume.InstanceIndex>
|
||||
}
|
||||
|
||||
export interface StreamlinesLoci extends DataLoci<VolumeStreamlines, StreamlinesElement> { }
|
||||
|
||||
export function StreamlinesLoci(streamlines: Streamlines, volume: Volume, elements: ReadonlyArray<StreamlinesElement>): StreamlinesLoci {
|
||||
return DataLoci('streamlines', { streamlines, volume }, elements,
|
||||
(boundingSphere) => getStreamlinesLociBoundingSphere(streamlines, volume, elements, boundingSphere),
|
||||
() => streamlinesLociLabel(streamlines, volume, elements));
|
||||
}
|
||||
|
||||
export function isStreamlinesLoci(x: any): x is StreamlinesLoci {
|
||||
return !!x && x.kind === 'data-loci' && x.tag === 'streamlines';
|
||||
}
|
||||
|
||||
export function getStreamlinesLociSize(elements: StreamlinesLoci['elements']): number {
|
||||
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: Streamlines, volume: Volume, elements: StreamlinesLoci['elements'], boundingSphere?: Sphere3D) {
|
||||
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: StreamlinesLoci, b: StreamlinesLoci) {
|
||||
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: StreamlinesLoci) {
|
||||
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 }),
|
||||
};
|
||||
export type CommonStreamlinesParams = typeof CommonStreamlinesParams
|
||||
export type CommonStreamlinesProps = PD.Values<CommonStreamlinesParams>
|
||||
|
||||
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: Streamline, gridToCartn: Mat4, props: CommonStreamlinesProps): boolean {
|
||||
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: Volume, _props: CommonStreamlinesProps) {
|
||||
const streamlines = StreamlinesProvider.get(volume).value!;
|
||||
const indices = Interval.ofLength(streamlines.length as Volume.InstanceIndex);
|
||||
const instances = Interval.ofLength(volume.instances.length as Volume.InstanceIndex);
|
||||
return StreamlinesLoci(streamlines, volume, [{ indices, instances }]);
|
||||
}
|
||||
|
||||
export function getStreamlinesLoci(pickingId: PickingId, volume: Volume, _key: number, _props: CommonStreamlinesProps, id: number) {
|
||||
const { objectId, groupId, instanceId } = pickingId;
|
||||
if (id === objectId) {
|
||||
const granularity = Volume.PickingGranularity.get(volume);
|
||||
const instances = OrderedSet.ofSingleton(instanceId as Volume.InstanceIndex);
|
||||
if (granularity === 'volume') return Volume.Loci(volume, instances);
|
||||
|
||||
const streamlines = StreamlinesProvider.get(volume).value!;
|
||||
const indices = OrderedSet.ofSingleton(groupId as StreamlinesIndex);
|
||||
return StreamlinesLoci(streamlines, volume, [{ indices, instances }]);
|
||||
}
|
||||
return EmptyLoci;
|
||||
}
|
||||
|
||||
export function eachStreamlines(loci: Loci, volume: Volume, _key: number, _props: CommonStreamlinesProps, apply: (interval: Interval) => boolean) {
|
||||
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: Volume): LocationIterator {
|
||||
const streamlines = StreamlinesProvider.get(volume).value!;
|
||||
const groupCount = streamlines.length;
|
||||
const instanceCount = volume.instances.length;
|
||||
|
||||
const l = StreamlinesLocation(streamlines, volume);
|
||||
const getLocation = (groupIndex: number, instanceIndex: number) => {
|
||||
l.element.index = groupIndex as StreamlinesIndex;
|
||||
l.element.instance = instanceIndex as Volume.InstanceIndex;
|
||||
return l;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -10,6 +10,10 @@ import { Tensor, Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Histogram, calculateHistogram } from '../../mol-math/histogram';
|
||||
import { lerp } from '../../mol-math/interpolate';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3transformMat4 = Vec3.transformMat4;
|
||||
const v3lerp = Vec3.lerp;
|
||||
|
||||
/** The basic unit cell that contains the grid data. */
|
||||
interface Grid {
|
||||
readonly transform: Grid.Transform,
|
||||
@@ -88,41 +92,14 @@ namespace Grid {
|
||||
const data = grid.cells.data;
|
||||
|
||||
const [mi, mj, mk] = dimensions;
|
||||
const getValue = (i: number, j: number, k: number) => get(data, i, j, k);
|
||||
|
||||
return function getTrilinearlyInterpolated(position: Vec3): number {
|
||||
Vec3.copy(gridCoords, position);
|
||||
Vec3.transformMat4(gridCoords, gridCoords, cartnToGrid);
|
||||
v3transformMat4(gridCoords, position, cartnToGrid);
|
||||
|
||||
const i = Math.trunc(gridCoords[0]);
|
||||
const j = Math.trunc(gridCoords[1]);
|
||||
const k = Math.trunc(gridCoords[2]);
|
||||
const value = trilinearlyInterpolate(gridCoords, mi, mj, mk, getValue);
|
||||
if (Number.isNaN(value)) return value;
|
||||
|
||||
if (i < 0 || i >= mi || j < 0 || j >= mj || k < 0 || k >= mk) {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
const u = gridCoords[0] - i;
|
||||
const v = gridCoords[1] - j;
|
||||
const w = gridCoords[2] - k;
|
||||
|
||||
// Tri-linear interpolation for the value
|
||||
const ii = Math.min(i + 1, mi - 1);
|
||||
const jj = Math.min(j + 1, mj - 1);
|
||||
const kk = Math.min(k + 1, mk - 1);
|
||||
|
||||
let a = get(data, i, j, k);
|
||||
let b = get(data, ii, j, k);
|
||||
let c = get(data, i, jj, k);
|
||||
let d = get(data, ii, jj, k);
|
||||
const x = lerp(lerp(a, b, u), lerp(c, d, u), v);
|
||||
|
||||
a = get(data, i, j, kk);
|
||||
b = get(data, ii, j, kk);
|
||||
c = get(data, i, jj, kk);
|
||||
d = get(data, ii, jj, kk);
|
||||
const y = lerp(lerp(a, b, u), lerp(c, d, u), v);
|
||||
|
||||
const value = lerp(x, y, w);
|
||||
if (transform === 'relative') {
|
||||
return (value - stats.mean) / stats.sigma;
|
||||
} else {
|
||||
@@ -130,6 +107,207 @@ namespace Grid {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Core trilinear interpolation function.
|
||||
* @param gridCoords - Position in grid coordinates (fractional indices)
|
||||
* @param mi, mj, mk - Grid dimensions
|
||||
* @param getValue - Function to get value at integer grid coordinates
|
||||
* @returns Interpolated value or NaN if out of bounds
|
||||
*/
|
||||
export function trilinearlyInterpolate(
|
||||
gridCoords: Vec3,
|
||||
mi: number, mj: number, mk: number,
|
||||
getValue: (i: number, j: number, k: number) => number
|
||||
): number {
|
||||
const i = Math.trunc(gridCoords[0]);
|
||||
const j = Math.trunc(gridCoords[1]);
|
||||
const k = Math.trunc(gridCoords[2]);
|
||||
|
||||
if (i < 0 || i >= mi || j < 0 || j >= mj || k < 0 || k >= mk) {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
const u = gridCoords[0] - i;
|
||||
const v = gridCoords[1] - j;
|
||||
const w = gridCoords[2] - k;
|
||||
|
||||
const ii = Math.min(i + 1, mi - 1);
|
||||
const jj = Math.min(j + 1, mj - 1);
|
||||
const kk = Math.min(k + 1, mk - 1);
|
||||
|
||||
let a = getValue(i, j, k);
|
||||
let b = getValue(ii, j, k);
|
||||
let c = getValue(i, jj, k);
|
||||
let d = getValue(ii, jj, k);
|
||||
const x = lerp(lerp(a, b, u), lerp(c, d, u), v);
|
||||
|
||||
a = getValue(i, j, kk);
|
||||
b = getValue(ii, j, kk);
|
||||
c = getValue(i, jj, kk);
|
||||
d = getValue(ii, jj, kk);
|
||||
const y = lerp(lerp(a, b, u), lerp(c, d, u), v);
|
||||
|
||||
return lerp(x, y, w);
|
||||
}
|
||||
|
||||
const _a = Vec3(), _b = Vec3(), _c = Vec3(), _d = Vec3();
|
||||
const _ab = Vec3(), _cd = Vec3(), _x = Vec3(), _y = Vec3();
|
||||
|
||||
/**
|
||||
* Core trilinear interpolation function for Vec3 values.
|
||||
* More efficient for interleaved data like gradients since getValue is called once per grid point.
|
||||
* @param gridCoords - Position in grid coordinates (fractional indices)
|
||||
* @param mi, mj, mk - Grid dimensions
|
||||
* @param getValue - Function to get Vec3 value at integer grid coordinates
|
||||
* @param out - Output Vec3
|
||||
* @returns true if interpolation succeeded, false if out of bounds
|
||||
*/
|
||||
export function trilinearlyInterpolateVec3(
|
||||
gridCoords: Vec3,
|
||||
mi: number, mj: number, mk: number,
|
||||
getValue: (i: number, j: number, k: number, out: Vec3) => void,
|
||||
out: Vec3
|
||||
): boolean {
|
||||
const i = Math.trunc(gridCoords[0]);
|
||||
const j = Math.trunc(gridCoords[1]);
|
||||
const k = Math.trunc(gridCoords[2]);
|
||||
|
||||
if (i < 0 || i >= mi || j < 0 || j >= mj || k < 0 || k >= mk) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const u = gridCoords[0] - i;
|
||||
const v = gridCoords[1] - j;
|
||||
const w = gridCoords[2] - k;
|
||||
|
||||
const ii = Math.min(i + 1, mi - 1);
|
||||
const jj = Math.min(j + 1, mj - 1);
|
||||
const kk = Math.min(k + 1, mk - 1);
|
||||
|
||||
// Interpolate in the k plane
|
||||
getValue(i, j, k, _a);
|
||||
getValue(ii, j, k, _b);
|
||||
getValue(i, jj, k, _c);
|
||||
getValue(ii, jj, k, _d);
|
||||
v3lerp(_ab, _a, _b, u);
|
||||
v3lerp(_cd, _c, _d, u);
|
||||
v3lerp(_x, _ab, _cd, v);
|
||||
|
||||
// Interpolate in the k+1 plane
|
||||
getValue(i, j, kk, _a);
|
||||
getValue(ii, j, kk, _b);
|
||||
getValue(i, jj, kk, _c);
|
||||
getValue(ii, jj, kk, _d);
|
||||
v3lerp(_ab, _a, _b, u);
|
||||
v3lerp(_cd, _c, _d, u);
|
||||
v3lerp(_y, _ab, _cd, v);
|
||||
|
||||
// Final interpolation between planes
|
||||
v3lerp(out, _x, _y, w);
|
||||
return true;
|
||||
}
|
||||
|
||||
type Gradients = {
|
||||
values: Float32Array,
|
||||
magnitude: {
|
||||
min: number,
|
||||
max: number,
|
||||
mean: number,
|
||||
sigma: number
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Pre-compute gradients at each grid cell using central differences.
|
||||
* Returns a single Float32Array with interleaved xyz components (x1, y1, z1, x2, y2, z2, ...).
|
||||
* Cached on the Grid object.
|
||||
*/
|
||||
export function getGradients(grid: Grid): Gradients {
|
||||
if ((grid as any)._gradients) {
|
||||
return (grid as any)._gradients as Gradients;
|
||||
}
|
||||
|
||||
const gradients = computeGradients(grid);
|
||||
(grid as any)._gradients = gradients;
|
||||
return gradients;
|
||||
}
|
||||
|
||||
function computeGradients(grid: Grid): Gradients {
|
||||
const { dimensions, get, dataOffset } = grid.cells.space;
|
||||
const data = grid.cells.data;
|
||||
const [mi, mj, mk] = dimensions;
|
||||
|
||||
const n = mi * mj * mk;
|
||||
const values = new Float32Array(n * 3);
|
||||
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
let sum = 0;
|
||||
let sumSq = 0;
|
||||
|
||||
for (let k = 0; k < mk; ++k) {
|
||||
for (let j = 0; j < mj; ++j) {
|
||||
for (let i = 0; i < mi; ++i) {
|
||||
const idx = dataOffset(i, j, k) * 3;
|
||||
|
||||
// Use central differences where possible, forward/backward at boundaries
|
||||
const im = Math.max(0, i - 1);
|
||||
const ip = Math.min(mi - 1, i + 1);
|
||||
const jm = Math.max(0, j - 1);
|
||||
const jp = Math.min(mj - 1, j + 1);
|
||||
const km = Math.max(0, k - 1);
|
||||
const kp = Math.min(mk - 1, k + 1);
|
||||
|
||||
// Gradient components (using central differences with proper divisor)
|
||||
const gx = (get(data, ip, j, k) - get(data, im, j, k)) / (ip - im || 1);
|
||||
const gy = (get(data, i, jp, k) - get(data, i, jm, k)) / (jp - jm || 1);
|
||||
const gz = (get(data, i, j, kp) - get(data, i, j, km)) / (kp - km || 1);
|
||||
|
||||
values[idx] = gx;
|
||||
values[idx + 1] = gy;
|
||||
values[idx + 2] = gz;
|
||||
|
||||
const mag = gx * gx + gy * gy + gz * gz;
|
||||
if (mag < min) min = mag;
|
||||
if (mag > max) max = mag;
|
||||
sum += mag;
|
||||
sumSq += mag * mag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (min === Infinity) min = 0;
|
||||
if (max === -Infinity) max = 1;
|
||||
min = Math.sqrt(min);
|
||||
max = Math.sqrt(max);
|
||||
|
||||
const mean = sum / n;
|
||||
const sigma = Math.sqrt(sumSq / n);
|
||||
|
||||
return { values, magnitude: { min, max, mean, sigma } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a function that returns trilinearly interpolated gradient at a grid position.
|
||||
* The gradient is pre-computed at grid cells and interpolated for smooth results.
|
||||
*/
|
||||
export function makeGetInterpolatedGradient(grid: Grid) {
|
||||
const { values: g } = getGradients(grid);
|
||||
const { dimensions, dataOffset } = grid.cells.space;
|
||||
const [mi, mj, mk] = dimensions;
|
||||
|
||||
const getGradientVec3 = (i: number, j: number, k: number, out: Vec3) => {
|
||||
const idx = dataOffset(i, j, k) * 3;
|
||||
out[0] = g[idx];
|
||||
out[1] = g[idx + 1];
|
||||
out[2] = g[idx + 2];
|
||||
};
|
||||
|
||||
return function getInterpolatedGradient(gridCoords: Vec3, out: Vec3): boolean {
|
||||
return trilinearlyInterpolateVec3(gridCoords, mi, mj, mk, getGradientVec3, out);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { Grid };
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -13,4 +13,6 @@ export { SecondaryStructure } from './custom-props/computed/secondary-structure'
|
||||
export { ValenceModel } from './custom-props/computed/valence-model';
|
||||
export { SIFTSMapping as BestDatabaseSequenceMapping } from './custom-props/sequence/sifts-mapping';
|
||||
|
||||
export { CrossLinkRestraint } from './custom-props/integrative/cross-link-restraint';
|
||||
export { CrossLinkRestraint } from './custom-props/integrative/cross-link-restraint';
|
||||
|
||||
export { Streamlines } from './custom-props/volume/streamlines';
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../../behavior';
|
||||
import { ParamDefinition as PD } from '../../../../../mol-util/param-definition';
|
||||
import { StreamlinesProvider } from '../../../../../mol-model-props/volume/streamlines';
|
||||
import { StreamlinesRepresentationProvider } from '../../../../../mol-model-props/volume/streamlines/representation';
|
||||
|
||||
export const Streamlines = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
name: 'streamlines-volume-prop',
|
||||
category: 'custom-props',
|
||||
display: { name: 'Streamlines' },
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
|
||||
private provider = StreamlinesProvider;
|
||||
|
||||
update(p: { autoAttach: boolean, showTooltip: boolean }) {
|
||||
const updated = (
|
||||
this.params.autoAttach !== p.autoAttach
|
||||
);
|
||||
this.params.autoAttach = p.autoAttach;
|
||||
this.ctx.customVolumeProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
|
||||
return updated;
|
||||
}
|
||||
|
||||
register(): void {
|
||||
this.ctx.customVolumeProperties.register(this.provider, this.params.autoAttach);
|
||||
this.ctx.representation.volume.registry.add(StreamlinesRepresentationProvider);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
this.ctx.customVolumeProperties.unregister(this.provider.descriptor.name);
|
||||
this.ctx.representation.volume.registry.remove(StreamlinesRepresentationProvider);
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(false)
|
||||
})
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -134,6 +134,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.SecondaryStructure),
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.ValenceModel),
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.CrossLinkRestraint),
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.Streamlines),
|
||||
],
|
||||
animations: [
|
||||
AnimateModelIndex,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -18,6 +18,7 @@ export interface VisualUpdateState {
|
||||
updateMatrix: boolean
|
||||
updateColor: boolean
|
||||
updateSize: boolean
|
||||
updateLocation: boolean
|
||||
createGeometry: boolean
|
||||
createNew: boolean
|
||||
|
||||
@@ -31,6 +32,7 @@ export namespace VisualUpdateState {
|
||||
updateMatrix: false,
|
||||
updateColor: false,
|
||||
updateSize: false,
|
||||
updateLocation: false,
|
||||
createGeometry: false,
|
||||
createNew: false,
|
||||
|
||||
@@ -42,6 +44,7 @@ export namespace VisualUpdateState {
|
||||
state.updateMatrix = false;
|
||||
state.updateColor = false;
|
||||
state.updateSize = false;
|
||||
state.updateLocation = false;
|
||||
state.createGeometry = false;
|
||||
state.createNew = false;
|
||||
}
|
||||
|
||||
@@ -173,12 +173,12 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
||||
throw new Error('expected renderObject to be available');
|
||||
}
|
||||
|
||||
if (updateState.updateColor || updateState.updateSize || updateState.updateTransform) {
|
||||
if (updateState.updateColor || updateState.updateSize || updateState.updateTransform || updateState.updateLocation) {
|
||||
// console.log('update locationIterator');
|
||||
locationIt = createLocationIterator(newVolume, newKey);
|
||||
}
|
||||
|
||||
if (updateState.updateTransform) {
|
||||
if (updateState.updateTransform || updateState.updateLocation) {
|
||||
// console.log('update transform');
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (newProps.instanceGranularity) {
|
||||
|
||||
Reference in New Issue
Block a user