mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 21:34:23 +08:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93a3eba66d | ||
|
|
41b8584fb7 | ||
|
|
523b17dfde | ||
|
|
c47b4d6078 | ||
|
|
b94073b96f | ||
|
|
905eb3ec2f | ||
|
|
3ae72e5c60 | ||
|
|
055dfd4946 | ||
|
|
2601d2ba63 | ||
|
|
340806d774 | ||
|
|
18ad848de2 | ||
|
|
9de8334af5 | ||
|
|
57580a5e6b | ||
|
|
7da4a85459 | ||
|
|
b7c380fd90 | ||
|
|
bcd304d058 | ||
|
|
fd50a8f8e0 | ||
|
|
27f251e8e4 | ||
|
|
8d2a44983e | ||
|
|
f806ac1444 | ||
|
|
63a585d88a | ||
|
|
a4b5a16fcd | ||
|
|
86bf859a63 | ||
|
|
1b8117d3f1 | ||
|
|
400e2bbc45 | ||
|
|
e2e26c7e9c | ||
|
|
5ca9020cbf | ||
|
|
ea4c411d5c | ||
|
|
ba7e3fe827 | ||
|
|
8f20571a17 | ||
|
|
c25a4247e6 | ||
|
|
1071d3d8ba | ||
|
|
e8dc046570 | ||
|
|
27f9c2aa67 | ||
|
|
a4962231c8 | ||
|
|
8833f29ce5 | ||
|
|
40b6038380 | ||
|
|
59e16e0187 | ||
|
|
ca5a50bd53 | ||
|
|
bccf54fabe | ||
|
|
57a790544c | ||
|
|
df0669598c | ||
|
|
fb912036af | ||
|
|
9efb5cd126 | ||
|
|
08a56ad6ab | ||
|
|
2c2bd6adda | ||
|
|
b010298acb | ||
|
|
7033a1e0b2 | ||
|
|
8ad617acdf | ||
|
|
31ab6aa93e | ||
|
|
0a2dbe14d7 | ||
|
|
89d305aaa1 | ||
|
|
dbb6b90fbc | ||
|
|
c57150f09f | ||
|
|
0b30c7344b | ||
|
|
d7ad5a6e9f | ||
|
|
19fec3bbc1 |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file, following t
|
||||
Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
|
||||
|
||||
## [Unreleased]
|
||||
- Fix exported image artifacts on transparent background with emissive, bloom, or antialiasing
|
||||
- Fix cel-shaded ambient color being stripped to luminance (now uses full RGB, matching the classic lighting path)
|
||||
- Fix empty transforms default in `ShapeFromPly`
|
||||
- Use morton order for spheres in dot visual with lod-levels
|
||||
- Add `Camera.changed` event and rotation/translation setter/getter
|
||||
- Add `instanceGranularity: 'auto'` as a memory guard
|
||||
- Honor `instanceGranularity` in `Visual.getLoci`
|
||||
- Add mesoscale representation preset
|
||||
- Add presets option to `ObjectList` param definition
|
||||
- Fix memory leak in `State.dispose()` not invoking transformer `dispose` callbacks for live cells
|
||||
- Fix bugs in ModelServer surroundingLigands endpoint, resulting in omitWater not honored
|
||||
- Fix `Volume` and `Isosurface` getBoundingSphere ignoring instances
|
||||
- Fix aromatic ring detection not accounting for hybridization
|
||||
- Add axis param to camera spin/rock animation
|
||||
- Fix SSAO half/quarter resolution textures for multi-scale
|
||||
- Non-covalent interactions: water bridge support
|
||||
|
||||
## [v5.9.0] - 2026-05-03
|
||||
- Fix edge case when `PluginSpec.animations` is empty
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"js"
|
||||
],
|
||||
"transform": {
|
||||
"\\.ts$": "esbuild-jest-transform"
|
||||
"\\.ts$": ["esbuild-jest-transform", { "tsconfigRaw": "{\"compilerOptions\":{\"useDefineForClassFields\":false}}" }]
|
||||
},
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
|
||||
@@ -22,7 +22,6 @@ import { Hcl } from '../../../mol-util/color/spaces/hcl';
|
||||
import { StateObjectCell, StateObjectRef, StateSelection } from '../../../mol-state';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../../mol-plugin-state/transforms/representation';
|
||||
import { SpacefillRepresentationProvider } from '../../../mol-repr/structure/representation/spacefill';
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { saturate } from '../../../mol-math/interpolate';
|
||||
import { Material } from '../../../mol-util/material';
|
||||
@@ -322,38 +321,7 @@ export function getMesoscaleGroupParams(graphicsMode: GraphicsMode): MesoscaleGr
|
||||
export type LodLevels = typeof SpacefillRepresentationProvider.defaultValues['lodLevels']
|
||||
|
||||
export function getLodLevels(graphicsMode: Exclude<GraphicsMode, 'custom'>): LodLevels {
|
||||
switch (graphicsMode) {
|
||||
case 'performance':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 300, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 300, maxDistance: 2000, overlap: 0, stride: 40, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 150, scaleBias: 3 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 300, scaleBias: 2.5 },
|
||||
];
|
||||
case 'balanced':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 500, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 500, maxDistance: 2000, overlap: 0, stride: 15, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 70, scaleBias: 2.7 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.5 },
|
||||
];
|
||||
case 'quality':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 1000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 1000, maxDistance: 4000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 4000, maxDistance: 10000, overlap: 0, stride: 50, scaleBias: 2.7 },
|
||||
{ minDistance: 10000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.3 },
|
||||
];
|
||||
case 'ultra':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
|
||||
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
|
||||
];
|
||||
default:
|
||||
assertUnreachable(graphicsMode);
|
||||
}
|
||||
return Spheres.LodLevelsPresets[graphicsMode];
|
||||
}
|
||||
|
||||
export type GraphicsMode = 'ultra' | 'quality' | 'balanced' | 'performance' | 'custom';
|
||||
|
||||
@@ -25,6 +25,7 @@ export type InteractionElementSchema =
|
||||
| { kind: 'weak-hydrogen-bond' } & InteractionElementSchemaBase
|
||||
| { kind: 'hydrophobic' } & InteractionElementSchemaBase
|
||||
| { kind: 'metal-coordination' } & InteractionElementSchemaBase
|
||||
| { kind: 'water-bridge' } & InteractionElementSchemaBase
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 } & InteractionElementSchemaBase
|
||||
|
||||
export type InteractionKind = InteractionElementSchema['kind']
|
||||
@@ -39,6 +40,7 @@ export const InteractionKinds: InteractionKind[] = [
|
||||
'weak-hydrogen-bond',
|
||||
'hydrophobic',
|
||||
'metal-coordination',
|
||||
'water-bridge',
|
||||
'covalent',
|
||||
];
|
||||
|
||||
@@ -52,6 +54,7 @@ export type InteractionInfo =
|
||||
| { kind: 'weak-hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
|
||||
| { kind: 'hydrophobic' }
|
||||
| { kind: 'metal-coordination' }
|
||||
| { kind: 'water-bridge' }
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 }
|
||||
|
||||
export interface StructureInteractionElement {
|
||||
@@ -80,4 +83,5 @@ export const InteractionTypeToKind = {
|
||||
[InteractionType.Hydrophobic]: 'hydrophobic' as InteractionKind,
|
||||
[InteractionType.MetalCoordination]: 'metal-coordination' as InteractionKind,
|
||||
[InteractionType.WeakHydrogenBond]: 'weak-hydrogen-bond' as InteractionKind,
|
||||
[InteractionType.WaterBridge]: 'water-bridge' as InteractionKind,
|
||||
};
|
||||
@@ -47,6 +47,7 @@ export const InteractionVisualParams = {
|
||||
'weak-hydrogen-bond': hydrogenVisualParams({ color: Color(0x0) }),
|
||||
'hydrophobic': visualParams({ color: Color(0x555555) }),
|
||||
'metal-coordination': visualParams({ color: Color(0x952e8f) }),
|
||||
'water-bridge': visualParams({ color: Color(0x00CCEE), style: 'dashed' }),
|
||||
'covalent': PD.Group({
|
||||
color: PD.Color(Color(0x999999)),
|
||||
radius: PD.Numeric(0.1, { min: 0.01, max: 1, step: 0.01 }),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { Viewport, cameraProject, cameraUnproject } from './camera/util';
|
||||
import { CameraTransitionManager } from './camera/transition';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { Scene } from '../mol-gl/scene';
|
||||
import { assertUnreachable } from '../mol-util/type-helpers';
|
||||
import { Ray3D } from '../mol-math/geometry/primitives/ray3d';
|
||||
@@ -15,6 +15,7 @@ import { Mat4 } from '../mol-math/linear-algebra/3d/mat4';
|
||||
import { Vec4 } from '../mol-math/linear-algebra/3d/vec4';
|
||||
import { Vec3 } from '../mol-math/linear-algebra/3d/vec3';
|
||||
import { EPSILON } from '../mol-math/linear-algebra/3d/common';
|
||||
import { Euler } from '../mol-math/linear-algebra/3d/euler';
|
||||
|
||||
export type { ICamera };
|
||||
|
||||
@@ -42,6 +43,12 @@ interface ICamera {
|
||||
}
|
||||
|
||||
const tmpClip = Vec4();
|
||||
const tmpForward = Vec3();
|
||||
const tmpRight = Vec3();
|
||||
const tmpUp = Vec3();
|
||||
const tmpBack = Vec3();
|
||||
const tmpDelta = Vec3();
|
||||
const tmpRotMat = Mat4.identity();
|
||||
|
||||
export class Camera implements ICamera {
|
||||
readonly view: Mat4 = Mat4.identity();
|
||||
@@ -70,6 +77,8 @@ export class Camera implements ICamera {
|
||||
|
||||
readonly transition: CameraTransitionManager = new CameraTransitionManager(this);
|
||||
readonly stateChanged = new BehaviorSubject<Partial<Camera.Snapshot>>(this.state);
|
||||
/** Fires whenever update() produces a changed view/projection (covers all mutations, including direct ones from controls). */
|
||||
readonly changed = new Subject<void>();
|
||||
|
||||
get position() { return this.state.position; }
|
||||
set position(v: Vec3) { Vec3.copy(this.state.position, v); }
|
||||
@@ -123,6 +132,7 @@ export class Camera implements ICamera {
|
||||
|
||||
Mat4.copy(this.prevView, this.view);
|
||||
Mat4.copy(this.prevProjection, this.projection);
|
||||
this.changed.next();
|
||||
}
|
||||
|
||||
return changed;
|
||||
@@ -237,6 +247,57 @@ export class Camera implements ICamera {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** How much the camera is rotated around its target. Uses 'ZYX' order. */
|
||||
getRotation(out: Euler) {
|
||||
const { position, target, up } = this.state;
|
||||
Vec3.normalize(tmpForward, Vec3.sub(tmpForward, target, position));
|
||||
Vec3.normalize(tmpRight, Vec3.cross(tmpRight, tmpForward, up));
|
||||
Vec3.cross(tmpUp, tmpRight, tmpForward);
|
||||
|
||||
Mat4.setIdentity(tmpRotMat);
|
||||
tmpRotMat[0] = tmpRight[0]; tmpRotMat[1] = tmpRight[1]; tmpRotMat[2] = tmpRight[2];
|
||||
tmpRotMat[4] = tmpUp[0]; tmpRotMat[5] = tmpUp[1]; tmpRotMat[6] = tmpUp[2];
|
||||
tmpRotMat[8] = -tmpForward[0]; tmpRotMat[9] = -tmpForward[1]; tmpRotMat[10] = -tmpForward[2];
|
||||
|
||||
return Euler.fromMat4(out, tmpRotMat, 'ZYX');
|
||||
}
|
||||
|
||||
/** Set the camera rotation around its target. Expects 'ZYX' order. */
|
||||
setRotation(rotation: Euler, durationMs?: number) {
|
||||
const snapshot = this.state as Camera.Snapshot;
|
||||
const distance = Vec3.distance(snapshot.position, snapshot.target);
|
||||
|
||||
Mat4.fromEuler(tmpRotMat, rotation, 'ZYX');
|
||||
|
||||
// back = R * (0,0,1) → column 2 of R
|
||||
Vec3.set(tmpBack, tmpRotMat[8], tmpRotMat[9], tmpRotMat[10]);
|
||||
// up = R * (0,1,0) → column 1 of R
|
||||
Vec3.set(tmpUp, tmpRotMat[4], tmpRotMat[5], tmpRotMat[6]);
|
||||
|
||||
const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), snapshot);
|
||||
Vec3.scaleAndAdd(state.position, snapshot.target, tmpBack, distance);
|
||||
Vec3.copy(state.up, tmpUp);
|
||||
|
||||
this.setState(state, durationMs);
|
||||
}
|
||||
|
||||
/** Translation of the camera target relative to world origin (0, 0, 0) */
|
||||
getTranslation(out: Vec3) {
|
||||
return Vec3.copy(out, this.state.target);
|
||||
}
|
||||
|
||||
/** Set the camera target to the given translation, moving position by the same delta so orientation/distance are preserved */
|
||||
setTranslation(translation: Vec3, durationMs?: number) {
|
||||
const snapshot = this.state as Camera.Snapshot;
|
||||
Vec3.sub(tmpDelta, translation, snapshot.target);
|
||||
|
||||
const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), snapshot);
|
||||
Vec3.add(state.position, snapshot.position, tmpDelta);
|
||||
Vec3.copy(state.target, translation);
|
||||
|
||||
this.setState(state, durationMs);
|
||||
}
|
||||
|
||||
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128)) {
|
||||
this.viewport = viewport;
|
||||
Camera.copySnapshot(this.state, state);
|
||||
|
||||
@@ -98,7 +98,6 @@ export const Canvas3DParams = {
|
||||
transparentBackground: PD.Boolean(false),
|
||||
checkeredTransparentBackground: PD.Boolean(false),
|
||||
dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
|
||||
enableAnimation: PD.Boolean(true, { description: 'Enable GPU time-based animations (wiggle/tumble).' }),
|
||||
pickPadding: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'Extra pixels to around target to check in case target is empty.' }),
|
||||
userInteractionReleaseMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time before the user is not considered interacting anymore.' }),
|
||||
|
||||
@@ -480,7 +479,6 @@ namespace Canvas3D {
|
||||
const hiZ = new HiZPass(webgl, passes.draw, canvas, p.hiZ);
|
||||
|
||||
const renderer = Renderer.create(webgl, p.renderer);
|
||||
renderer.setProps({ enableAnimation: p.enableAnimation });
|
||||
renderer.setOcclusionTest(hiZ.isOccluded);
|
||||
|
||||
const shaderManager = new ShaderManager(webgl, scene);
|
||||
@@ -677,7 +675,7 @@ namespace Canvas3D {
|
||||
const xrChanged = xrManager.update(xrFrame);
|
||||
if (!xrChanged && xrFrame) return false;
|
||||
|
||||
const activeAnimation = p.enableAnimation && scene.hasAnimation;
|
||||
const activeAnimation = renderer.props.enableAnimation && scene.hasAnimation;
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged || activeAnimation;
|
||||
forceNextRender = false;
|
||||
|
||||
@@ -1071,7 +1069,6 @@ namespace Canvas3D {
|
||||
transparentBackground: p.transparentBackground,
|
||||
checkeredTransparentBackground: p.checkeredTransparentBackground,
|
||||
dpoitIterations: p.dpoitIterations,
|
||||
enableAnimation: p.enableAnimation,
|
||||
pickPadding: p.pickPadding,
|
||||
userInteractionReleaseMs: p.userInteractionReleaseMs,
|
||||
viewport: p.viewport,
|
||||
@@ -1317,10 +1314,6 @@ namespace Canvas3D {
|
||||
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
|
||||
if (props.checkeredTransparentBackground !== undefined) p.checkeredTransparentBackground = props.checkeredTransparentBackground;
|
||||
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
|
||||
if (props.enableAnimation !== undefined) {
|
||||
p.enableAnimation = props.enableAnimation;
|
||||
renderer.setProps({ enableAnimation: p.enableAnimation });
|
||||
}
|
||||
if (props.pickPadding !== undefined) {
|
||||
p.pickPadding = props.pickPadding;
|
||||
pickHelper.setPickPadding(p.pickPadding);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
@@ -484,27 +484,45 @@ export class SsaoPass {
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
|
||||
}
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
|
||||
if (multiScale) {
|
||||
// half-resolution viewport (matches dimensions of depthHalfTarget*)
|
||||
const hsx = Math.floor(sx * 0.5);
|
||||
const hsy = Math.floor(sy * 0.5);
|
||||
const hsw = Math.ceil(sw * 0.5);
|
||||
const hsh = Math.ceil(sh * 0.5);
|
||||
state.viewport(hsx, hsy, hsw, hsh);
|
||||
state.scissor(hsx, hsy, hsw, hsh);
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
|
||||
this.depthHalfTargetOpaque.bind();
|
||||
this.depthHalfRenderableOpaque.render();
|
||||
}
|
||||
if (multiScale && includeTransparent) {
|
||||
this.depthHalfTargetTransparent.bind();
|
||||
this.depthHalfRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
|
||||
if (includeTransparent) {
|
||||
this.depthHalfTargetTransparent.bind();
|
||||
this.depthHalfRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
|
||||
if (multiScale) {
|
||||
// quarter-resolution viewport (matches dimensions of depthQuarterTarget*)
|
||||
const qsx = Math.floor(sx * 0.25);
|
||||
const qsy = Math.floor(sy * 0.25);
|
||||
const qsw = Math.ceil(sw * 0.25);
|
||||
const qsh = Math.ceil(sh * 0.25);
|
||||
state.viewport(qsx, qsy, qsw, qsh);
|
||||
state.scissor(qsx, qsy, qsw, qsh);
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
|
||||
this.depthQuarterTargetOpaque.bind();
|
||||
this.depthQuarterRenderableOpaque.render();
|
||||
if (includeTransparent) {
|
||||
this.depthQuarterTargetTransparent.bind();
|
||||
this.depthQuarterRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
|
||||
|
||||
// restore full-scale viewport for SSAO + blur passes
|
||||
state.viewport(sx, sy, sw, sh);
|
||||
state.scissor(sx, sy, sw, sh);
|
||||
}
|
||||
if (multiScale && includeTransparent) {
|
||||
this.depthQuarterTargetTransparent.bind();
|
||||
this.depthQuarterRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.opaque');
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.framebuffer, 'color0');
|
||||
|
||||
@@ -72,6 +72,25 @@ export function getColorSmoothingProps(smoothColors: PD.Values<ColorSmoothingPar
|
||||
|
||||
//
|
||||
|
||||
export type InstanceGranularityValue = true | false | 'auto'
|
||||
export const InstanceGranularityOptions: [InstanceGranularityValue, string][] = [[true, 'On'], [false, 'Off'], ['auto', 'Auto']];
|
||||
|
||||
/**
|
||||
* Threshold (in `groupCount * instanceCount`, e.g. number of marker-texture
|
||||
* slots) above which `instanceGranularity: 'auto'` resolves to `true`.
|
||||
*/
|
||||
export const AutoInstanceGranularityThreshold = 50_000_000;
|
||||
|
||||
/**
|
||||
* Resolves the `instanceGranularity` param value to a boolean.
|
||||
*/
|
||||
export function resolveInstanceGranularity(value: InstanceGranularityValue, groupCount: number, instanceCount: number): boolean {
|
||||
if (value === 'auto') return groupCount * instanceCount > AutoInstanceGranularityThreshold;
|
||||
return value;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export namespace BaseGeometry {
|
||||
export const MaterialCategory: PD.Info = { category: 'Material' };
|
||||
export const ShadingCategory: PD.Info = { category: 'Shading' };
|
||||
@@ -88,7 +107,7 @@ export namespace BaseGeometry {
|
||||
clip: PD.Group(Clip.Params),
|
||||
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
|
||||
density: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }, { description: 'Density value to estimate object thickness.' }),
|
||||
instanceGranularity: PD.Boolean(false, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory.' }),
|
||||
instanceGranularity: PD.Select<InstanceGranularityValue>('auto', InstanceGranularityOptions, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory. When set to `auto`, granularity is enabled if `groupCount * instanceCount` exceeds `AutoInstanceGranularityThreshold`.' }),
|
||||
lod: PD.Vec3(Vec3(), undefined, { ...CullingLodCategory, description: 'Level of detail.', fieldLabels: { x: 'Min Distance', y: 'Max Distance', z: 'Overlap (Shader)' } }),
|
||||
cellSize: PD.Numeric(200, { min: 0, max: 5000, step: 100 }, { ...CullingLodCategory, description: 'Instance grid cell size.' }),
|
||||
batchSize: PD.Numeric(2000, { min: 0, max: 50000, step: 500 }, { ...CullingLodCategory, description: 'Instance grid batch size.' }),
|
||||
@@ -130,7 +149,7 @@ export namespace BaseGeometry {
|
||||
uClipObjectScale: ValueCell.create(clip.objects.scale),
|
||||
uClipObjectTransform: ValueCell.create(clip.objects.transform),
|
||||
|
||||
instanceGranularity: ValueCell.create(props.instanceGranularity),
|
||||
instanceGranularity: ValueCell.create(resolveInstanceGranularity(props.instanceGranularity, counts.groupCount, counts.instanceCount)),
|
||||
uLod: ValueCell.create(Vec4.create(props.lod[0], props.lod[1], props.lod[2], 0)),
|
||||
};
|
||||
}
|
||||
@@ -153,7 +172,7 @@ export namespace BaseGeometry {
|
||||
ValueCell.update(values.uClipObjectScale, clip.objects.scale);
|
||||
ValueCell.update(values.uClipObjectTransform, clip.objects.transform);
|
||||
|
||||
ValueCell.updateIfChanged(values.instanceGranularity, props.instanceGranularity);
|
||||
ValueCell.updateIfChanged(values.instanceGranularity, resolveInstanceGranularity(props.instanceGranularity, values.uGroupCount.ref.value, values.instanceCount.ref.value));
|
||||
ValueCell.update(values.uLod, Vec4.set(values.uLod.ref.value, props.lod[0], props.lod[1], props.lod[2], 0));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -225,7 +225,7 @@ export namespace Cylinders {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ValueCell } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Box } from '../../primitive/box';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createColors } from '../color-data';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { createMarkers } from '../marker-data';
|
||||
@@ -228,7 +228,7 @@ export namespace DirectVolume {
|
||||
const positionIt = createPositionIterator(directVolume, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Theme } from '../../../mol-theme/theme';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createColors } from '../color-data';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { createMarkers } from '../marker-data';
|
||||
@@ -201,7 +201,7 @@ namespace Image {
|
||||
const positionIt = createPositionIterator(image, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
|
||||
@@ -21,7 +21,7 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -232,7 +232,7 @@ export namespace Lines {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
|
||||
@@ -20,7 +20,7 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { MeshValues } from '../../../mol-gl/renderable/mesh';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
@@ -684,7 +684,7 @@ export namespace Mesh {
|
||||
const positionIt = createPositionIterator(mesh, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Theme } from '../../../mol-theme/theme';
|
||||
import { PointsValues } from '../../../mol-gl/renderable/points';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -178,7 +178,7 @@ export namespace Points {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
|
||||
@@ -17,7 +17,7 @@ import { TextureImage, calculateInvariantBoundingSphere, calculateTransformBound
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { createSizes, getMaxSize } from '../size-data';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -249,6 +249,33 @@ export namespace Spheres {
|
||||
return lodLevels.map(l => getAdjustedStride(l, sizeFactor)).reverse();
|
||||
}
|
||||
|
||||
export const LodLevelsPresets: { [key in 'performance' | 'balanced' | 'quality' | 'ultra']: LodLevels } = {
|
||||
performance: [
|
||||
{ minDistance: 1, maxDistance: 300, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 300, maxDistance: 2000, overlap: 0, stride: 40, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 150, scaleBias: 3 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 300, scaleBias: 2.5 },
|
||||
],
|
||||
balanced: [
|
||||
{ minDistance: 1, maxDistance: 500, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 500, maxDistance: 2000, overlap: 0, stride: 15, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 70, scaleBias: 2.7 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.5 },
|
||||
],
|
||||
quality: [
|
||||
{ minDistance: 1, maxDistance: 1000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 1000, maxDistance: 4000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 4000, maxDistance: 10000, overlap: 0, stride: 50, scaleBias: 2.7 },
|
||||
{ minDistance: 10000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.3 },
|
||||
],
|
||||
ultra: [
|
||||
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
|
||||
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
export const Params = {
|
||||
...BaseGeometry.Params,
|
||||
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
|
||||
@@ -273,7 +300,8 @@ export namespace Spheres {
|
||||
scaleBias: PD.Numeric(3, { min: 0.1, max: 10, step: 0.1 }),
|
||||
}, o => `${o.stride}`, {
|
||||
...BaseGeometry.CullingLodCategory,
|
||||
defaultValue: [] as LodLevels
|
||||
defaultValue: [] as LodLevels,
|
||||
presets: Object.entries(LodLevelsPresets).map(([k, v]) => [v, k])
|
||||
})
|
||||
};
|
||||
export type Params = typeof Params
|
||||
@@ -314,7 +342,7 @@ export namespace Spheres {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
|
||||
@@ -25,7 +25,7 @@ import { FontAtlasParams } from './font-atlas';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { clamp } from '../../../mol-math/interpolate';
|
||||
import { createRenderObject as _createRenderObject } from '../../../mol-gl/render-object';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -219,7 +219,7 @@ export namespace Text {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
|
||||
@@ -15,7 +15,7 @@ import { createMarkers } from '../marker-data';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
|
||||
@@ -203,7 +203,7 @@ export namespace TextureMesh {
|
||||
const positionIt = Utils.createPositionIterator(textureMesh, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
|
||||
@@ -78,7 +78,7 @@ export const apply_light_color = `
|
||||
}
|
||||
#pragma unroll_loop_end
|
||||
|
||||
outgoingLight += physicalMaterial.diffuseColor * luminance(uAmbientColor);
|
||||
outgoingLight += physicalMaterial.diffuseColor * uAmbientColor;
|
||||
#else
|
||||
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ export enum InteractionType {
|
||||
Hydrophobic = 6,
|
||||
MetalCoordination = 7,
|
||||
WeakHydrogenBond = 8,
|
||||
WaterBridge = 9,
|
||||
}
|
||||
|
||||
export function interactionTypeLabel(type: InteractionType): string {
|
||||
@@ -153,6 +154,8 @@ export function interactionTypeLabel(type: InteractionType): string {
|
||||
return 'Pi Stacking';
|
||||
case InteractionType.WeakHydrogenBond:
|
||||
return 'Weak Hydrogen Bond';
|
||||
case InteractionType.WaterBridge:
|
||||
return 'Water Bridge';
|
||||
case InteractionType.Unknown:
|
||||
return 'Unknown Interaction';
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { FeatureType, FeatureGroup, InteractionType } from './common';
|
||||
import { ContactProvider } from './contacts';
|
||||
import { MoleculeType, ProteinBackboneAtoms } from '../../../mol-model/structure/model/types';
|
||||
|
||||
const GeometryParams = {
|
||||
export const GeometryParams = {
|
||||
distanceMax: PD.Numeric(3.5, { min: 1, max: 5, step: 0.1 }),
|
||||
backbone: PD.Boolean(true, { description: 'Include backbone-to-backbone hydrogen bonds' }),
|
||||
accAngleDevMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal acceptor angle' }),
|
||||
@@ -29,7 +29,7 @@ const GeometryParams = {
|
||||
accOutOfPlaneAngleMax: PD.Numeric(90, { min: 0, max: 180, step: 1 }),
|
||||
donOutOfPlaneAngleMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }),
|
||||
};
|
||||
type GeometryParams = typeof GeometryParams
|
||||
export type GeometryParams = typeof GeometryParams
|
||||
type GeometryProps = PD.Values<GeometryParams>
|
||||
|
||||
const HydrogenBondsParams = {
|
||||
@@ -208,7 +208,7 @@ function isWeakHydrogenBond(ti: FeatureType, tj: FeatureType) {
|
||||
);
|
||||
}
|
||||
|
||||
function getGeometryOptions(props: GeometryProps) {
|
||||
export function getGeometryOptions(props: GeometryProps) {
|
||||
return {
|
||||
ignoreHydrogens: props.ignoreHydrogens,
|
||||
includeBackbone: props.backbone,
|
||||
@@ -218,7 +218,7 @@ function getGeometryOptions(props: GeometryProps) {
|
||||
maxDonOutOfPlaneAngle: degToRad(props.donOutOfPlaneAngleMax),
|
||||
};
|
||||
}
|
||||
type GeometryOptions = ReturnType<typeof getGeometryOptions>
|
||||
export type GeometryOptions = ReturnType<typeof getGeometryOptions>
|
||||
|
||||
function getHydrogenBondsOptions(props: HydrogenBondsProps) {
|
||||
return {
|
||||
@@ -232,7 +232,7 @@ type HydrogenBondsOptions = ReturnType<typeof getHydrogenBondsOptions>
|
||||
|
||||
const deg120InRad = degToRad(120);
|
||||
|
||||
function checkGeometry(structure: Structure, don: Features.Info, acc: Features.Info, opts: GeometryOptions): true | undefined {
|
||||
export function checkGeometry(structure: Structure, don: Features.Info, acc: Features.Info, opts: GeometryOptions): true | undefined {
|
||||
const donIndex = don.members[don.offsets[don.feature]];
|
||||
const accIndex = acc.members[acc.offsets[acc.feature]];
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Structure, Unit, Bond } from '../../../mol-model/structure';
|
||||
import { Structure, Unit, Bond, StructureElement } from '../../../mol-model/structure';
|
||||
import { Features, FeaturesBuilder } from './features';
|
||||
import { ValenceModelProvider } from '../valence-model';
|
||||
import { InteractionsIntraContacts, InteractionsInterContacts, FeatureType, interactionTypeLabel } from './common';
|
||||
import { InteractionsIntraContacts, InteractionsInterContacts, FeatureType, InteractionType, InteractionFlag, interactionTypeLabel } from './common';
|
||||
import { IntraContactsBuilder, InterContactsBuilder } from './contacts-builder';
|
||||
import { IntMap } from '../../../mol-data/int';
|
||||
import { IntMap, OrderedSet } from '../../../mol-data/int';
|
||||
import { addUnitContacts, ContactTester, addStructureContacts, ContactsParams, ContactsProps } from './contacts';
|
||||
import { HalogenDonorProvider, HalogenAcceptorProvider, HalogenBondsProvider } from './halogen-bonds';
|
||||
import { HydrogenDonorProvider, WeakHydrogenDonorProvider, HydrogenAcceptorProvider, HydrogenBondsProvider, WeakHydrogenBondsProvider } from './hydrogen-bonds';
|
||||
import { WaterBridgesProvider } from './water-bridges';
|
||||
import { NegativChargeProvider, PositiveChargeProvider, AromaticRingProvider, IonicProvider, PiStackingProvider, CationPiProvider } from './charged';
|
||||
import { HydrophobicAtomProvider, HydrophobicProvider } from './hydrophobic';
|
||||
import { SetUtils } from '../../../mol-util/set';
|
||||
@@ -25,10 +26,26 @@ import { DataLocation } from '../../../mol-model/location';
|
||||
import { CentroidHelper } from '../../../mol-math/geometry/centroid-helper';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { DataLoci } from '../../../mol-model/loci';
|
||||
import { bondLabel, LabelGranularity } from '../../../mol-theme/label';
|
||||
import { bondLabel, bundleLabel, LabelGranularity } from '../../../mol-theme/label';
|
||||
import { ObjectKeys } from '../../../mol-util/type-helpers';
|
||||
|
||||
export { Interactions };
|
||||
export { Interactions, Bridges };
|
||||
export type { BridgeContact, BridgeContacts };
|
||||
|
||||
interface BridgeContact {
|
||||
readonly unitA: number
|
||||
readonly indexA: Features.FeatureIndex
|
||||
readonly unitB: number
|
||||
readonly indexB: Features.FeatureIndex
|
||||
/** mediator unit id */
|
||||
readonly unitM: number
|
||||
/** mediator feature facing endpoint A */
|
||||
readonly indexMA: Features.FeatureIndex
|
||||
/** mediator feature facing endpoint B */
|
||||
readonly indexMB: Features.FeatureIndex
|
||||
props: { type: InteractionType, flag: InteractionFlag }
|
||||
}
|
||||
type BridgeContacts = ReadonlyArray<BridgeContact>
|
||||
|
||||
interface Interactions {
|
||||
/** Features of each unit */
|
||||
@@ -37,6 +54,8 @@ interface Interactions {
|
||||
unitsContacts: IntMap<InteractionsIntraContacts>
|
||||
/** Interactions between units */
|
||||
contacts: InteractionsInterContacts
|
||||
/** Bridge-mediated interactions covering the whole structure */
|
||||
bridges: BridgeContacts
|
||||
}
|
||||
|
||||
namespace Interactions {
|
||||
@@ -129,6 +148,93 @@ namespace Interactions {
|
||||
}
|
||||
}
|
||||
|
||||
namespace Bridges {
|
||||
export interface Data {
|
||||
readonly structure: Structure
|
||||
readonly bridges: BridgeContacts
|
||||
readonly unitsFeatures: IntMap<Features>
|
||||
}
|
||||
|
||||
export interface Element { bridgeIndex: number }
|
||||
|
||||
export interface Location extends DataLocation<Data, Element> {}
|
||||
|
||||
export function Location(data: Data, bridgeIndex = 0): Location {
|
||||
return DataLocation('bridges', data, { bridgeIndex });
|
||||
}
|
||||
|
||||
export function isLocation(x: any): x is Location {
|
||||
return !!x && x.kind === 'data-location' && x.tag === 'bridges';
|
||||
}
|
||||
|
||||
export interface Loci extends DataLoci<Data, Element> {}
|
||||
|
||||
export function Loci(data: Data, elements: ReadonlyArray<Element>): Loci {
|
||||
return DataLoci('bridges', data, elements,
|
||||
bs => getBoundingSphere(data, elements, bs),
|
||||
() => getLabel(data, elements));
|
||||
}
|
||||
|
||||
export function isLoci(x: any): x is Loci {
|
||||
return !!x && x.kind === 'data-loci' && x.tag === 'bridges';
|
||||
}
|
||||
|
||||
function getLabel(data: Data, elements: ReadonlyArray<Element>): string {
|
||||
const e = elements[0];
|
||||
if (e === undefined) return '';
|
||||
|
||||
const { structure, bridges, unitsFeatures } = data;
|
||||
const bridge = bridges[e.bridgeIndex];
|
||||
|
||||
const uA = structure.unitMap.get(bridge.unitA) as Unit.Atomic;
|
||||
const fA = unitsFeatures.get(bridge.unitA);
|
||||
const uM = structure.unitMap.get(bridge.unitM) as Unit.Atomic;
|
||||
const fM = unitsFeatures.get(bridge.unitM);
|
||||
const uB = structure.unitMap.get(bridge.unitB) as Unit.Atomic;
|
||||
const fB = unitsFeatures.get(bridge.unitB);
|
||||
|
||||
const options = { granularity: 'element' as LabelGranularity };
|
||||
if (fA.offsets[bridge.indexA + 1] - fA.offsets[bridge.indexA] > 1 ||
|
||||
fB.offsets[bridge.indexB + 1] - fB.offsets[bridge.indexB] > 1) {
|
||||
options.granularity = 'residue';
|
||||
}
|
||||
|
||||
return [
|
||||
interactionTypeLabel(bridge.props.type),
|
||||
bundleLabel({ loci: [
|
||||
StructureElement.Loci(structure, [{ unit: uA, indices: OrderedSet.ofSingleton(fA.members[fA.offsets[bridge.indexA]] as StructureElement.UnitIndex) }]),
|
||||
StructureElement.Loci(structure, [{ unit: uM, indices: OrderedSet.ofSingleton(fM.members[fM.offsets[bridge.indexMA]] as StructureElement.UnitIndex) }]),
|
||||
StructureElement.Loci(structure, [{ unit: uB, indices: OrderedSet.ofSingleton(fB.members[fB.offsets[bridge.indexB]] as StructureElement.UnitIndex) }]),
|
||||
] }, options),
|
||||
].join('</br>');
|
||||
}
|
||||
|
||||
function getBoundingSphere(data: Data, elements: ReadonlyArray<Element>, boundingSphere: Sphere3D) {
|
||||
return CentroidHelper.fromPairProvider(elements.length * 2, (i, pA, pB) => {
|
||||
const bridge = data.bridges[elements[i >> 1].bridgeIndex];
|
||||
|
||||
const uA = data.structure.unitMap.get(bridge.unitA) as Unit.Atomic;
|
||||
const fA = data.unitsFeatures.get(bridge.unitA);
|
||||
const uM = data.structure.unitMap.get(bridge.unitM) as Unit.Atomic;
|
||||
const fM = data.unitsFeatures.get(bridge.unitM);
|
||||
const uB = data.structure.unitMap.get(bridge.unitB) as Unit.Atomic;
|
||||
const fB = data.unitsFeatures.get(bridge.unitB);
|
||||
|
||||
const aIdx = fA.members[fA.offsets[bridge.indexA]];
|
||||
const mIdx = fM.members[fM.offsets[bridge.indexMA]];
|
||||
const bIdx = fB.members[fB.offsets[bridge.indexB]];
|
||||
|
||||
if ((i & 1) === 0) {
|
||||
uA.conformation.position(uA.elements[aIdx], pA);
|
||||
uM.conformation.position(uM.elements[mIdx], pB);
|
||||
} else {
|
||||
uM.conformation.position(uM.elements[mIdx], pA);
|
||||
uB.conformation.position(uB.elements[bIdx], pB);
|
||||
}
|
||||
}, boundingSphere);
|
||||
}
|
||||
}
|
||||
|
||||
const FeatureProviders = [
|
||||
HydrogenDonorProvider, WeakHydrogenDonorProvider, HydrogenAcceptorProvider,
|
||||
NegativChargeProvider, PositiveChargeProvider, AromaticRingProvider,
|
||||
@@ -174,8 +280,30 @@ export const ContactProviderParams = getProvidersParams([
|
||||
// 'weak-hydrogen-bonds',
|
||||
]);
|
||||
|
||||
const BridgeProviders = {
|
||||
'water-bridges': WaterBridgesProvider,
|
||||
};
|
||||
type BridgeProviders = typeof BridgeProviders
|
||||
|
||||
function getBridgeProviderParams(defaultOn: string[] = []) {
|
||||
const params: { [k in keyof BridgeProviders]: PD.Mapped<PD.NamedParamUnion<{
|
||||
on: PD.Group<BridgeProviders[k]['params']>
|
||||
off: PD.Group<{}>
|
||||
}>> } = Object.create(null);
|
||||
|
||||
Object.keys(BridgeProviders).forEach(k => {
|
||||
(params as any)[k] = PD.MappedStatic(defaultOn.includes(k) ? 'on' : 'off', {
|
||||
on: PD.Group(BridgeProviders[k as keyof BridgeProviders].params),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true });
|
||||
});
|
||||
return params;
|
||||
}
|
||||
export const BridgeProviderParams = getBridgeProviderParams([]);
|
||||
|
||||
export const InteractionsParams = {
|
||||
providers: PD.Group(ContactProviderParams, { isFlat: true }),
|
||||
bridges: PD.Group(BridgeProviderParams, { isFlat: true }),
|
||||
contacts: PD.Group(ContactsParams, { label: 'Advanced Options' }),
|
||||
};
|
||||
export type InteractionsParams = typeof InteractionsParams
|
||||
@@ -202,6 +330,9 @@ export async function computeInteractions(ctx: CustomProperty.Context, structure
|
||||
|
||||
const requiredFeatures = new Set<FeatureType>();
|
||||
contactTesters.forEach(l => SetUtils.add(requiredFeatures, l.requiredFeatures));
|
||||
ObjectKeys(BridgeProviders).forEach(k => {
|
||||
if (p.bridges[k].name === 'on') SetUtils.add(requiredFeatures, BridgeProviders[k].requiredFeatures);
|
||||
});
|
||||
const featureProviders = FeatureProviders.filter(f => SetUtils.areIntersecting(requiredFeatures, f.types));
|
||||
|
||||
const unitsFeatures = IntMap.Mutable<Features>();
|
||||
@@ -228,8 +359,9 @@ export async function computeInteractions(ctx: CustomProperty.Context, structure
|
||||
}
|
||||
|
||||
const contacts = findInterUnitContacts(structure, unitsFeatures, contactTesters, p.contacts, options);
|
||||
const bridges = findBridges(structure, unitsFeatures, p.bridges);
|
||||
const interactions = { unitsFeatures, unitsContacts, contacts, bridges };
|
||||
|
||||
const interactions = { unitsFeatures, unitsContacts, contacts };
|
||||
refineInteractions(structure, interactions);
|
||||
return interactions;
|
||||
}
|
||||
@@ -260,6 +392,19 @@ function findIntraUnitContacts(structure: Structure, unit: Unit, features: Featu
|
||||
return builder.getContacts();
|
||||
}
|
||||
|
||||
function findBridges(structure: Structure, unitsFeatures: IntMap<Features>, props: PD.Values<typeof BridgeProviderParams>): BridgeContacts {
|
||||
const bridges: BridgeContact[] = [];
|
||||
|
||||
ObjectKeys(BridgeProviders).forEach(k => {
|
||||
const { name, params } = props[k];
|
||||
if (name === 'on') {
|
||||
for (const b of BridgeProviders[k].find(structure, unitsFeatures, params as any)) bridges.push(b);
|
||||
}
|
||||
});
|
||||
|
||||
return bridges;
|
||||
}
|
||||
|
||||
function findInterUnitContacts(structure: Structure, unitsFeatures: IntMap<Features>, contactTesters: ReadonlyArray<ContactTester>, props: ContactsProps, options?: ComputeInterctionsOptions) {
|
||||
const builder = InterContactsBuilder.create();
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*
|
||||
* based in part on NGL (https://github.com/arose/ngl)
|
||||
*/
|
||||
|
||||
import { Interactions } from './interactions';
|
||||
import { InteractionType, InteractionFlag, InteractionsIntraContacts, FeatureType, InteractionsInterContacts } from './common';
|
||||
import { Unit, Structure } from '../../../mol-model/structure';
|
||||
import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { Features } from './features';
|
||||
import { cantorPairing } from '../../../mol-data/util/hash-functions';
|
||||
|
||||
interface ContactRefiner {
|
||||
isApplicable: (type: InteractionType) => boolean
|
||||
@@ -27,6 +29,7 @@ export function refineInteractions(structure: Structure, interactions: Interacti
|
||||
saltBridgeRefiner(structure, interactions),
|
||||
piStackingRefiner(structure, interactions),
|
||||
metalCoordinationRefiner(structure, interactions),
|
||||
waterBridgeRefiner(structure, interactions),
|
||||
];
|
||||
|
||||
for (let i = 0, il = contacts.edgeCount; i < il; ++i) {
|
||||
@@ -278,4 +281,117 @@ function metalCoordinationRefiner(structure: Structure, interactions: Interactio
|
||||
filterIntra([InteractionType.MetalCoordination], index, infoA, infoB, interactions.unitsContacts.get(infoA.unit.id));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function waterBridgeRefiner(_structure: Structure, interactions: Interactions): ContactRefiner {
|
||||
const { contacts, bridges, unitsFeatures } = interactions;
|
||||
|
||||
type AtomKey = number;
|
||||
type AtomPairSet = Map<AtomKey, Set<AtomKey>>;
|
||||
|
||||
function atomKey(unitId: number, atomIndex: StructureElement.UnitIndex): AtomKey {
|
||||
return cantorPairing(unitId, atomIndex);
|
||||
}
|
||||
|
||||
function featureMember(features: Features, featureIndex: Features.FeatureIndex): StructureElement.UnitIndex {
|
||||
return features.members[features.offsets[featureIndex]] as StructureElement.UnitIndex;
|
||||
}
|
||||
|
||||
function addAtomPair(
|
||||
set: AtomPairSet,
|
||||
unitA: number,
|
||||
atomA: StructureElement.UnitIndex,
|
||||
unitB: number,
|
||||
atomB: StructureElement.UnitIndex
|
||||
) {
|
||||
const a = atomKey(unitA, atomA);
|
||||
const b = atomKey(unitB, atomB);
|
||||
|
||||
let bs = set.get(a);
|
||||
if (bs === undefined) {
|
||||
bs = new Set();
|
||||
set.set(a, bs);
|
||||
}
|
||||
bs.add(b);
|
||||
|
||||
let as = set.get(b);
|
||||
if (as === undefined) {
|
||||
as = new Set();
|
||||
set.set(b, as);
|
||||
}
|
||||
as.add(a);
|
||||
}
|
||||
|
||||
function hasAtomPair(
|
||||
set: AtomPairSet,
|
||||
unitA: number,
|
||||
atomA: StructureElement.UnitIndex,
|
||||
unitB: number,
|
||||
atomB: StructureElement.UnitIndex
|
||||
): boolean {
|
||||
return set.get(atomKey(unitA, atomA))?.has(atomKey(unitB, atomB)) === true;
|
||||
}
|
||||
|
||||
function hasInfoPair(set: AtomPairSet, infoA: Features.Info, infoB: Features.Info): boolean {
|
||||
const { offsets: offsetsA, members: membersA, feature: featureA } = infoA;
|
||||
const { offsets: offsetsB, members: membersB, feature: featureB } = infoB;
|
||||
|
||||
for (let i = offsetsA[featureA], il = offsetsA[featureA + 1]; i < il; ++i) {
|
||||
const a = membersA[i] as StructureElement.UnitIndex;
|
||||
|
||||
for (let j = offsetsB[featureB], jl = offsetsB[featureB + 1]; j < jl; ++j) {
|
||||
const b = membersB[j] as StructureElement.UnitIndex;
|
||||
|
||||
if (hasAtomPair(set, infoA.unit.id, a, infoB.unit.id, b)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const bridgeLegs: AtomPairSet = new Map();
|
||||
|
||||
for (const wb of bridges) {
|
||||
if (wb.props.type !== InteractionType.WaterBridge) continue;
|
||||
|
||||
const fA = unitsFeatures.get(wb.unitA);
|
||||
const fM = unitsFeatures.get(wb.unitM);
|
||||
const fB = unitsFeatures.get(wb.unitB);
|
||||
|
||||
if (!fA || !fM || !fB) continue;
|
||||
|
||||
const atomA = featureMember(fA, wb.indexA);
|
||||
const atomMA = featureMember(fM, wb.indexMA);
|
||||
const atomMB = featureMember(fM, wb.indexMB);
|
||||
const atomB = featureMember(fB, wb.indexB);
|
||||
|
||||
// donor atom ↔ water oxygen
|
||||
addAtomPair(bridgeLegs, wb.unitA, atomA, wb.unitM, atomMA);
|
||||
|
||||
// water oxygen ↔ acceptor atom
|
||||
addAtomPair(bridgeLegs, wb.unitM, atomMB, wb.unitB, atomB);
|
||||
}
|
||||
|
||||
let intraContacts: InteractionsIntraContacts | undefined;
|
||||
|
||||
return {
|
||||
isApplicable: (type: InteractionType) => {
|
||||
return bridgeLegs.size > 0 && type === InteractionType.HydrogenBond;
|
||||
},
|
||||
handleInterContact: (index: number, infoA: Features.Info, infoB: Features.Info) => {
|
||||
if (hasInfoPair(bridgeLegs, infoA, infoB)) {
|
||||
contacts.edges[index].props.flag = InteractionFlag.Filtered;
|
||||
}
|
||||
},
|
||||
startUnit: (_unit: Unit.Atomic, contacts: InteractionsIntraContacts) => {
|
||||
intraContacts = contacts;
|
||||
},
|
||||
handleIntraContact: (index: number, infoA: Features.Info, infoB: Features.Info) => {
|
||||
if (!intraContacts) return;
|
||||
|
||||
if (hasInfoPair(bridgeLegs, infoA, infoB)) {
|
||||
intraContacts.edgeProps.flag[index] = InteractionFlag.Filtered;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
331
src/mol-model-props/computed/interactions/water-bridges.ts
Normal file
331
src/mol-model-props/computed/interactions/water-bridges.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*/
|
||||
|
||||
import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
|
||||
import { IntMap } from '../../../mol-data/int';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { MoleculeType, NucleicBackboneAtoms, ProteinBackboneAtoms } from '../../../mol-model/structure/model/types';
|
||||
import { StructureLookup3DResultContext } from '../../../mol-model/structure/structure/util/lookup3d';
|
||||
import { Features } from './features';
|
||||
import { FeatureType, InteractionType, InteractionFlag } from './common';
|
||||
import { GeometryOptions, checkGeometry } from './hydrogen-bonds';
|
||||
import { degToRad } from '../../../mol-math/misc';
|
||||
import { cantorPairing } from '../../../mol-data/util/hash-functions';
|
||||
|
||||
export type { WaterBridgeContact, WaterBridgeContacts };
|
||||
|
||||
interface WaterBridgeContact {
|
||||
/** non-water donor unit id */
|
||||
readonly unitA: number
|
||||
/** donor feature index in unitA */
|
||||
readonly indexA: Features.FeatureIndex
|
||||
/** non-water acceptor unit id */
|
||||
readonly unitB: number
|
||||
/** acceptor feature index in unitB */
|
||||
readonly indexB: Features.FeatureIndex
|
||||
/** bridging water unit id */
|
||||
readonly unitM: number
|
||||
/** water oxygen as HydrogenAcceptor (leg: donor → water) */
|
||||
readonly indexMA: Features.FeatureIndex
|
||||
/** water oxygen as HydrogenDonor (leg: water → acceptor) */
|
||||
readonly indexMB: Features.FeatureIndex
|
||||
props: { type: InteractionType.WaterBridge, flag: InteractionFlag }
|
||||
}
|
||||
|
||||
type WaterBridgeContacts = ReadonlyArray<WaterBridgeContact>;
|
||||
|
||||
export const WaterBridgesParams = {
|
||||
backbone: PD.Boolean(true, { description: 'Include backbone hydrogen bonds' }),
|
||||
ignoreHydrogens: PD.Boolean(true, { description: 'Ignore explicit hydrogens in geometric constraints' }),
|
||||
legDistMin: PD.Numeric(2.5, { min: 1, max: 4, step: 0.1 }, { description: 'Minimum leg distance (Å)' }),
|
||||
legDistMax: PD.Numeric(4.1, { min: 1, max: 6, step: 0.1 }, { description: 'Maximum leg distance (Å)' }),
|
||||
donAngleDevMax: PD.Numeric(80, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal donor angle' }),
|
||||
accAngleDevMax: PD.Numeric(50, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal acceptor angle' }),
|
||||
donOutOfPlaneAngleMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }),
|
||||
accOutOfPlaneAngleMax: PD.Numeric(90, { min: 0, max: 180, step: 1 }),
|
||||
omegaMin: PD.Numeric(71, { min: 0, max: 180, step: 1 }, { description: 'Minimum A–W–B angle (°)' }),
|
||||
omegaMax: PD.Numeric(140, { min: 0, max: 180, step: 1 }, { description: 'Maximum A–W–B angle (°)' }),
|
||||
};
|
||||
export type WaterBridgesParams = typeof WaterBridgesParams;
|
||||
export type WaterBridgesProps = PD.Values<WaterBridgesParams>;
|
||||
|
||||
export const WaterBridgesProvider = {
|
||||
requiredFeatures: new Set([FeatureType.HydrogenDonor, FeatureType.HydrogenAcceptor]),
|
||||
params: WaterBridgesParams,
|
||||
find: findWaterBridgeContacts,
|
||||
};
|
||||
|
||||
function isWater(unit: Unit.Atomic, index: StructureElement.UnitIndex): boolean {
|
||||
return unit.model.atomicHierarchy.derived.residue.moleculeType[
|
||||
unit.residueIndex[unit.elements[index]]
|
||||
] === MoleculeType.Water;
|
||||
}
|
||||
|
||||
function isBackboneAtom(unit: Unit.Atomic, index: StructureElement.UnitIndex): boolean {
|
||||
const element = unit.elements[index];
|
||||
const moleculeType = unit.model.atomicHierarchy.derived.residue.moleculeType[unit.residueIndex[element]];
|
||||
if (moleculeType !== MoleculeType.Protein && moleculeType !== MoleculeType.RNA && moleculeType !== MoleculeType.DNA) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const atomId = unit.model.atomicHierarchy.atoms.label_atom_id.value(element);
|
||||
if (moleculeType === MoleculeType.Protein) {
|
||||
return ProteinBackboneAtoms.has(atomId);
|
||||
}
|
||||
|
||||
return NucleicBackboneAtoms.has(atomId);
|
||||
}
|
||||
|
||||
const _lookupCtx = StructureLookup3DResultContext();
|
||||
|
||||
type Candidate = {
|
||||
unit: Unit.Atomic
|
||||
featureIdx: Features.FeatureIndex
|
||||
memberIdx: StructureElement.UnitIndex
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
distSq: number
|
||||
};
|
||||
|
||||
type FeatureKey = number;
|
||||
|
||||
function featureKey(unitId: number, featureIndex: Features.FeatureIndex): FeatureKey {
|
||||
return cantorPairing(unitId, featureIndex);
|
||||
}
|
||||
|
||||
type BestBridge = { contact: WaterBridgeContact; combinedDistSq: number };
|
||||
type BestBridgeMap = Map<FeatureKey, Map<FeatureKey, BestBridge>>;
|
||||
|
||||
function getBestBridge(best: BestBridgeMap, donorKey: FeatureKey, acceptorKey: FeatureKey): BestBridge | undefined {
|
||||
return best.get(donorKey)?.get(acceptorKey);
|
||||
}
|
||||
|
||||
function setBestBridge(best: BestBridgeMap, donorKey: FeatureKey, acceptorKey: FeatureKey, value: BestBridge) {
|
||||
let acceptors = best.get(donorKey);
|
||||
if (acceptors === undefined) {
|
||||
acceptors = new Map();
|
||||
best.set(donorKey, acceptors);
|
||||
}
|
||||
acceptors.set(acceptorKey, value);
|
||||
}
|
||||
|
||||
function bestBridgeValues(best: BestBridgeMap): BestBridge[] {
|
||||
const values: BestBridge[] = [];
|
||||
for (const acceptors of best.values()) {
|
||||
for (const value of acceptors.values()) values.push(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function checkOmega(don: Candidate, posW: Vec3, acc: Candidate, cosOmegaMin: number, cosOmegaMax: number): boolean {
|
||||
const ax = don.x - posW[0];
|
||||
const ay = don.y - posW[1];
|
||||
const az = don.z - posW[2];
|
||||
|
||||
const bx = acc.x - posW[0];
|
||||
const by = acc.y - posW[1];
|
||||
const bz = acc.z - posW[2];
|
||||
|
||||
const aLenSq = ax * ax + ay * ay + az * az;
|
||||
const bLenSq = bx * bx + by * by + bz * bz;
|
||||
|
||||
if (aLenSq === 0 || bLenSq === 0) return false;
|
||||
|
||||
const cosOmega = (ax * bx + ay * by + az * bz) / Math.sqrt(aLenSq * bLenSq);
|
||||
|
||||
// cos decreases monotonically on [0, pi], so:
|
||||
// omega >= omegaMin && omega <= omegaMax
|
||||
// is equivalent to:
|
||||
// cos(omega) <= cos(omegaMin) && cos(omega) >= cos(omegaMax)
|
||||
return cosOmega <= cosOmegaMin && cosOmega >= cosOmegaMax;
|
||||
}
|
||||
|
||||
export function findWaterBridgeContacts(
|
||||
structure: Structure,
|
||||
unitsFeatures: IntMap<Features>,
|
||||
props: WaterBridgesProps
|
||||
): WaterBridgeContacts {
|
||||
const legOpts: GeometryOptions = {
|
||||
ignoreHydrogens: props.ignoreHydrogens,
|
||||
includeBackbone: props.backbone,
|
||||
maxAccAngleDev: degToRad(props.accAngleDevMax),
|
||||
maxDonAngleDev: degToRad(props.donAngleDevMax),
|
||||
maxAccOutOfPlaneAngle: degToRad(props.accOutOfPlaneAngleMax),
|
||||
maxDonOutOfPlaneAngle: degToRad(props.donOutOfPlaneAngleMax),
|
||||
};
|
||||
|
||||
const legDistMinSq = props.legDistMin * props.legDistMin;
|
||||
const legDistMaxSq = props.legDistMax * props.legDistMax;
|
||||
|
||||
const omegaMinRad = degToRad(props.omegaMin);
|
||||
const omegaMaxRad = degToRad(props.omegaMax);
|
||||
|
||||
if (omegaMinRad > omegaMaxRad) return [];
|
||||
|
||||
const cosOmegaMin = Math.cos(omegaMinRad);
|
||||
const cosOmegaMax = Math.cos(omegaMaxRad);
|
||||
|
||||
// Best bridge per unique donor/acceptor feature pair across all water molecules.
|
||||
const best: BestBridgeMap = new Map();
|
||||
|
||||
const wPos = Vec3();
|
||||
const candidatePos = Vec3();
|
||||
|
||||
for (const unitW of structure.units) {
|
||||
if (!Unit.isAtomic(unitW)) continue;
|
||||
|
||||
const featW = unitsFeatures.get(unitW.id);
|
||||
if (!featW || featW.count === 0) continue;
|
||||
|
||||
// Map each water-oxygen local index to its acceptor and donor feature indices.
|
||||
const waterMap = new Map<StructureElement.UnitIndex, {
|
||||
acc: Features.FeatureIndex | undefined,
|
||||
don: Features.FeatureIndex | undefined
|
||||
}>();
|
||||
|
||||
for (let fi = 0 as Features.FeatureIndex; fi < featW.count; fi++) {
|
||||
const mi = featW.members[featW.offsets[fi]] as StructureElement.UnitIndex;
|
||||
if (!isWater(unitW, mi)) continue;
|
||||
|
||||
const t = featW.types[fi];
|
||||
if (t !== FeatureType.HydrogenAcceptor && t !== FeatureType.HydrogenDonor) continue;
|
||||
|
||||
let e = waterMap.get(mi);
|
||||
if (!e) waterMap.set(mi, (e = { acc: undefined, don: undefined }));
|
||||
|
||||
if (t === FeatureType.HydrogenAcceptor) e.acc = fi;
|
||||
else e.don = fi;
|
||||
}
|
||||
|
||||
if (waterMap.size === 0) continue;
|
||||
|
||||
const infoWAcc = Features.Info(structure, unitW, featW);
|
||||
const infoWDon = Features.Info(structure, unitW, featW);
|
||||
|
||||
for (const [waterAtomIdx, { acc: accFW, don: donFW }] of waterMap) {
|
||||
if (accFW === undefined || donFW === undefined) continue;
|
||||
|
||||
unitW.conformation.position(unitW.elements[waterAtomIdx], wPos);
|
||||
|
||||
infoWAcc.feature = accFW;
|
||||
infoWDon.feature = donFW;
|
||||
|
||||
const { count, indices, units: hitUnits } =
|
||||
structure.lookup3d.find(wPos[0], wPos[1], wPos[2], props.legDistMax, _lookupCtx);
|
||||
|
||||
const donors: Candidate[] = [];
|
||||
const acceptors: Candidate[] = [];
|
||||
|
||||
const donorKeys = new Set<FeatureKey>();
|
||||
const acceptorKeys = new Set<FeatureKey>();
|
||||
|
||||
for (let r = 0; r < count; r++) {
|
||||
const hitUnit = hitUnits[r];
|
||||
if (!Unit.isAtomic(hitUnit)) continue;
|
||||
|
||||
const atomicUnit = hitUnit as Unit.Atomic;
|
||||
const hitLocalIdx = indices[r] as StructureElement.UnitIndex;
|
||||
|
||||
// Only skip the water atom itself. Other atoms in the same unit can still be valid.
|
||||
if (atomicUnit === unitW && hitLocalIdx === waterAtomIdx) continue;
|
||||
if (isWater(atomicUnit, hitLocalIdx)) continue;
|
||||
|
||||
const hitFeat = unitsFeatures.get(atomicUnit.id);
|
||||
if (!hitFeat || hitFeat.count === 0) continue;
|
||||
|
||||
const infoHit = Features.Info(structure, atomicUnit, hitFeat);
|
||||
|
||||
const { indices: fIdxs, offsets: fOff } = hitFeat.elementsIndex;
|
||||
for (let k = fOff[hitLocalIdx], kl = fOff[hitLocalIdx + 1]; k < kl; k++) {
|
||||
const fi = fIdxs[k] as Features.FeatureIndex;
|
||||
const fType = hitFeat.types[fi];
|
||||
|
||||
if (fType !== FeatureType.HydrogenDonor && fType !== FeatureType.HydrogenAcceptor) continue;
|
||||
|
||||
const memberIdx = hitFeat.members[hitFeat.offsets[fi]] as StructureElement.UnitIndex;
|
||||
|
||||
if (!props.backbone && isBackboneAtom(atomicUnit, memberIdx)) continue;
|
||||
|
||||
atomicUnit.conformation.position(atomicUnit.elements[memberIdx], candidatePos);
|
||||
|
||||
const distSq = Vec3.squaredDistance(candidatePos, wPos);
|
||||
if (distSq < legDistMinSq || distSq > legDistMaxSq) continue;
|
||||
|
||||
infoHit.feature = fi;
|
||||
|
||||
if (fType === FeatureType.HydrogenDonor) {
|
||||
const key = featureKey(atomicUnit.id, fi);
|
||||
if (donorKeys.has(key)) continue;
|
||||
|
||||
if (checkGeometry(structure, infoHit, infoWAcc, legOpts)) {
|
||||
donorKeys.add(key);
|
||||
donors.push({
|
||||
unit: atomicUnit,
|
||||
featureIdx: fi,
|
||||
memberIdx,
|
||||
x: candidatePos[0],
|
||||
y: candidatePos[1],
|
||||
z: candidatePos[2],
|
||||
distSq,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const key = featureKey(atomicUnit.id, fi);
|
||||
if (acceptorKeys.has(key)) continue;
|
||||
|
||||
if (checkGeometry(structure, infoWDon, infoHit, legOpts)) {
|
||||
acceptorKeys.add(key);
|
||||
acceptors.push({
|
||||
unit: atomicUnit,
|
||||
featureIdx: fi,
|
||||
memberIdx,
|
||||
x: candidatePos[0],
|
||||
y: candidatePos[1],
|
||||
z: candidatePos[2],
|
||||
distSq,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const don of donors) {
|
||||
for (const acc of acceptors) {
|
||||
// Reject bridges where donor and acceptor are the same physical atom
|
||||
// represented by different feature indices.
|
||||
if (don.unit === acc.unit && don.memberIdx === acc.memberIdx) continue;
|
||||
|
||||
if (!checkOmega(don, wPos, acc, cosOmegaMin, cosOmegaMax)) continue;
|
||||
|
||||
const combinedDistSq = don.distSq + acc.distSq;
|
||||
const donorKey = featureKey(don.unit.id, don.featureIdx);
|
||||
const acceptorKey = featureKey(acc.unit.id, acc.featureIdx);
|
||||
|
||||
const existing = getBestBridge(best, donorKey, acceptorKey);
|
||||
if (!existing || combinedDistSq < existing.combinedDistSq) {
|
||||
setBestBridge(best, donorKey, acceptorKey, {
|
||||
contact: {
|
||||
unitA: don.unit.id,
|
||||
indexA: don.featureIdx,
|
||||
unitB: acc.unit.id,
|
||||
indexB: acc.featureIdx,
|
||||
unitM: unitW.id,
|
||||
indexMA: accFW,
|
||||
indexMB: donFW,
|
||||
props: { type: InteractionType.WaterBridge, flag: InteractionFlag.None },
|
||||
},
|
||||
combinedDistSq,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestBridgeValues(best).map(e => e.contact);
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { VisualContext } from '../../../mol-repr/visual';
|
||||
import { Structure, StructureElement, Unit } from '../../../mol-model/structure';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
|
||||
import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../../../mol-repr/structure/complex-visual';
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { PickingId } from '../../../mol-geo/geometry/picking';
|
||||
import { EmptyLoci, Loci } from '../../../mol-model/loci';
|
||||
import { NullLocation } from '../../../mol-model/location';
|
||||
import { Interval, OrderedSet } from '../../../mol-data/int';
|
||||
import { InteractionsProvider } from '../interactions';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { BridgeContacts, Bridges } from '../interactions/interactions';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { InteractionsSharedParams } from './shared';
|
||||
import { Features } from '../interactions/features';
|
||||
|
||||
type CanonicalLegIndices = {
|
||||
endpointA: Int32Array
|
||||
endpointB: Int32Array
|
||||
};
|
||||
|
||||
const CanonicalLegIndicesCache = new WeakMap<BridgeContacts, CanonicalLegIndices>();
|
||||
|
||||
function getCanonicalLegIndices(bridges: BridgeContacts): CanonicalLegIndices {
|
||||
const cached = CanonicalLegIndicesCache.get(bridges);
|
||||
if (cached) return cached;
|
||||
|
||||
const n = bridges.length;
|
||||
const endpointA = new Int32Array(n);
|
||||
const endpointB = new Int32Array(n);
|
||||
|
||||
const legA = new Map<string, number>();
|
||||
const legB = new Map<string, number>();
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const b = bridges[i];
|
||||
|
||||
const kA = `${b.unitA}|${b.indexA}|${b.unitM}|${b.indexMA}`;
|
||||
const kB = `${b.unitM}|${b.indexMB}|${b.unitB}|${b.indexB}`;
|
||||
|
||||
let ai = legA.get(kA);
|
||||
if (ai === undefined) {
|
||||
ai = i;
|
||||
legA.set(kA, i);
|
||||
}
|
||||
endpointA[i] = ai;
|
||||
|
||||
let bi = legB.get(kB);
|
||||
if (bi === undefined) {
|
||||
bi = i;
|
||||
legB.set(kB, i);
|
||||
}
|
||||
endpointB[i] = bi;
|
||||
}
|
||||
|
||||
const indices = { endpointA, endpointB };
|
||||
CanonicalLegIndicesCache.set(bridges, indices);
|
||||
return indices;
|
||||
}
|
||||
|
||||
function getFeatureMember(features: Features, featureIndex: Features.FeatureIndex): StructureElement.UnitIndex {
|
||||
return features.members[features.offsets[featureIndex]] as StructureElement.UnitIndex;
|
||||
}
|
||||
|
||||
function atomPosition(unit: Unit.Atomic, features: Features, featureIndex: Features.FeatureIndex, out: Vec3) {
|
||||
const atomLocalIdx = getFeatureMember(features, featureIndex);
|
||||
unit.conformation.position(unit.elements[atomLocalIdx], out);
|
||||
}
|
||||
|
||||
function setFeatureLocation(
|
||||
structure: Structure,
|
||||
location: StructureElement.Location,
|
||||
unitId: number,
|
||||
features: Features,
|
||||
featureIndex: Features.FeatureIndex
|
||||
) {
|
||||
const unit = structure.unitMap.get(unitId) as Unit.Atomic;
|
||||
const atomLocalIdx = getFeatureMember(features, featureIndex);
|
||||
|
||||
location.unit = unit;
|
||||
location.element = unit.elements[atomLocalIdx];
|
||||
}
|
||||
|
||||
function applyLegA(
|
||||
bridgeIndex: number,
|
||||
bridgeCount: number,
|
||||
canonical: CanonicalLegIndices,
|
||||
apply: (interval: Interval) => boolean
|
||||
) {
|
||||
let changed = false;
|
||||
const i = canonical.endpointA[bridgeIndex];
|
||||
|
||||
if (apply(Interval.ofSingleton(i))) changed = true;
|
||||
if (apply(Interval.ofSingleton(i + bridgeCount))) changed = true;
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function applyLegB(
|
||||
bridgeIndex: number,
|
||||
bridgeCount: number,
|
||||
canonical: CanonicalLegIndices,
|
||||
apply: (interval: Interval) => boolean
|
||||
) {
|
||||
let changed = false;
|
||||
const i = canonical.endpointB[bridgeIndex];
|
||||
|
||||
if (apply(Interval.ofSingleton(i + 2 * bridgeCount))) changed = true;
|
||||
if (apply(Interval.ofSingleton(i + 3 * bridgeCount))) changed = true;
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function createBridgeCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<BridgeParams>, mesh?: Mesh) {
|
||||
if (!structure.hasAtomic) return Mesh.createEmpty(mesh);
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return Mesh.createEmpty(mesh);
|
||||
|
||||
const { bridges, unitsFeatures } = interactions;
|
||||
|
||||
const n = bridges.length;
|
||||
if (!n) return Mesh.createEmpty(mesh);
|
||||
|
||||
const l = StructureElement.Location.create(structure);
|
||||
const { sizeFactor } = props;
|
||||
const canonical = getCanonicalLegIndices(bridges);
|
||||
|
||||
const builderProps = {
|
||||
// Four half-cylinders per bridge; createLinkCylinderMesh draws the A-side half per call:
|
||||
// [0, n): A→mediator, forward (A side)
|
||||
// [n, 2n): A→mediator, backward (mediator side)
|
||||
// [2n, 3n): mediator→B, forward (mediator side)
|
||||
// [3n, 4n): mediator→B, backward (B side)
|
||||
//
|
||||
// When multiple bridges share the same physical leg, only the first
|
||||
// occurrence is drawn; later ones map back to the canonical edge index.
|
||||
linkCount: 4 * n,
|
||||
|
||||
position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
|
||||
const b = bridges[edgeIndex % n];
|
||||
const uM = structure.unitMap.get(b.unitM) as Unit.Atomic;
|
||||
const fM = unitsFeatures.get(b.unitM);
|
||||
const leg = Math.floor(edgeIndex / n);
|
||||
|
||||
if (leg === 0) {
|
||||
const uA = structure.unitMap.get(b.unitA) as Unit.Atomic;
|
||||
const fA = unitsFeatures.get(b.unitA);
|
||||
atomPosition(uA, fA, b.indexA, posA);
|
||||
atomPosition(uM, fM, b.indexMA, posB);
|
||||
} else if (leg === 1) {
|
||||
const uA = structure.unitMap.get(b.unitA) as Unit.Atomic;
|
||||
const fA = unitsFeatures.get(b.unitA);
|
||||
atomPosition(uM, fM, b.indexMA, posA);
|
||||
atomPosition(uA, fA, b.indexA, posB);
|
||||
} else if (leg === 2) {
|
||||
const uB = structure.unitMap.get(b.unitB) as Unit.Atomic;
|
||||
const fB = unitsFeatures.get(b.unitB);
|
||||
atomPosition(uM, fM, b.indexMB, posA);
|
||||
atomPosition(uB, fB, b.indexB, posB);
|
||||
} else {
|
||||
const uB = structure.unitMap.get(b.unitB) as Unit.Atomic;
|
||||
const fB = unitsFeatures.get(b.unitB);
|
||||
atomPosition(uB, fB, b.indexB, posA);
|
||||
atomPosition(uM, fM, b.indexMB, posB);
|
||||
}
|
||||
},
|
||||
|
||||
ignore: (edgeIndex: number) => {
|
||||
const bi = edgeIndex % n;
|
||||
const leg = Math.floor(edgeIndex / n);
|
||||
|
||||
return leg <= 1
|
||||
? canonical.endpointA[bi] !== bi
|
||||
: canonical.endpointB[bi] !== bi;
|
||||
},
|
||||
|
||||
style: (_edgeIndex: number) => LinkStyle.Dashed,
|
||||
|
||||
radius: (edgeIndex: number) => {
|
||||
const b = bridges[edgeIndex % n];
|
||||
const leg = Math.floor(edgeIndex / n);
|
||||
const isLegA = leg <= 1;
|
||||
|
||||
if (isLegA) {
|
||||
const fA = unitsFeatures.get(b.unitA);
|
||||
const fM = unitsFeatures.get(b.unitM);
|
||||
|
||||
setFeatureLocation(structure, l, b.unitA, fA, b.indexA);
|
||||
const sizeA = theme.size.size(l);
|
||||
|
||||
setFeatureLocation(structure, l, b.unitM, fM, b.indexMA);
|
||||
const sizeM = theme.size.size(l);
|
||||
|
||||
return Math.min(sizeA, sizeM) * sizeFactor;
|
||||
} else {
|
||||
const fM = unitsFeatures.get(b.unitM);
|
||||
const fB = unitsFeatures.get(b.unitB);
|
||||
|
||||
setFeatureLocation(structure, l, b.unitM, fM, b.indexMB);
|
||||
const sizeM = theme.size.size(l);
|
||||
|
||||
setFeatureLocation(structure, l, b.unitB, fB, b.indexB);
|
||||
const sizeB = theme.size.size(l);
|
||||
|
||||
return Math.min(sizeM, sizeB) * sizeFactor;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
|
||||
|
||||
if (boundingSphere) {
|
||||
m.setBoundingSphere(boundingSphere);
|
||||
} else if (m.triangleCount > 0) {
|
||||
const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, sizeFactor);
|
||||
m.setBoundingSphere(sphere);
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
export const BridgeParams = {
|
||||
...ComplexMeshParams,
|
||||
...LinkCylinderParams,
|
||||
...InteractionsSharedParams,
|
||||
};
|
||||
export type BridgeParams = typeof BridgeParams
|
||||
|
||||
export function BridgeVisual(materialId: number): ComplexVisual<BridgeParams> {
|
||||
return ComplexMeshVisual<BridgeParams>({
|
||||
defaultProps: PD.getDefaultValues(BridgeParams),
|
||||
createGeometry: createBridgeCylinderMesh,
|
||||
createLocationIterator: createBridgeIterator,
|
||||
getLoci: getBridgeLoci,
|
||||
eachLocation: eachBridgeInteraction,
|
||||
|
||||
setUpdateState: (
|
||||
state: VisualUpdateState,
|
||||
newProps: PD.Values<BridgeParams>,
|
||||
currentProps: PD.Values<BridgeParams>,
|
||||
newTheme: Theme,
|
||||
currentTheme: Theme,
|
||||
newStructure: Structure,
|
||||
_currentStructure: Structure
|
||||
) => {
|
||||
state.createGeometry = (
|
||||
newProps.sizeFactor !== currentProps.sizeFactor ||
|
||||
newProps.dashCount !== currentProps.dashCount ||
|
||||
newProps.dashScale !== currentProps.dashScale ||
|
||||
newProps.dashCap !== currentProps.dashCap ||
|
||||
newProps.radialSegments !== currentProps.radialSegments ||
|
||||
newTheme.size !== currentTheme.size
|
||||
);
|
||||
|
||||
const interactionsHash = InteractionsProvider.get(newStructure).version;
|
||||
if ((state.info.interactionsHash as number) !== interactionsHash) {
|
||||
state.createGeometry = true;
|
||||
state.updateTransform = true;
|
||||
state.updateColor = true;
|
||||
state.info.interactionsHash = interactionsHash;
|
||||
}
|
||||
}
|
||||
}, materialId);
|
||||
}
|
||||
|
||||
function getBridgeLoci(pickingId: PickingId, structure: Structure, id: number) {
|
||||
const { objectId, groupId } = pickingId;
|
||||
if (id !== objectId) return EmptyLoci;
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return EmptyLoci;
|
||||
|
||||
const { bridges, unitsFeatures } = interactions;
|
||||
const n = bridges.length;
|
||||
|
||||
if (!n || groupId < 0 || groupId >= 4 * n) return EmptyLoci;
|
||||
|
||||
const bridgeIndex = groupId % n;
|
||||
|
||||
return Bridges.Loci({ structure, bridges, unitsFeatures }, [{ bridgeIndex }]);
|
||||
}
|
||||
|
||||
const __unitMap = new Map<number, OrderedSet<StructureElement.UnitIndex>>();
|
||||
|
||||
function eachBridgeInteraction(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean, _isMarking: boolean) {
|
||||
let changed = false;
|
||||
|
||||
if (Bridges.isLoci(loci)) {
|
||||
if (!Structure.areEquivalent(loci.data.structure, structure)) return false;
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return false;
|
||||
|
||||
const { bridges } = interactions;
|
||||
const n = bridges.length;
|
||||
if (!n) return false;
|
||||
|
||||
const canonical = getCanonicalLegIndices(bridges);
|
||||
|
||||
for (const e of loci.elements) {
|
||||
if (e.bridgeIndex < 0 || e.bridgeIndex >= n) continue;
|
||||
|
||||
if (applyLegA(e.bridgeIndex, n, canonical, apply)) changed = true;
|
||||
if (applyLegB(e.bridgeIndex, n, canonical, apply)) changed = true;
|
||||
}
|
||||
} else if (StructureElement.Loci.is(loci)) {
|
||||
if (!Structure.areEquivalent(loci.structure, structure)) return false;
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return false;
|
||||
|
||||
const { bridges, unitsFeatures } = interactions;
|
||||
const n = bridges.length;
|
||||
if (!n) return false;
|
||||
|
||||
const canonical = getCanonicalLegIndices(bridges);
|
||||
|
||||
__unitMap.clear();
|
||||
for (const e of loci.elements) {
|
||||
__unitMap.set(e.unit.id, e.indices);
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const b = bridges[i];
|
||||
|
||||
const indicesA = __unitMap.get(b.unitA);
|
||||
const indicesM = __unitMap.get(b.unitM);
|
||||
const indicesB = __unitMap.get(b.unitB);
|
||||
|
||||
if (!indicesA && !indicesM && !indicesB) continue;
|
||||
|
||||
let hitA = false;
|
||||
if (indicesA) {
|
||||
const fA = unitsFeatures.get(b.unitA);
|
||||
const mi = getFeatureMember(fA, b.indexA);
|
||||
hitA = OrderedSet.has(indicesA, mi);
|
||||
}
|
||||
|
||||
let hitM = false;
|
||||
if (indicesM) {
|
||||
const fM = unitsFeatures.get(b.unitM);
|
||||
const miA = getFeatureMember(fM, b.indexMA);
|
||||
const miB = getFeatureMember(fM, b.indexMB);
|
||||
hitM = OrderedSet.has(indicesM, miA) || OrderedSet.has(indicesM, miB);
|
||||
}
|
||||
|
||||
let hitB = false;
|
||||
if (indicesB) {
|
||||
const fB = unitsFeatures.get(b.unitB);
|
||||
const mi = getFeatureMember(fB, b.indexB);
|
||||
hitB = OrderedSet.has(indicesB, mi);
|
||||
}
|
||||
|
||||
if (hitA || hitM) {
|
||||
if (applyLegA(i, n, canonical, apply)) changed = true;
|
||||
}
|
||||
|
||||
if (hitB || hitM) {
|
||||
if (applyLegB(i, n, canonical, apply)) changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
__unitMap.clear();
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function createBridgeIterator(structure: Structure): LocationIterator {
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return LocationIterator(0, 1, 1, () => NullLocation, true);
|
||||
|
||||
const { bridges, unitsFeatures } = interactions;
|
||||
|
||||
const n = bridges.length;
|
||||
const groupCount = 4 * n;
|
||||
const instanceCount = 1;
|
||||
|
||||
const data: Bridges.Data = { structure, bridges, unitsFeatures };
|
||||
const location = Bridges.Location(data);
|
||||
const { element } = location;
|
||||
|
||||
const getLocation = (groupIndex: number) => {
|
||||
element.bridgeIndex = n === 0 ? 0 : groupIndex % n;
|
||||
return location;
|
||||
};
|
||||
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation, true);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -12,20 +12,23 @@ import { UnitsRepresentation, StructureRepresentation, StructureRepresentationSt
|
||||
import { InteractionsIntraUnitParams, InteractionsIntraUnitVisual } from './interactions-intra-unit-cylinder';
|
||||
import { InteractionsProvider } from '../interactions';
|
||||
import { InteractionsInterUnitParams, InteractionsInterUnitVisual } from './interactions-inter-unit-cylinder';
|
||||
import { BridgeParams, BridgeVisual } from './interactions-bridge-cylinder';
|
||||
import { CustomProperty } from '../../common/custom-property';
|
||||
import { getUnitKindsParam } from '../../../mol-repr/structure/params';
|
||||
|
||||
const InteractionsVisuals = {
|
||||
'intra-unit': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InteractionsIntraUnitParams>) => UnitsRepresentation('Intra-unit interactions cylinder', ctx, getParams, InteractionsIntraUnitVisual),
|
||||
'inter-unit': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InteractionsInterUnitParams>) => ComplexRepresentation('Inter-unit interactions cylinder', ctx, getParams, InteractionsInterUnitVisual),
|
||||
'bridge': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, BridgeParams>) => ComplexRepresentation('Bridge cylinder', ctx, getParams, BridgeVisual),
|
||||
};
|
||||
|
||||
export const InteractionsParams = {
|
||||
...InteractionsIntraUnitParams,
|
||||
...InteractionsInterUnitParams,
|
||||
...BridgeParams,
|
||||
unitKinds: getUnitKindsParam(['atomic']),
|
||||
sizeFactor: PD.Numeric(0.2, { min: 0.01, max: 1, step: 0.01 }),
|
||||
visuals: PD.MultiSelect(['intra-unit', 'inter-unit'], PD.objectToOptions(InteractionsVisuals)),
|
||||
visuals: PD.MultiSelect(['intra-unit', 'inter-unit', 'bridge'], PD.objectToOptions(InteractionsVisuals)),
|
||||
};
|
||||
export type InteractionsParams = typeof InteractionsParams
|
||||
export function getInteractionParams(ctx: ThemeRegistryContext, structure: Structure) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*/
|
||||
|
||||
import { Location } from '../../../mol-model/location';
|
||||
@@ -12,7 +13,7 @@ import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { InteractionType } from '../interactions/common';
|
||||
import { TableLegend } from '../../../mol-util/legend';
|
||||
import { Interactions } from '../interactions/interactions';
|
||||
import { Interactions, Bridges } from '../interactions/interactions';
|
||||
import { CustomProperty } from '../../common/custom-property';
|
||||
import { hash2 } from '../../../mol-data/util';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
@@ -29,6 +30,7 @@ const InteractionTypeColors = ColorMap({
|
||||
CationPi: 0xFF8000,
|
||||
PiStacking: 0x8CB366,
|
||||
WeakHydrogenBond: 0xC5DDEC,
|
||||
WaterBridge: 0x00CCEE,
|
||||
});
|
||||
|
||||
const InteractionTypeColorTable: [string, Color][] = [
|
||||
@@ -40,6 +42,7 @@ const InteractionTypeColorTable: [string, Color][] = [
|
||||
['Cation Pi', InteractionTypeColors.CationPi],
|
||||
['Pi Stacking', InteractionTypeColors.PiStacking],
|
||||
['Weak HydrogenBond', InteractionTypeColors.WeakHydrogenBond],
|
||||
['Water Bridge', InteractionTypeColors.WaterBridge],
|
||||
];
|
||||
|
||||
function typeColor(type: InteractionType): Color {
|
||||
@@ -60,6 +63,8 @@ function typeColor(type: InteractionType): Color {
|
||||
return InteractionTypeColors.PiStacking;
|
||||
case InteractionType.WeakHydrogenBond:
|
||||
return InteractionTypeColors.WeakHydrogenBond;
|
||||
case InteractionType.WaterBridge:
|
||||
return InteractionTypeColors.WaterBridge;
|
||||
case InteractionType.Unknown:
|
||||
return DefaultColor;
|
||||
}
|
||||
@@ -91,6 +96,9 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value
|
||||
return typeColor(contacts.edges[idx].props.type);
|
||||
}
|
||||
}
|
||||
if (Bridges.isLocation(location)) {
|
||||
return typeColor(location.data.bridges[location.element.bridgeIndex].props.type);
|
||||
}
|
||||
return DefaultColor;
|
||||
};
|
||||
} else {
|
||||
|
||||
@@ -167,9 +167,9 @@ namespace Loci {
|
||||
} else if (loci.kind === 'data-loci') {
|
||||
return loci.getBoundingSphere?.(boundingSphere);
|
||||
} else if (loci.kind === 'volume-loci') {
|
||||
return Volume.getBoundingSphere(loci.volume, boundingSphere);
|
||||
return Volume.getBoundingSphere(loci.volume, loci.instances, boundingSphere);
|
||||
} else if (loci.kind === 'isosurface-loci') {
|
||||
return Volume.Isosurface.getBoundingSphere(loci.volume, loci.isoValue, boundingSphere);
|
||||
return Volume.Isosurface.getBoundingSphere(loci.volume, loci.isoValue, loci.instances, boundingSphere);
|
||||
} else if (loci.kind === 'cell-loci') {
|
||||
return Volume.Cell.getBoundingSphere(loci.volume, loci.elements, boundingSphere);
|
||||
} else if (loci.kind === 'segment-loci') {
|
||||
|
||||
@@ -545,6 +545,12 @@ export function surroundingLigands({ query, radius, includeWater }: SurroundingL
|
||||
continue;
|
||||
}
|
||||
|
||||
// Water is handled exclusively by the `includeWater` 3D-lookup branch below.
|
||||
// A single water pulled in via a struct_conn metalc/covale edge would
|
||||
// otherwise match every other water in the chain (all share label_seq_id
|
||||
// and label_comp_id) and leak the entire chain.
|
||||
if (StructureProperties.entity.type(l) === 'water') continue;
|
||||
|
||||
residuesIt.setSegment(chainSegment);
|
||||
while (residuesIt.hasNext) {
|
||||
const residueSegment = residuesIt.move();
|
||||
|
||||
@@ -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>
|
||||
@@ -95,10 +95,22 @@ namespace UnitRing {
|
||||
Elements.SN, Elements.SB,
|
||||
Elements.BI
|
||||
] as ElementSymbol[]);
|
||||
/**
|
||||
* Elements that are sp3 (and therefore non-aromatic) when degree >= 4 with no pi bonds.
|
||||
* Excludes O (never realistically reaches degree 4) and N (quaternary N can be aromatic,
|
||||
* but is guarded by the hasPiBond check below).
|
||||
*/
|
||||
const Sp3RingCheckElements = new Set([
|
||||
Elements.B, Elements.C, Elements.N,
|
||||
Elements.SI, Elements.P, Elements.S,
|
||||
Elements.GE, Elements.AS,
|
||||
Elements.SN, Elements.SB,
|
||||
Elements.BI
|
||||
] as ElementSymbol[]);
|
||||
const AromaticRingPlanarityThreshold = 0.05;
|
||||
|
||||
export function isAromatic(unit: Unit.Atomic, ring: UnitRing): boolean {
|
||||
const { elements, bonds: { b, offset, edgeProps: { flags } } } = unit;
|
||||
const { elements, bonds: { b, offset, edgeProps: { flags, order } } } = unit;
|
||||
const { type_symbol, label_comp_id } = unit.model.atomicHierarchy.atoms;
|
||||
|
||||
// ignore Proline (can be flat because of bad geometry)
|
||||
@@ -120,6 +132,25 @@ namespace UnitRing {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0, il = ring.length; i < il; ++i) {
|
||||
const aI = ring[i];
|
||||
const elem = type_symbol.value(elements[aI]);
|
||||
if (!Sp3RingCheckElements.has(elem)) continue;
|
||||
|
||||
let degree = 0;
|
||||
let hasPiBond = false;
|
||||
for (let j = offset[aI], jl = offset[aI + 1]; j < jl; ++j) {
|
||||
degree += 1;
|
||||
const f = flags[j];
|
||||
const o = order[j];
|
||||
if (BondType.is(BondType.Flag.Aromatic, f) || o === 2 || o === 3) {
|
||||
hasPiBond = true;
|
||||
}
|
||||
}
|
||||
if (degree >= 4 && !hasPiBond) return false;
|
||||
}
|
||||
|
||||
if (aromaticBondCount === 2 * ring.length) return true;
|
||||
if (!hasAromaticRingElement) return false;
|
||||
if (ring.length < 5) return false;
|
||||
|
||||
@@ -68,6 +68,36 @@ namespace Grid {
|
||||
return Sphere3D.fromDimensionsAndTransform(boundingSphere, dimensions, transform);
|
||||
}
|
||||
|
||||
const _isoBbox = Box3D();
|
||||
export function getIsosurfaceBoundingSphere(grid: Grid, isoValue: number, boundingSphere?: Sphere3D) {
|
||||
const neg = isoValue < 0;
|
||||
|
||||
const c = [0, 0, 0];
|
||||
const getCoords = grid.cells.space.getCoords;
|
||||
const d = grid.cells.data;
|
||||
const [xn, yn, zn] = grid.cells.space.dimensions;
|
||||
|
||||
let minx = xn - 1, miny = yn - 1, minz = zn - 1;
|
||||
let maxx = 0, maxy = 0, maxz = 0;
|
||||
for (let i = 0, il = d.length; i < il; ++i) {
|
||||
if ((neg && d[i] <= isoValue) || (!neg && d[i] >= isoValue)) {
|
||||
getCoords(i, c);
|
||||
if (c[0] < minx) minx = c[0];
|
||||
if (c[1] < miny) miny = c[1];
|
||||
if (c[2] < minz) minz = c[2];
|
||||
if (c[0] > maxx) maxx = c[0];
|
||||
if (c[1] > maxy) maxy = c[1];
|
||||
if (c[2] > maxz) maxz = c[2];
|
||||
}
|
||||
}
|
||||
|
||||
Vec3.set(_isoBbox.min, minx - 1, miny - 1, minz - 1);
|
||||
Vec3.set(_isoBbox.max, maxx + 1, maxy + 1, maxz + 1);
|
||||
const transform = Grid.getGridToCartesianTransform(grid);
|
||||
Box3D.transform(_isoBbox, _isoBbox, transform);
|
||||
return Sphere3D.fromBox3D(boundingSphere || Sphere3D(), _isoBbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute histogram with given bin count.
|
||||
* Cached on the Grid object.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { Grid } from './grid';
|
||||
import { OrderedSet } from '../../mol-data/int';
|
||||
import { Interval, OrderedSet } from '../../mol-data/int';
|
||||
import { Box3D, Sphere3D } from '../../mol-math/geometry';
|
||||
import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
|
||||
@@ -191,14 +191,14 @@ export namespace Volume {
|
||||
export function isLociEmpty(loci: Loci) { return isEmpty(loci.volume) || OrderedSet.isEmpty(loci.instances); }
|
||||
|
||||
const boundaryHelper = new BoundaryHelper('98');
|
||||
export function getBoundingSphere(volume: Volume, boundingSphere?: Sphere3D) {
|
||||
export function getBoundingSphere(volume: Volume, instances: OrderedSet<InstanceIndex>, boundingSphere?: Sphere3D) {
|
||||
const gs = Grid.getBoundingSphere(volume.grid);
|
||||
if (!boundingSphere) boundingSphere = Sphere3D();
|
||||
if (volume.instances.length === 0) return Sphere3D.copy(boundingSphere, gs);
|
||||
if (OrderedSet.isEmpty(instances)) return Sphere3D.copy(boundingSphere, gs);
|
||||
|
||||
const spheres: Sphere3D[] = [];
|
||||
for (let i = 0, il = volume.instances.length; i < il; ++i) {
|
||||
const { transform } = volume.instances[i];
|
||||
for (let i = 0, il = OrderedSet.size(instances); i < il; ++i) {
|
||||
const { transform } = volume.instances[OrderedSet.getAt(instances, i)];
|
||||
spheres.push(Sphere3D.transform(Sphere3D(), gs, transform));
|
||||
}
|
||||
|
||||
@@ -220,35 +220,23 @@ export namespace Volume {
|
||||
export function areLociEqual(a: Loci, b: Loci) { return a.volume === b.volume && Volume.IsoValue.areSame(a.isoValue, b.isoValue, a.volume.grid.stats) && OrderedSet.areEqual(a.instances, b.instances); }
|
||||
export function isLociEmpty(loci: Loci) { return isEmpty(loci.volume) || OrderedSet.isEmpty(loci.instances); }
|
||||
|
||||
const bbox = Box3D();
|
||||
export function getBoundingSphere(volume: Volume, isoValue: Volume.IsoValue, boundingSphere?: Sphere3D) {
|
||||
const boundaryHelper = new BoundaryHelper('98');
|
||||
export function getBoundingSphere(volume: Volume, isoValue: Volume.IsoValue, instances: OrderedSet<InstanceIndex>, boundingSphere?: Sphere3D) {
|
||||
const value = Volume.IsoValue.toAbsolute(isoValue, volume.grid.stats).absoluteValue;
|
||||
const neg = value < 0;
|
||||
const gs = Grid.getIsosurfaceBoundingSphere(volume.grid, value);
|
||||
|
||||
const c = [0, 0, 0];
|
||||
const getCoords = volume.grid.cells.space.getCoords;
|
||||
const d = volume.grid.cells.data;
|
||||
const [xn, yn, zn] = volume.grid.cells.space.dimensions;
|
||||
if (OrderedSet.isEmpty(instances)) return Sphere3D.copy(boundingSphere || Sphere3D(), gs);
|
||||
|
||||
let minx = xn - 1, miny = yn - 1, minz = zn - 1;
|
||||
let maxx = 0, maxy = 0, maxz = 0;
|
||||
for (let i = 0, il = d.length; i < il; ++i) {
|
||||
if ((neg && d[i] <= value) || (!neg && d[i] >= value)) {
|
||||
getCoords(i, c);
|
||||
if (c[0] < minx) minx = c[0];
|
||||
if (c[1] < miny) miny = c[1];
|
||||
if (c[2] < minz) minz = c[2];
|
||||
if (c[0] > maxx) maxx = c[0];
|
||||
if (c[1] > maxy) maxy = c[1];
|
||||
if (c[2] > maxz) maxz = c[2];
|
||||
}
|
||||
const spheres: Sphere3D[] = [];
|
||||
for (let i = 0, il = OrderedSet.size(instances); i < il; ++i) {
|
||||
spheres.push(Sphere3D.transform(Sphere3D(), gs, volume.instances[OrderedSet.getAt(instances, i)].transform));
|
||||
}
|
||||
|
||||
Vec3.set(bbox.min, minx - 1, miny - 1, minz - 1);
|
||||
Vec3.set(bbox.max, maxx + 1, maxy + 1, maxz + 1);
|
||||
const transform = Grid.getGridToCartesianTransform(volume.grid);
|
||||
Box3D.transform(bbox, bbox, transform);
|
||||
return Sphere3D.fromBox3D(boundingSphere || Sphere3D(), bbox);
|
||||
boundaryHelper.reset();
|
||||
for (const s of spheres) boundaryHelper.includeSphere(s);
|
||||
boundaryHelper.finishedIncludeStep();
|
||||
for (const s of spheres) boundaryHelper.radiusSphere(s);
|
||||
return boundaryHelper.getSphere(boundingSphere);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +404,7 @@ export namespace Volume {
|
||||
}
|
||||
return Sphere3D.fromBox3D(boundingSphere || Sphere3D(), bbox);
|
||||
} else {
|
||||
return Volume.getBoundingSphere(volume, boundingSphere);
|
||||
return Volume.getBoundingSphere(volume, Interval.ofLength(volume.instances.length as InstanceIndex), boundingSphere);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -12,7 +12,7 @@ import { degToRad } from '../../../mol-math/misc';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { PluginStateAnimation } from '../model';
|
||||
|
||||
const _dir = Vec3(), _axis = Vec3(), _rot = Quat();
|
||||
const _dir = Vec3(), _axis = Vec3(), _rot = Quat(), _up = Vec3(), _side = Vec3();
|
||||
|
||||
type State = { snapshot: Camera.Snapshot };
|
||||
|
||||
@@ -24,6 +24,7 @@ export const AnimateCameraRock = PluginStateAnimation.create({
|
||||
durationInMs: PD.Numeric(4000, { min: 100, max: 20000, step: 100 }),
|
||||
speed: PD.Numeric(1, { min: 1, max: 10, step: 1 }, { description: 'How many times to rock from side to side.' }),
|
||||
angle: PD.Numeric(10, { min: 0, max: 180, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
|
||||
axis: PD.Vec3(Vec3.create(0, -1, 0), {}, { description: 'Axis of rotation in camera space' }),
|
||||
}),
|
||||
initialState: (p, ctx) => ({ snapshot: ctx.canvas3d!.camera.getSnapshot() }) as State,
|
||||
getDuration: p => ({ kind: 'fixed', durationMs: p.durationInMs }),
|
||||
@@ -47,11 +48,25 @@ export const AnimateCameraRock = PluginStateAnimation.create({
|
||||
const angle = Math.sin(phase * ctx.params.speed * Math.PI * 2) * degToRad(ctx.params.angle);
|
||||
|
||||
Vec3.sub(_dir, snapshot.position, snapshot.target);
|
||||
Vec3.normalize(_axis, snapshot.up);
|
||||
|
||||
// Transform axis from camera space to world space
|
||||
Vec3.normalize(_axis, _dir); // Z = view direction
|
||||
Vec3.normalize(_up, snapshot.up); // Y = up
|
||||
Vec3.cross(_side, _up, _axis); // X = right
|
||||
Vec3.normalize(_side, _side);
|
||||
const a = ctx.params.axis ?? Vec3.create(0, -1, 0); // default for backwards compatibility
|
||||
Vec3.set(_axis,
|
||||
a[0] * _side[0] + a[1] * _up[0] + a[2] * _axis[0],
|
||||
a[0] * _side[1] + a[1] * _up[1] + a[2] * _axis[1],
|
||||
a[0] * _side[2] + a[1] * _up[2] + a[2] * _axis[2]
|
||||
);
|
||||
Vec3.normalize(_axis, _axis);
|
||||
|
||||
Quat.setAxisAngle(_rot, _axis, angle);
|
||||
Vec3.transformQuat(_dir, _dir, _rot);
|
||||
Vec3.transformQuat(_up, snapshot.up, _rot);
|
||||
const position = Vec3.add(Vec3(), snapshot.target, _dir);
|
||||
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position }, durationMs: 0 });
|
||||
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position, up: _up }, durationMs: 0 });
|
||||
|
||||
if (phase >= 0.99999) {
|
||||
return { kind: 'finished' };
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-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>
|
||||
*/
|
||||
|
||||
import { Camera } from '../../../mol-canvas3d/camera';
|
||||
@@ -11,7 +12,7 @@ import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { PluginStateAnimation } from '../model';
|
||||
|
||||
const _dir = Vec3(), _axis = Vec3(), _rot = Quat();
|
||||
const _dir = Vec3(), _axis = Vec3(), _rot = Quat(), _up = Vec3(), _side = Vec3();
|
||||
|
||||
type State = { snapshot: Camera.Snapshot };
|
||||
|
||||
@@ -22,7 +23,7 @@ export const AnimateCameraSpin = PluginStateAnimation.create({
|
||||
params: () => ({
|
||||
durationInMs: PD.Numeric(4000, { min: 100, max: 20000, step: 100 }),
|
||||
speed: PD.Numeric(1, { min: 1, max: 10, step: 1 }, { description: 'How many times to spin in the specified duration.' }),
|
||||
direction: PD.Select<'cw' | 'ccw'>('cw', [['cw', 'Clockwise'], ['ccw', 'Counter Clockwise']], { cycle: true })
|
||||
axis: PD.Vec3(Vec3.create(0, -1, 0), {}, { description: 'Axis of rotation in camera space' }),
|
||||
}),
|
||||
initialState: (_, ctx) => ({ snapshot: ctx.canvas3d?.camera.getSnapshot()! }) as State,
|
||||
getDuration: p => ({ kind: 'fixed', durationMs: p.durationInMs }),
|
||||
@@ -42,14 +43,28 @@ export const AnimateCameraSpin = PluginStateAnimation.create({
|
||||
const phase = t.animation
|
||||
? t.animation?.currentFrame / (t.animation.frameCount + 1)
|
||||
: clamp(t.current / ctx.params.durationInMs, 0, 1);
|
||||
const angle = 2 * Math.PI * phase * ctx.params.speed * (ctx.params.direction === 'ccw' ? -1 : 1);
|
||||
const angle = 2 * Math.PI * phase * ctx.params.speed;
|
||||
|
||||
Vec3.sub(_dir, snapshot.position, snapshot.target);
|
||||
Vec3.normalize(_axis, snapshot.up);
|
||||
|
||||
// Transform axis from camera space to world space
|
||||
Vec3.normalize(_axis, _dir); // Z = view direction
|
||||
Vec3.normalize(_up, snapshot.up); // Y = up
|
||||
Vec3.cross(_side, _up, _axis); // X = right
|
||||
Vec3.normalize(_side, _side);
|
||||
const a = ctx.params.axis ?? Vec3.create(0, -1, 0); // default for backwards compatibility
|
||||
Vec3.set(_axis,
|
||||
a[0] * _side[0] + a[1] * _up[0] + a[2] * _axis[0],
|
||||
a[0] * _side[1] + a[1] * _up[1] + a[2] * _axis[1],
|
||||
a[0] * _side[2] + a[1] * _up[2] + a[2] * _axis[2]
|
||||
);
|
||||
Vec3.normalize(_axis, _axis);
|
||||
|
||||
Quat.setAxisAngle(_rot, _axis, angle);
|
||||
Vec3.transformQuat(_dir, _dir, _rot);
|
||||
Vec3.transformQuat(_up, snapshot.up, _rot);
|
||||
const position = Vec3.add(Vec3(), snapshot.target, _dir);
|
||||
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position }, durationMs: 0 });
|
||||
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position, up: _up }, durationMs: 0 });
|
||||
|
||||
if (phase >= 0.99999) {
|
||||
return { kind: 'finished' };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-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>
|
||||
@@ -26,6 +26,7 @@ import { StructConn } from '../../../mol-model-formats/structure/property/bonds/
|
||||
import { StructureRepresentationRegistry } from '../../../mol-repr/structure/registry';
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
|
||||
|
||||
export interface StructureRepresentationPresetProvider<P = any, S extends _Result = _Result> extends PresetProvider<PluginStateObject.Molecule.Structure, P, S> { }
|
||||
export function StructureRepresentationPresetProvider<P, S extends _Result>(repr: StructureRepresentationPresetProvider<P, S>) { return repr; }
|
||||
@@ -495,6 +496,61 @@ const autoLod = StructureRepresentationPresetProvider({
|
||||
}
|
||||
});
|
||||
|
||||
type MesoscaleGraphicsMode = keyof typeof Spheres.LodLevelsPresets
|
||||
const MesoscaleGraphicsOptions = PD.arrayToOptions(Object.keys(Spheres.LodLevelsPresets) as MesoscaleGraphicsMode[]);
|
||||
function getMesoscaleLodLevels(mode: MesoscaleGraphicsMode) {
|
||||
return Spheres.LodLevelsPresets[mode];
|
||||
}
|
||||
|
||||
const mesoscale = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure-representation-mesoscale',
|
||||
display: {
|
||||
name: 'Mesoscale', group: 'Miscellaneous',
|
||||
description: 'Show everything in spacefill representation with instance-granularity and level-of-detail tuned for large particle scenes.'
|
||||
},
|
||||
params: () => ({
|
||||
...CommonParams,
|
||||
graphics: PD.Select<MesoscaleGraphicsMode>('quality', MesoscaleGraphicsOptions),
|
||||
}),
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
if (!structureCell) return {};
|
||||
|
||||
const components = {
|
||||
all: await presetStaticComponent(plugin, structureCell, 'all'),
|
||||
};
|
||||
|
||||
const structure = structureCell.obj!.data;
|
||||
|
||||
const { update, builder, typeParams, color } = reprBuilder(plugin, params, structure);
|
||||
|
||||
const graphics: MesoscaleGraphicsMode = params.graphics ?? 'quality';
|
||||
const lodLevels = getMesoscaleLodLevels(graphics);
|
||||
const approximate = graphics !== 'quality' && graphics !== 'ultra';
|
||||
const alphaThickness = graphics === 'performance' ? 15 : 12;
|
||||
|
||||
const representations = {
|
||||
all: builder.buildRepresentation(update, components.all, {
|
||||
type: 'spacefill',
|
||||
typeParams: {
|
||||
...typeParams,
|
||||
instanceGranularity: true,
|
||||
lodLevels,
|
||||
approximate,
|
||||
alphaThickness,
|
||||
clipPrimitive: true,
|
||||
},
|
||||
color: color || 'entity-id',
|
||||
}, { tag: 'all' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
await updateFocusRepr(plugin, structure, params.theme?.focus?.name ?? color, params.theme?.focus?.params);
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
|
||||
export function presetStaticComponent(plugin: PluginContext, structure: StateObjectRef<PluginStateObject.Molecule.Structure>, type: StaticStructureComponentType, params?: { label?: string, tags?: string[] }) {
|
||||
return plugin.builders.structure.tryCreateComponentStatic(structure, type, params);
|
||||
}
|
||||
@@ -514,5 +570,6 @@ export const PresetStructureRepresentations = {
|
||||
illustrative,
|
||||
'molecular-surface': molecularSurface,
|
||||
'auto-lod': autoLod,
|
||||
mesoscale,
|
||||
};
|
||||
export type PresetStructureRepresentations = typeof PresetStructureRepresentations;
|
||||
@@ -1304,7 +1304,7 @@ const ShapeFromPly = PluginStateTransform.BuiltIn({
|
||||
to: SO.Shape.Provider,
|
||||
params(a) {
|
||||
return {
|
||||
transforms: PD.Optional(PD.Value<Mat4[]>([], { isHidden: true })),
|
||||
transforms: PD.Optional(PD.Value([Mat4.identity()], { isHidden: true })),
|
||||
label: PD.Optional(PD.Text('', { isHidden: true }))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1414,8 +1414,8 @@ class ObjectListItem extends React.PureComponent<ObjectListItemProps, { isExpand
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectList>, { isExpanded: boolean }> {
|
||||
state = { isExpanded: false };
|
||||
export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectList>, { isExpanded: boolean, showPresets: boolean }> {
|
||||
state = { isExpanded: false, showPresets: false };
|
||||
|
||||
change(value: any) {
|
||||
this.props.onChange({ name: this.props.name, param: this.props.param, value });
|
||||
@@ -1459,12 +1459,29 @@ export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectL
|
||||
e.currentTarget.blur();
|
||||
};
|
||||
|
||||
toggleShowPresets = () => this.setState({ showPresets: !this.state.showPresets });
|
||||
|
||||
presetItems = memoizeLatest((param: PD.ObjectList) => ActionMenu.createItemsFromSelectOptions(param.presets ?? []));
|
||||
|
||||
onSelectPreset: ActionMenu.OnSelect = item => {
|
||||
this.setState({ showPresets: false });
|
||||
this.change(item?.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const v = this.props.value;
|
||||
const label = this.props.param.label || camelCaseToWords(this.props.name);
|
||||
const value = `${v.length} item${v.length !== 1 ? 's' : ''}`;
|
||||
const hasPresets = !!this.props.param.presets;
|
||||
const control = hasPresets
|
||||
? <div className='msp-flex-row'>
|
||||
<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>
|
||||
<IconButton svg={BookmarksOutlinedSvg} onClick={this.toggleShowPresets} toggleState={this.state.showPresets} title='Presets' disabled={this.props.isDisabled} />
|
||||
</div>
|
||||
: <button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>;
|
||||
return <>
|
||||
<ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
|
||||
<ControlRow label={label} control={control} />
|
||||
{hasPresets && this.state.showPresets && <ActionMenu items={this.presetItems(this.props.param)} onSelect={this.onSelectPreset} />}
|
||||
{this.state.isExpanded && <div className='msp-control-offset'>
|
||||
{this.props.value.map((v, i) => <ObjectListItem key={i} param={this.props.param} value={v} index={i} actions={this.actions} isDisabled={this.props.isDisabled} />)}
|
||||
<ControlGroup header='New Item'>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { Geometry, GeometryUtils } from '../../mol-geo/geometry/geometry';
|
||||
import { resolveInstanceGranularity } from '../../mol-geo/geometry/base';
|
||||
import { Representation } from '../representation';
|
||||
import { Shape, ShapeGroup } from '../../mol-model/shape';
|
||||
import { Subject } from 'rxjs';
|
||||
@@ -129,7 +130,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
|
||||
// console.log('update transform')
|
||||
locationIt = Shape.groupIterator(_shape);
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (props.instanceGranularity) {
|
||||
if (resolveInstanceGranularity(newProps.instanceGranularity, groupCount, instanceCount)) {
|
||||
createMarkers(instanceCount, 'instance', _renderObject.values);
|
||||
} else {
|
||||
createMarkers(instanceCount * groupCount, 'groupInstance', _renderObject.values);
|
||||
@@ -197,14 +198,15 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
|
||||
}
|
||||
|
||||
function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
|
||||
const instanceGranularity = resolveInstanceGranularity(currentProps.instanceGranularity, _shape.groupCount, _shape.transforms.length);
|
||||
if (isEveryLoci(loci) || (Shape.isLoci(loci) && loci.shape === _shape)) {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return apply(Interval.ofBounds(0, _shape.transforms.length));
|
||||
} else {
|
||||
return apply(Interval.ofBounds(0, _shape.groupCount * _shape.transforms.length));
|
||||
}
|
||||
} else {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return eachInstance(loci, _shape, apply);
|
||||
} else {
|
||||
return eachShapeGroup(loci, _shape, apply);
|
||||
@@ -226,7 +228,8 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
|
||||
getLoci(pickingId: PickingId) {
|
||||
const { objectId, groupId, instanceId } = pickingId;
|
||||
if (_renderObject && _renderObject.id === objectId) {
|
||||
if (groupId === PickingId.Null) {
|
||||
const instanceGranularity = resolveInstanceGranularity(currentProps.instanceGranularity, _shape.groupCount, _shape.transforms.length);
|
||||
if (groupId === PickingId.Null || instanceGranularity) {
|
||||
return Shape.Loci(_shape);
|
||||
} else {
|
||||
return ShapeGroup.Loci(_shape, [{ ids: OrderedSet.ofSingleton(groupId), instance: instanceId }]);
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Text } from '../../mol-geo/geometry/text/text';
|
||||
import { SizeTheme } from '../../mol-theme/size';
|
||||
import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
|
||||
import { createMarkers } from '../../mol-geo/geometry/marker-data';
|
||||
import { resolveInstanceGranularity } from '../../mol-geo/geometry/base';
|
||||
import { StructureParams, StructureMeshParams, StructureTextParams, StructureDirectVolumeParams, StructureLinesParams, StructureCylindersParams, StructureTextureMeshParams, StructureSpheresParams, StructurePointsParams, StructureImageParams } from './params';
|
||||
import { Clipping } from '../../mol-theme/clipping';
|
||||
import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
|
||||
@@ -173,7 +174,7 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
|
||||
if (updateState.updateTransform) {
|
||||
// console.log('update transform')
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (newProps.instanceGranularity) {
|
||||
if (resolveInstanceGranularity(newProps.instanceGranularity, groupCount, instanceCount)) {
|
||||
createMarkers(instanceCount, 'instance', renderObject.values);
|
||||
} else {
|
||||
createMarkers(instanceCount * groupCount, 'groupInstance', renderObject.values);
|
||||
@@ -237,14 +238,15 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
|
||||
}
|
||||
|
||||
function lociApply(loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) {
|
||||
const instanceGranularity = resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount);
|
||||
if (lociIsSuperset(loci)) {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return apply(Interval.ofBounds(0, locationIt.instanceCount));
|
||||
} else {
|
||||
return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
|
||||
}
|
||||
} else {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return eachInstance(loci, currentStructure, apply);
|
||||
} else {
|
||||
return eachLocation(loci, currentStructure, apply, isMarking);
|
||||
@@ -279,7 +281,11 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
|
||||
finalize(ctx);
|
||||
},
|
||||
getLoci(pickingId: PickingId) {
|
||||
return renderObject ? getLoci(pickingId, currentStructure, renderObject.id) : EmptyLoci;
|
||||
if (!renderObject) return EmptyLoci;
|
||||
if (resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount)) {
|
||||
pickingId = { ...pickingId, groupId: PickingId.Null };
|
||||
}
|
||||
return getLoci(pickingId, currentStructure, renderObject.id);
|
||||
},
|
||||
eachLocation(cb: LocationCallback) {
|
||||
locationIt.reset();
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Interval } from '../../mol-data/int';
|
||||
import { LocationCallback, VisualUpdateState } from '../util';
|
||||
import { ColorTheme } from '../../mol-theme/color';
|
||||
import { createMarkers } from '../../mol-geo/geometry/marker-data';
|
||||
import { resolveInstanceGranularity } from '../../mol-geo/geometry/base';
|
||||
import { MarkerAction } from '../../mol-util/marker-action';
|
||||
import { ValueCell, deepEqual } from '../../mol-util';
|
||||
import { createSizes } from '../../mol-geo/geometry/size-data';
|
||||
@@ -214,7 +215,7 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
|
||||
if (updateState.updateTransform) {
|
||||
// console.log('update transform');
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (newProps.instanceGranularity) {
|
||||
if (resolveInstanceGranularity(newProps.instanceGranularity, groupCount, instanceCount)) {
|
||||
createMarkers(instanceCount, 'instance', renderObject.values);
|
||||
} else {
|
||||
createMarkers(instanceCount * groupCount, 'groupInstance', renderObject.values);
|
||||
@@ -313,14 +314,15 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
|
||||
}
|
||||
|
||||
function lociApply(loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) {
|
||||
const instanceGranularity = resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount);
|
||||
if (lociIsSuperset(loci)) {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return apply(Interval.ofBounds(0, locationIt.instanceCount));
|
||||
} else {
|
||||
return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
|
||||
}
|
||||
} else {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return eachInstance(loci, currentStructureGroup, apply);
|
||||
} else {
|
||||
return eachLocation(loci, currentStructureGroup, apply, isMarking);
|
||||
@@ -355,7 +357,11 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
|
||||
finalize(ctx);
|
||||
},
|
||||
getLoci(pickingId: PickingId) {
|
||||
return renderObject ? getLoci(pickingId, currentStructureGroup, renderObject.id) : EmptyLoci;
|
||||
if (!renderObject) return EmptyLoci;
|
||||
if (resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount)) {
|
||||
pickingId = { ...pickingId, groupId: PickingId.Null };
|
||||
}
|
||||
return getLoci(pickingId, currentStructureGroup, renderObject.id);
|
||||
},
|
||||
eachLocation(cb: LocationCallback) {
|
||||
locationIt.reset();
|
||||
|
||||
59
src/mol-repr/volume/_spec/dot.spec.ts
Normal file
59
src/mol-repr/volume/_spec/dot.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Ludovic Autin <autin@scripps.edu>
|
||||
*/
|
||||
|
||||
import { CustomProperties } from '../../../mol-model/custom-property';
|
||||
import { Grid, Volume } from '../../../mol-model/volume';
|
||||
import { Mat4, Tensor } from '../../../mol-math/linear-algebra';
|
||||
import { createVolumeSphereImpostor } from '../dot';
|
||||
|
||||
function createTestVolume(dimensions: [number, number, number], data: number[]): Volume {
|
||||
return {
|
||||
grid: {
|
||||
transform: { kind: 'matrix', matrix: Mat4.identity() },
|
||||
cells: Tensor.create(Tensor.Space(dimensions, [2, 1, 0]), Tensor.Data1(data)),
|
||||
stats: { min: 0, max: 1, mean: 0.5, sigma: 0.5 },
|
||||
} satisfies Grid,
|
||||
instances: [{ transform: Mat4.identity() }],
|
||||
sourceData: { kind: 'test', name: 'test', data: {} } as any,
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
_localPropertyData: Object.create(null),
|
||||
};
|
||||
}
|
||||
|
||||
describe('volume dot representation', () => {
|
||||
it('adds sphere impostor dots in Morton order for LOD sampling', () => {
|
||||
const volume = createTestVolume([2, 2, 2], [
|
||||
1, 1,
|
||||
1, 1,
|
||||
1, 1,
|
||||
1, 1,
|
||||
]);
|
||||
const spheres = createVolumeSphereImpostor(undefined as any, volume, 0, undefined as any, {
|
||||
isoValue: Volume.IsoValue.absolute(0.5),
|
||||
perturbPositions: false,
|
||||
lodLevels: [{ minDistance: 0, maxDistance: 0, overlap: 0, stride: 0, scaleBias: 3 }],
|
||||
} as any);
|
||||
|
||||
expect(Array.from(spheres.groupBuffer.ref.value)).toEqual([0, 4, 2, 6, 1, 5, 3, 7]);
|
||||
});
|
||||
|
||||
it('adds sphere impostor dots in row-major order when no LOD levels are configured', () => {
|
||||
const volume = createTestVolume([2, 2, 2], [
|
||||
1, 1,
|
||||
1, 1,
|
||||
1, 1,
|
||||
1, 1,
|
||||
]);
|
||||
const spheres = createVolumeSphereImpostor(undefined as any, volume, 0, undefined as any, {
|
||||
isoValue: Volume.IsoValue.absolute(0.5),
|
||||
perturbPositions: false,
|
||||
lodLevels: [],
|
||||
} as any);
|
||||
|
||||
expect(Array.from(spheres.groupBuffer.ref.value)).toEqual([0, 1, 2, 3, 4, 5, 6, 7]);
|
||||
});
|
||||
});
|
||||
@@ -67,7 +67,8 @@ export function VolumeSphereImpostorVisual(materialId: number): VolumeVisual<Vol
|
||||
setUpdateState: (state: VisualUpdateState, newVolume: Volume, currentVolume: Volume, newProps: PD.Values<VolumeSphereParams>, currentProps: PD.Values<VolumeSphereParams>, newTheme: Theme, currentTheme: Theme) => {
|
||||
state.createGeometry = (
|
||||
!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, newVolume.grid.stats) ||
|
||||
newProps.perturbPositions !== currentProps.perturbPositions
|
||||
newProps.perturbPositions !== currentProps.perturbPositions ||
|
||||
newProps.lodLevels.length > 0 && currentProps.lodLevels.length === 0
|
||||
);
|
||||
},
|
||||
geometryUtils: Spheres.Utils,
|
||||
@@ -128,38 +129,71 @@ export function createVolumeSphereImpostor(ctx: VisualContext, volume: Volume, k
|
||||
|
||||
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 count = Math.ceil((xn * yn * zn) / 10);
|
||||
const builder = SpheresBuilder.create(count, Math.ceil(count / 2), spheres);
|
||||
|
||||
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);
|
||||
const add = (x: number, y: number, z: number) => {
|
||||
const value = space.get(data, x, y, z);
|
||||
if (!invert && value < isoVal || invert && value > isoVal) return;
|
||||
|
||||
const cellIdx = space.dataOffset(x, y, z);
|
||||
Vec3.set(p, x, y, z);
|
||||
Vec3.transformMat4(p, p, gridToCartn);
|
||||
if (basis) {
|
||||
Vec3.add(p, p, getRandomOffsetFromBasis(basis));
|
||||
}
|
||||
builder.add(p[0], p[1], p[2], cellIdx);
|
||||
};
|
||||
|
||||
// Morton ordering keeps stride-based LOD sampling spatially balanced.
|
||||
// Only worthwhile when LOD levels are configured; otherwise use the
|
||||
// direct row-major path to avoid the extra allocations and sort.
|
||||
const useMortonOrder = props.lodLevels.length > 0;
|
||||
|
||||
if (useMortonOrder) {
|
||||
// Recursive octree traversal over the bounding power-of-two cube,
|
||||
// visiting children in Morton order (octant bit2=x, bit1=y, bit0=z).
|
||||
// Octants whose origin already exceeds the grid extent are pruned,
|
||||
// so out-of-range subtrees of non-cube grids cost ~O(log) per skip.
|
||||
let size = 1;
|
||||
while (size < xn || size < yn || size < zn) size <<= 1;
|
||||
|
||||
const visit = (x0: number, y0: number, z0: number, s: number): void => {
|
||||
if (x0 >= xn || y0 >= yn || z0 >= zn) return;
|
||||
|
||||
if (s === 1) {
|
||||
add(x0, y0, z0);
|
||||
return;
|
||||
}
|
||||
const h = s >> 1;
|
||||
visit(x0, y0, z0, h);
|
||||
visit(x0, y0, z0 + h, h);
|
||||
visit(x0, y0 + h, z0, h);
|
||||
visit(x0, y0 + h, z0 + h, h);
|
||||
visit(x0 + h, y0, z0, h);
|
||||
visit(x0 + h, y0, z0 + h, h);
|
||||
visit(x0 + h, y0 + h, z0, h);
|
||||
visit(x0 + h, y0 + h, z0 + h, h);
|
||||
};
|
||||
|
||||
visit(0, 0, 0, size);
|
||||
} else {
|
||||
for (let z = 0; z < zn; ++z) {
|
||||
for (let y = 0; y < yn; ++y) {
|
||||
for (let x = 0; x < xn; ++x) {
|
||||
add(x, y, z);
|
||||
}
|
||||
builder.add(p[0], p[1], p[2], cellIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const s = builder.getSpheres();
|
||||
s.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
|
||||
s.setBoundingSphere(Grid.getIsosurfaceBoundingSphere(volume.grid, Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue));
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -209,7 +243,7 @@ export function createVolumeSphereMesh(ctx: VisualContext, volume: Volume, key:
|
||||
}
|
||||
|
||||
const m = MeshBuilder.getMesh(builderState);
|
||||
m.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
|
||||
m.setBoundingSphere(Grid.getIsosurfaceBoundingSphere(volume.grid, Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue));
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -277,7 +311,7 @@ export function createVolumePoint(ctx: VisualContext, volume: Volume, key: numbe
|
||||
}
|
||||
|
||||
const pt = builder.getPoints();
|
||||
pt.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
|
||||
pt.setBoundingSphere(Grid.getIsosurfaceBoundingSphere(volume.grid, Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue));
|
||||
return pt;
|
||||
}
|
||||
|
||||
@@ -320,6 +354,7 @@ const DotVisuals = {
|
||||
export const DotParams = {
|
||||
...VolumeSphereParams,
|
||||
...VolumePointParams,
|
||||
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
|
||||
visuals: PD.MultiSelect(['sphere'], PD.objectToOptions(DotVisuals)),
|
||||
bumpFrequency: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
};
|
||||
@@ -346,4 +381,4 @@ export const DotRepresentationProvider = VolumeRepresentationProvider({
|
||||
defaultSizeTheme: { name: 'uniform' },
|
||||
locationKinds: ['cell-location', 'position-location'],
|
||||
isApplicable: (volume: Volume) => !Volume.isEmpty(volume) && !Volume.Segmentation.get(volume)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol
|
||||
ValueCell.updateIfChanged(surface.varyingGroup, true);
|
||||
}
|
||||
|
||||
surface.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
|
||||
surface.setBoundingSphere(Grid.getIsosurfaceBoundingSphere(volume.grid, Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue));
|
||||
|
||||
return surface;
|
||||
}
|
||||
@@ -318,7 +318,7 @@ export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume
|
||||
const transform = Grid.getGridToCartesianTransform(volume.grid);
|
||||
Lines.transform(wireframe, transform);
|
||||
|
||||
wireframe.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
|
||||
wireframe.setBoundingSphere(Grid.getIsosurfaceBoundingSphere(volume.grid, Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue));
|
||||
|
||||
return wireframe;
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ function getSampledImage(volume: Volume, theme: Theme, info: SamplingInfo, isoVa
|
||||
const isoLevel = clamp(normalize(Volume.IsoValue.toAbsolute(isoValue, stats).absoluteValue, min, max), 0, 1);
|
||||
|
||||
const im = Image.create(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image);
|
||||
im.setBoundingSphere(Volume.isPeriodic(volume) ? Volume.getBoundingSphere(volume) : Grid.getBoundingSphere(volume.grid));
|
||||
im.setBoundingSphere(Volume.isPeriodic(volume) ? Volume.getBoundingSphere(volume, Interval.ofLength(volume.instances.length as Volume.InstanceIndex)) : Grid.getBoundingSphere(volume.grid));
|
||||
im.meta.mapping = mapping;
|
||||
|
||||
return im;
|
||||
@@ -480,7 +480,7 @@ async function createGridImage(ctx: VisualContext, volume: Volume, key: number,
|
||||
const isoLevel = clamp(normalize(Volume.IsoValue.toAbsolute(isoValue, stats).absoluteValue, min, max), 0, 1);
|
||||
|
||||
const im = Image.create(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image);
|
||||
im.setBoundingSphere(Volume.isPeriodic(volume) ? Volume.getBoundingSphere(volume) : Grid.getBoundingSphere(volume.grid));
|
||||
im.setBoundingSphere(Volume.isPeriodic(volume) ? Volume.getBoundingSphere(volume, Interval.ofLength(volume.instances.length as Volume.InstanceIndex)) : Grid.getBoundingSphere(volume.grid));
|
||||
im.meta.mapping = mapping;
|
||||
|
||||
return im;
|
||||
|
||||
@@ -33,7 +33,7 @@ import { Emissive } from '../../mol-theme/emissive';
|
||||
import { Wiggle } from '../../mol-theme/wiggle';
|
||||
import { SizeTheme } from '../../mol-theme/size';
|
||||
import { Sphere3D } from '../../mol-math/geometry/primitives/sphere3d';
|
||||
import { BaseGeometry } from '../../mol-geo/geometry/base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../../mol-geo/geometry/base';
|
||||
|
||||
export const VolumeParams = {
|
||||
...BaseGeometry.Params,
|
||||
@@ -182,7 +182,7 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
||||
if (updateState.updateTransform || updateState.updateLocation) {
|
||||
// console.log('update transform');
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (newProps.instanceGranularity) {
|
||||
if (resolveInstanceGranularity(newProps.instanceGranularity, groupCount, instanceCount)) {
|
||||
createMarkers(instanceCount, 'instance', renderObject.values);
|
||||
} else {
|
||||
createMarkers(instanceCount * groupCount, 'groupInstance', renderObject.values);
|
||||
@@ -279,14 +279,15 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
||||
}
|
||||
|
||||
function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
|
||||
const instanceGranularity = resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount);
|
||||
if (isEveryLoci(loci)) {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return apply(Interval.ofBounds(0, locationIt.instanceCount));
|
||||
} else {
|
||||
return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
|
||||
}
|
||||
} else {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return eachInstance(loci, currentVolume, currentKey, apply);
|
||||
} else {
|
||||
return eachLocation(loci, currentVolume, currentKey, currentProps, apply, geometry);
|
||||
@@ -308,7 +309,11 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
||||
}
|
||||
},
|
||||
getLoci(pickingId: PickingId) {
|
||||
return renderObject ? getLoci(pickingId, currentVolume, currentKey, currentProps, renderObject.id, geometry) : EmptyLoci;
|
||||
if (!renderObject) return EmptyLoci;
|
||||
if (resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount)) {
|
||||
pickingId = { ...pickingId, groupId: PickingId.Null };
|
||||
}
|
||||
return getLoci(pickingId, currentVolume, currentKey, currentProps, renderObject.id, geometry);
|
||||
},
|
||||
eachLocation(cb: LocationCallback) {
|
||||
locationIt.reset();
|
||||
|
||||
126
src/mol-state/_spec/state.spec.ts
Normal file
126
src/mol-state/_spec/state.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*/
|
||||
|
||||
import { State, StateObject, StateTransformer } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
|
||||
interface TypeInfo { name: string; typeClass: 'Root' | 'Data' }
|
||||
const Create = StateObject.factory<TypeInfo>();
|
||||
|
||||
class Root extends Create({ name: 'Root', typeClass: 'Root' }) { }
|
||||
class Leaf extends Create<{ value: number }>({ name: 'Leaf', typeClass: 'Data' }) { }
|
||||
|
||||
const NS = 'state-dispose-spec';
|
||||
let counter = 0;
|
||||
|
||||
function leafTransformer(spy: () => void) {
|
||||
return StateTransformer.create<Root, Leaf, { value: number }>(NS, {
|
||||
name: `create-leaf-${counter++}`,
|
||||
from: [Root],
|
||||
to: [Leaf],
|
||||
display: { name: 'Create Leaf' },
|
||||
params: () => ({} as any),
|
||||
apply({ params }) { return new Leaf({ value: params.value }); },
|
||||
dispose() { spy(); }
|
||||
});
|
||||
}
|
||||
|
||||
function chainedTransformer(spy: () => void) {
|
||||
return StateTransformer.create<Leaf, Leaf, {}>(NS, {
|
||||
name: `chained-leaf-${counter++}`,
|
||||
from: [Leaf],
|
||||
to: [Leaf],
|
||||
display: { name: 'Chained Leaf' },
|
||||
apply({ a }) { return new Leaf({ value: a.data.value + 1 }); },
|
||||
dispose() { spy(); }
|
||||
});
|
||||
}
|
||||
|
||||
function newState() {
|
||||
return State.create(new Root({}), { runTask: <T>(t: Task<T>) => t.run() });
|
||||
}
|
||||
|
||||
describe('State.dispose', () => {
|
||||
it('calls transformer.dispose for every live cell', async () => {
|
||||
const leafSpy = jest.fn();
|
||||
const chainSpy = jest.fn();
|
||||
const A = leafTransformer(leafSpy);
|
||||
const B = chainedTransformer(chainSpy);
|
||||
|
||||
const state = newState();
|
||||
const builder = state.build();
|
||||
builder.toRoot<Root>().apply(A as any, { value: 1 }).apply(B as any, {});
|
||||
await state.runTask(state.updateTree(builder));
|
||||
|
||||
// root + 2 transformer outputs.
|
||||
expect(state.cells.size).toBe(3);
|
||||
|
||||
state.dispose();
|
||||
|
||||
expect(leafSpy).toHaveBeenCalledTimes(1);
|
||||
expect(chainSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('disposes all sibling subtrees', async () => {
|
||||
const spyA = jest.fn();
|
||||
const spyB = jest.fn();
|
||||
const A = leafTransformer(spyA);
|
||||
const B = leafTransformer(spyB);
|
||||
|
||||
const state = newState();
|
||||
const builder = state.build();
|
||||
builder.toRoot<Root>().apply(A as any, { value: 1 });
|
||||
builder.toRoot<Root>().apply(B as any, { value: 2 });
|
||||
await state.runTask(state.updateTree(builder));
|
||||
|
||||
state.dispose();
|
||||
|
||||
expect(spyA).toHaveBeenCalledTimes(1);
|
||||
expect(spyB).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not throw when a transformer dispose throws', async () => {
|
||||
const goodSpy = jest.fn();
|
||||
const Throwing = StateTransformer.create<Root, Leaf, { value: number }>(NS, {
|
||||
name: `throwing-leaf-${counter++}`,
|
||||
from: [Root],
|
||||
to: [Leaf],
|
||||
display: { name: 'Throwing Leaf' },
|
||||
apply({ params }) { return new Leaf({ value: params.value }); },
|
||||
dispose() { throw new Error('boom'); }
|
||||
});
|
||||
const Good = leafTransformer(goodSpy);
|
||||
|
||||
const state = newState();
|
||||
const builder = state.build();
|
||||
builder.toRoot<Root>().apply(Throwing as any, { value: 1 });
|
||||
builder.toRoot<Root>().apply(Good as any, { value: 2 });
|
||||
await state.runTask(state.updateTree(builder));
|
||||
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
expect(() => state.dispose()).not.toThrow();
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
expect(goodSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('is a no-op for transformers without a dispose definition', async () => {
|
||||
const NoDispose = StateTransformer.create<Root, Leaf, { value: number }>(NS, {
|
||||
name: `no-dispose-${counter++}`,
|
||||
from: [Root],
|
||||
to: [Leaf],
|
||||
display: { name: 'No-dispose Leaf' },
|
||||
apply({ params }) { return new Leaf({ value: params.value }); }
|
||||
});
|
||||
|
||||
const state = newState();
|
||||
const builder = state.build();
|
||||
builder.toRoot<Root>().apply(NoDispose as any, { value: 1 });
|
||||
await state.runTask(state.updateTree(builder));
|
||||
|
||||
expect(() => state.dispose()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -159,6 +159,23 @@ class State {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Dispose every still-live cell so transformer dispose callbacks
|
||||
// (e.g. WebGL/GL buffer cleanup) actually run. Without this,
|
||||
// calling dispose() on a State that still has cells leaks any
|
||||
// resources held by transformer dispose callbacks because they
|
||||
// would only fire on per-cell deletion (see updateNode/findDeletes).
|
||||
const refs: StateTransform.Ref[] = [];
|
||||
StateTree.doPostOrder(this._tree, this._tree.root, { refs }, (n, _, s) => { s.refs.push(n.ref); });
|
||||
for (let i = refs.length - 1; i >= 0; i--) {
|
||||
const cell = (this.cells as Map<StateTransform.Ref, StateObjectCell>).get(refs[i]);
|
||||
if (!cell) continue;
|
||||
try {
|
||||
dispose(cell.transform, cell.obj, cell.transform.params, cell.cache, this.globalContext);
|
||||
} catch (e) {
|
||||
console.warn('Error in transformer dispose during State.dispose', e);
|
||||
}
|
||||
}
|
||||
|
||||
this.ev.dispose();
|
||||
this.actions.dispose();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
export { PixelData };
|
||||
@@ -37,12 +38,14 @@ namespace PixelData {
|
||||
/** to undo pre-multiplied alpha */
|
||||
export function divideByAlpha(pixelData: PixelData): PixelData {
|
||||
const { array } = pixelData;
|
||||
const factor = (array instanceof Uint8Array) ? 255 : 1;
|
||||
// clamp: emissive, bloom and antialiasing can lift premul RGB above alpha; without it Uint8Array silently wraps.
|
||||
const max = (array instanceof Uint8Array) ? 255 : 1;
|
||||
for (let i = 0, il = array.length; i < il; i += 4) {
|
||||
const a = array[i + 3] / factor;
|
||||
array[i] /= a;
|
||||
array[i + 1] /= a;
|
||||
array[i + 2] /= a;
|
||||
const a = array[i + 3] / max;
|
||||
if (a === 0) continue;
|
||||
array[i] = Math.min(max, array[i] / a);
|
||||
array[i + 1] = Math.min(max, array[i + 1] / a);
|
||||
array[i + 2] = Math.min(max, array[i + 2] / a);
|
||||
}
|
||||
return pixelData;
|
||||
}
|
||||
|
||||
@@ -295,10 +295,13 @@ export namespace ParamDefinition {
|
||||
type: 'object-list',
|
||||
element: Params,
|
||||
ctor(): T,
|
||||
getLabel(t: T): string
|
||||
getLabel(t: T): string,
|
||||
presets?: Select<T[]>['options']
|
||||
}
|
||||
export function ObjectList<T>(element: For<T>, getLabel: (e: T) => string, info?: Info & { defaultValue?: T[], ctor?: () => T }): ObjectList<Normalize<T>> {
|
||||
return setInfo<ObjectList<Normalize<T>>>({ type: 'object-list', element: element as any as Params, getLabel, ctor: _defaultObjectListCtor, defaultValue: (info?.defaultValue) || [] }, info);
|
||||
export function ObjectList<T>(element: For<T>, getLabel: (e: T) => string, info?: Info & { defaultValue?: T[], ctor?: () => T, presets?: Select<T[]>['options'] }): ObjectList<Normalize<T>> {
|
||||
const ret = setInfo<ObjectList<Normalize<T>>>({ type: 'object-list', element: element as any as Params, getLabel, ctor: _defaultObjectListCtor, defaultValue: (info?.defaultValue) || [] }, info);
|
||||
if (info?.presets) ret.presets = info.presets as any;
|
||||
return ret;
|
||||
}
|
||||
function _defaultObjectListCtor(this: ObjectList) { return getDefaultValues(this.element) as any; }
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# 0.9.13
|
||||
* /surroundingLigands: honor `omit_water=true|false` for REST GET requests (boolean parser previously coerced both to `false`)
|
||||
* /surroundingLigands: stop leaking the asymmetric unit's water chain into the result when `omit_water=true` (water residues pulled in via struct_conn covale/metalc edges no longer match every other water in the chain)
|
||||
|
||||
# 0.9.12
|
||||
* add `health-check` endpoint + `healthCheckPath` config prop to report service health
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ function _normalizeQueryParams(params: { [p: string]: string }, paramList: Query
|
||||
case QueryParamType.String: el = value; break;
|
||||
case QueryParamType.Integer: el = parseInt(value); break;
|
||||
case QueryParamType.Float: el = parseFloat(value); break;
|
||||
case QueryParamType.Boolean: el = Boolean(+value); break;
|
||||
case QueryParamType.Boolean: el = isTrue(value); break;
|
||||
}
|
||||
|
||||
if (p.validation) p.validation(el);
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export const VERSION = '0.9.12';
|
||||
export const VERSION = '0.9.13';
|
||||
Reference in New Issue
Block a user