mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 21:34:23 +08:00
Compare commits
24 Commits
optimize-c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93a3eba66d | ||
|
|
41b8584fb7 | ||
|
|
523b17dfde | ||
|
|
c47b4d6078 | ||
|
|
b94073b96f | ||
|
|
905eb3ec2f | ||
|
|
3ae72e5c60 | ||
|
|
2601d2ba63 | ||
|
|
340806d774 | ||
|
|
18ad848de2 | ||
|
|
b7c380fd90 | ||
|
|
bcd304d058 | ||
|
|
fd50a8f8e0 | ||
|
|
f806ac1444 | ||
|
|
2c2bd6adda | ||
|
|
b010298acb | ||
|
|
7033a1e0b2 | ||
|
|
8ad617acdf | ||
|
|
31ab6aa93e | ||
|
|
0a2dbe14d7 | ||
|
|
89d305aaa1 | ||
|
|
dbb6b90fbc | ||
|
|
c57150f09f | ||
|
|
0b30c7344b |
@@ -16,11 +16,10 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- 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
|
||||
- Camera improvements
|
||||
- Add the option to approximate "least obstructed direction" when focusing camera, accessibe via `PluginContext.managers.camera.focusLoci` with `optimizeDirection` option
|
||||
- Add `CameraFocusOptions.zoomOut` option that zooms out to to make the entire scene visible before focusing on the target
|
||||
- Add easing support in camera transtion
|
||||
- Non-covalent interactions: water bridge support
|
||||
|
||||
## [v5.9.0] - 2026-05-03
|
||||
- Fix edge case when `PluginSpec.animations` is empty
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 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>
|
||||
@@ -51,7 +51,7 @@ export { consoleStats, isDebugMode, isProductionMode, isTimingMode, setDebugMode
|
||||
import { decodeColor } from '../../mol-util/color/utils';
|
||||
import '../../mol-util/polyfill';
|
||||
import { ViewerAutoPreset } from './presets';
|
||||
import { CameraFocusLociOptions } from '../../mol-plugin-state/manager/camera';
|
||||
import { CameraFocusOptions } from '../../mol-plugin-state/manager/camera';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { NoPrimaryFocusLociBindings } from '../../mol-plugin/behavior/dynamic/camera';
|
||||
|
||||
@@ -535,33 +535,26 @@ export class Viewer {
|
||||
* If neither `expression` nor `elements` are provided, all selections/highlights
|
||||
* will be cleared based on the specified `action`.
|
||||
*/
|
||||
structureInteractivity({ expression, elements, action: action_, applyGranularity = false, filterStructure, focusOptions }: {
|
||||
structureInteractivity({ expression, elements, action, applyGranularity = false, filterStructure, focusOptions }: {
|
||||
expression?: (queryBuilder: typeof MolScriptBuilder) => Expression,
|
||||
elements?: StructureElement.Schema,
|
||||
action: 'highlight' | 'select' | 'focus' | ('highlight' | 'select' | 'focus')[],
|
||||
action: 'highlight' | 'select' | 'focus',
|
||||
applyGranularity?: boolean,
|
||||
filterStructure?: (structure: Structure) => boolean,
|
||||
focusOptions?: Partial<CameraFocusLociOptions>
|
||||
focusOptions?: Partial<CameraFocusOptions>
|
||||
}) {
|
||||
const plugin = this.plugin;
|
||||
const actions = Array.isArray(action_) ? action_ : [action_];
|
||||
|
||||
if (!expression && !elements) {
|
||||
if (actions.includes('select')) {
|
||||
if (action === 'select') {
|
||||
plugin.managers.interactivity.lociSelects.deselectAll();
|
||||
}
|
||||
if (actions.includes('highlight')) {
|
||||
} else if (action === 'highlight') {
|
||||
plugin.managers.interactivity.lociHighlights.clearHighlights();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (actions.includes('select')) {
|
||||
plugin.managers.interactivity.lociSelects.deselectAll();
|
||||
}
|
||||
|
||||
const structures = this.plugin.state.data.selectQ(Q => Q.rootsOfType(PluginStateObject.Molecule.Structure));
|
||||
let focused = false;
|
||||
for (const s of structures) {
|
||||
if (!s.obj?.data) continue;
|
||||
|
||||
@@ -571,16 +564,13 @@ export class Viewer {
|
||||
? StructureElement.Loci.fromExpression(s.obj.data, expression)
|
||||
: StructureElement.Loci.fromSchema(s.obj.data, elements!);
|
||||
|
||||
for (const action of actions) {
|
||||
if (action === 'select') {
|
||||
plugin.managers.interactivity.lociSelects.select({ loci }, applyGranularity);
|
||||
} else if (action === 'highlight') {
|
||||
plugin.managers.interactivity.lociHighlights.highlight({ loci }, applyGranularity);
|
||||
} else if (action === 'focus' && !StructureElement.Loci.isEmpty(loci) && !focused) {
|
||||
plugin.managers.camera.focusLoci(loci, focusOptions);
|
||||
focused = true;
|
||||
if (actions.length === 1) return; // if only focusing, focus the first matching structure and return immediately
|
||||
}
|
||||
if (action === 'select') {
|
||||
plugin.managers.interactivity.lociSelects.select({ loci }, applyGranularity);
|
||||
} else if (action === 'highlight') {
|
||||
plugin.managers.interactivity.lociHighlights.highlight({ loci }, applyGranularity);
|
||||
} else if (action === 'focus' && !StructureElement.Loci.isEmpty(loci)) {
|
||||
plugin.managers.camera.focusLoci(loci, focusOptions);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { EasingFunctions } from '../../../mol-math/easing';
|
||||
import * as EasingFns from '../../../mol-math/easing';
|
||||
import { clamp, lerp } from '../../../mol-math/interpolate';
|
||||
import { EPSILON, Mat3, Mat4, Quat, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
@@ -65,7 +65,27 @@ export async function generateStateTransition(ctx: RuntimeContext, snapshot: Sna
|
||||
return { tree, frametimeMs: dt, frames };
|
||||
}
|
||||
|
||||
const EasingFnMap: Record<MVSAnimationEasing, (t: number) => number> = EasingFunctions;
|
||||
const EasingFnMap: Record<MVSAnimationEasing, (t: number) => number> = {
|
||||
'linear': t => t,
|
||||
'bounce-in': EasingFns.bounceIn,
|
||||
'bounce-out': EasingFns.bounceOut,
|
||||
'bounce-in-out': EasingFns.bounceInOut,
|
||||
'circle-in': EasingFns.circleIn,
|
||||
'circle-out': EasingFns.circleOut,
|
||||
'circle-in-out': EasingFns.circleInOut,
|
||||
'cubic-in': EasingFns.cubicIn,
|
||||
'cubic-out': EasingFns.cubicOut,
|
||||
'cubic-in-out': EasingFns.cubicInOut,
|
||||
'exp-in': EasingFns.expIn,
|
||||
'exp-out': EasingFns.expOut,
|
||||
'exp-in-out': EasingFns.expInOut,
|
||||
'quad-in': EasingFns.quadIn,
|
||||
'quad-out': EasingFns.quadOut,
|
||||
'quad-in-out': EasingFns.quadInOut,
|
||||
'sin-in': EasingFns.sinIn,
|
||||
'sin-out': EasingFns.sinOut,
|
||||
'sin-in-out': EasingFns.sinInOut,
|
||||
};
|
||||
|
||||
interface InterpolationCacheEntry {
|
||||
paletteFn?: (value: number) => Color,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 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 { Viewport, cameraProject, cameraUnproject } from './camera/util';
|
||||
import { CameraTransitionManager, CameraTransitionOptions } from './camera/transition';
|
||||
import { CameraTransitionManager } from './camera/transition';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { Scene } from '../mol-gl/scene';
|
||||
import { assertUnreachable } from '../mol-util/type-helpers';
|
||||
@@ -138,12 +138,8 @@ export class Camera implements ICamera {
|
||||
return changed;
|
||||
}
|
||||
|
||||
setState(
|
||||
snapshot: Partial<Camera.Snapshot>,
|
||||
durationMs?: number,
|
||||
options?: CameraTransitionOptions
|
||||
) {
|
||||
this.transition.apply(snapshot, durationMs, undefined, options);
|
||||
setState(snapshot: Partial<Camera.Snapshot>, durationMs?: number) {
|
||||
this.transition.apply(snapshot, durationMs);
|
||||
this.stateChanged.next(snapshot);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2026 Mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2024 Mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -8,17 +8,9 @@ import { Camera } from '../camera';
|
||||
import { lerp } from '../../mol-math/interpolate';
|
||||
import { Quat } from '../../mol-math/linear-algebra/3d/quat';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { EasingFunction, getEasingFn } from '../../mol-math/easing';
|
||||
|
||||
export { CameraTransitionManager };
|
||||
|
||||
export interface CameraTransitionOptions {
|
||||
/** If present, approximates the transion between, [current] -> [keyframes] -> -> [target] */
|
||||
keyframes?: CameraTransitionManager.TransitionKeyframes,
|
||||
/** Global easing, if easing is specified for keyframes, the "end" frame value is used */
|
||||
easing?: EasingFunction
|
||||
}
|
||||
|
||||
class CameraTransitionManager {
|
||||
private t = 0;
|
||||
|
||||
@@ -28,18 +20,12 @@ class CameraTransitionManager {
|
||||
private durationMs = 0;
|
||||
private _source: Camera.Snapshot = Camera.createDefaultSnapshot();
|
||||
private _target: Camera.Snapshot = Camera.createDefaultSnapshot();
|
||||
private _options: CameraTransitionOptions | undefined = void 0;
|
||||
private _current = Camera.createDefaultSnapshot();
|
||||
|
||||
get source(): Readonly<Camera.Snapshot> { return this._source; }
|
||||
get target(): Readonly<Camera.Snapshot> { return this._target; }
|
||||
|
||||
apply(
|
||||
to: Partial<Camera.Snapshot>,
|
||||
durationMs: number = 0,
|
||||
transition?: CameraTransitionManager.TransitionFunc,
|
||||
options?: CameraTransitionOptions,
|
||||
) {
|
||||
apply(to: Partial<Camera.Snapshot>, durationMs: number = 0, transition?: CameraTransitionManager.TransitionFunc) {
|
||||
if (!this.inTransition || durationMs > 0) {
|
||||
Camera.copySnapshot(this._source, this.camera.state);
|
||||
}
|
||||
@@ -64,7 +50,6 @@ class CameraTransitionManager {
|
||||
|
||||
this.inTransition = true;
|
||||
this.func = transition || CameraTransitionManager.defaultTransition;
|
||||
this._options = options;
|
||||
|
||||
if (!this.inTransition || durationMs > 0) {
|
||||
this.start = this.t;
|
||||
@@ -91,7 +76,7 @@ class CameraTransitionManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.func(this._current, normalized, this._source, this._target, this._options);
|
||||
this.func(this._current, normalized, this._source, this._target);
|
||||
Camera.copySnapshot(this.camera.state, this._current);
|
||||
}
|
||||
|
||||
@@ -101,8 +86,7 @@ class CameraTransitionManager {
|
||||
}
|
||||
|
||||
namespace CameraTransitionManager {
|
||||
export type TransitionKeyframes = { t: number, snapshot: Partial<Camera.Snapshot>, easing?: EasingFunction }[]
|
||||
export type TransitionFunc = (out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot, options?: { keyframes?: TransitionKeyframes }) => void
|
||||
export type TransitionFunc = (out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot) => void
|
||||
|
||||
const _rotUp = Quat.identity();
|
||||
const _rotDist = Quat.identity();
|
||||
@@ -110,58 +94,7 @@ namespace CameraTransitionManager {
|
||||
const _sourcePosition = Vec3();
|
||||
const _targetPosition = Vec3();
|
||||
|
||||
let _tempSource: Camera.Snapshot | undefined = void 0;
|
||||
let _tempTarget: Camera.Snapshot | undefined = void 0;
|
||||
|
||||
export function defaultTransition(
|
||||
out: Camera.Snapshot,
|
||||
t_: number,
|
||||
source_: Camera.Snapshot,
|
||||
target_: Camera.Snapshot,
|
||||
options?: CameraTransitionOptions
|
||||
): void {
|
||||
let sourcePartial: Partial<Camera.Snapshot> = source_;
|
||||
let targetPartial: Partial<Camera.Snapshot> = target_;
|
||||
|
||||
let tStart = 0;
|
||||
let tEnd = 1;
|
||||
let easingKind = options?.easing;
|
||||
|
||||
const keyframes = options?.keyframes;
|
||||
if (keyframes && keyframes.length > 0) {
|
||||
for (let i = 0; i < keyframes.length; i++) {
|
||||
const keyframe = keyframes[i];
|
||||
if (t_ >= keyframe.t) {
|
||||
sourcePartial = keyframe.snapshot;
|
||||
tStart = keyframe.t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < keyframes.length; i++) {
|
||||
const keyframe = keyframes[i];
|
||||
if (keyframe.t >= t_) {
|
||||
targetPartial = keyframe.snapshot;
|
||||
tEnd = keyframe.t;
|
||||
easingKind = keyframe.easing ?? easingKind;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const easing = getEasingFn(easingKind);
|
||||
const t = easing((t_ - tStart) / (tEnd - tStart));
|
||||
|
||||
if (!_tempSource) _tempSource = Camera.createDefaultSnapshot();
|
||||
if (!_tempTarget) _tempTarget = Camera.createDefaultSnapshot();
|
||||
|
||||
Camera.copySnapshot(_tempSource, source_);
|
||||
Camera.copySnapshot(_tempSource, sourcePartial);
|
||||
Camera.copySnapshot(_tempTarget, target_);
|
||||
Camera.copySnapshot(_tempTarget, targetPartial);
|
||||
|
||||
const source = _tempSource;
|
||||
const target = _tempTarget;
|
||||
|
||||
export function defaultTransition(out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot): void {
|
||||
Camera.copySnapshot(out, target);
|
||||
|
||||
// Rotate up
|
||||
|
||||
@@ -53,8 +53,6 @@ import { RayHelper } from './helper/ray-helper';
|
||||
import { produce } from '../mol-util/produce';
|
||||
import { ShaderManager } from './helper/shader-manager';
|
||||
import { toFixed } from '../mol-util/number';
|
||||
import type { CameraTransitionManager } from './camera/transition';
|
||||
import { EasingFunction } from '../mol-math/easing';
|
||||
|
||||
export const CameraFogParams = {
|
||||
intensity: PD.Numeric(15, { min: 1, max: 100, step: 1 }),
|
||||
@@ -323,13 +321,6 @@ namespace Canvas3DContext {
|
||||
|
||||
export { Canvas3D };
|
||||
|
||||
export interface Canvas3DCameraResetOptions {
|
||||
durationMs?: number,
|
||||
snapshot?: Camera.SnapshotProvider,
|
||||
keyframes?: CameraTransitionManager.TransitionKeyframes,
|
||||
easing?: EasingFunction,
|
||||
}
|
||||
|
||||
interface Canvas3D {
|
||||
readonly webgl: WebGLContext,
|
||||
|
||||
@@ -380,7 +371,7 @@ interface Canvas3D {
|
||||
/** performs handleResize on the next animation frame */
|
||||
requestResize(): void
|
||||
/** Focuses camera on scene's bounding sphere, centered and zoomed. */
|
||||
requestCameraReset(options?: Canvas3DCameraResetOptions): void
|
||||
requestCameraReset(options?: { durationMs?: number, snapshot?: Camera.SnapshotProvider }): void
|
||||
readonly camera: Camera
|
||||
readonly boundingSphere: Readonly<Sphere3D>
|
||||
readonly boundingSphereVisible: Readonly<Sphere3D>
|
||||
@@ -507,12 +498,8 @@ namespace Canvas3D {
|
||||
});
|
||||
|
||||
let cameraResetRequested = false;
|
||||
const nextCameraResetOptions: Canvas3DCameraResetOptions = {
|
||||
durationMs: undefined,
|
||||
snapshot: undefined,
|
||||
keyframes: undefined,
|
||||
easing: undefined,
|
||||
};
|
||||
let nextCameraResetDuration: number | undefined = void 0;
|
||||
let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0;
|
||||
let resizeRequested = false;
|
||||
|
||||
//
|
||||
@@ -891,18 +878,15 @@ namespace Canvas3D {
|
||||
}
|
||||
|
||||
if (radius > 0) {
|
||||
const duration = nextCameraResetOptions.durationMs === undefined ? p.cameraResetDurationMs : nextCameraResetOptions.durationMs;
|
||||
const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration;
|
||||
const focus = camera.getFocus(center, radius);
|
||||
const next = typeof nextCameraResetOptions.snapshot === 'function' ? nextCameraResetOptions.snapshot(scene, camera) : nextCameraResetOptions.snapshot;
|
||||
const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot;
|
||||
const snapshot = next ? { ...focus, ...next } : focus;
|
||||
camera.setState({ ...snapshot, radiusMax: getSceneRadius() }, duration, { keyframes: nextCameraResetOptions.keyframes, easing: nextCameraResetOptions.easing });
|
||||
camera.setState({ ...snapshot, radiusMax: getSceneRadius() }, duration);
|
||||
}
|
||||
|
||||
nextCameraResetOptions.durationMs = void 0;
|
||||
nextCameraResetOptions.snapshot = void 0;
|
||||
nextCameraResetOptions.keyframes = void 0;
|
||||
nextCameraResetOptions.easing = void 0;
|
||||
|
||||
nextCameraResetDuration = void 0;
|
||||
nextCameraResetSnapshot = void 0;
|
||||
cameraResetRequested = false;
|
||||
}
|
||||
|
||||
@@ -912,7 +896,7 @@ namespace Canvas3D {
|
||||
function shouldResetCamera() {
|
||||
if (camera.state.radiusMax === 0) return true;
|
||||
|
||||
if (camera.transition.inTransition || nextCameraResetOptions.snapshot) return false;
|
||||
if (camera.transition.inTransition || nextCameraResetSnapshot) return false;
|
||||
|
||||
let cameraSphereOverlapsNone = true, isEmpty = true;
|
||||
Sphere3D.set(cameraSphere, camera.state.target, camera.state.radius);
|
||||
@@ -954,7 +938,7 @@ namespace Canvas3D {
|
||||
if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) {
|
||||
cameraResetRequested = true;
|
||||
}
|
||||
if (oldBoundingSphereVisible.radius === 0) nextCameraResetOptions.durationMs = 0;
|
||||
if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
|
||||
|
||||
if (!p.camera.manualReset) camera.setState({ radiusMax: getSceneRadius() }, 0);
|
||||
reprCount.next(reprRenderObjects.size);
|
||||
@@ -1238,7 +1222,7 @@ namespace Canvas3D {
|
||||
syncVisibility: () => {
|
||||
if (camera.state.radiusMax === 0) {
|
||||
cameraResetRequested = true;
|
||||
nextCameraResetOptions.durationMs = 0;
|
||||
nextCameraResetDuration = 0;
|
||||
}
|
||||
|
||||
if (scene.syncVisibility()) {
|
||||
@@ -1267,7 +1251,8 @@ namespace Canvas3D {
|
||||
resizeRequested = true;
|
||||
},
|
||||
requestCameraReset: options => {
|
||||
Object.assign(nextCameraResetOptions, options);
|
||||
nextCameraResetDuration = options?.durationMs;
|
||||
nextCameraResetSnapshot = options?.snapshot;
|
||||
cameraResetRequested = true;
|
||||
},
|
||||
camera,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2018-26 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018 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>
|
||||
*
|
||||
* adapted from https://github.com/d3/d3-ease
|
||||
*/
|
||||
@@ -104,33 +103,3 @@ export function sinInOut(t: number) {
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export const EasingFunctions = {
|
||||
'linear': (t: number) => t,
|
||||
'bounce-in': bounceIn,
|
||||
'bounce-out': bounceOut,
|
||||
'bounce-in-out': bounceInOut,
|
||||
'circle-in': circleIn,
|
||||
'circle-out': circleOut,
|
||||
'circle-in-out': circleInOut,
|
||||
'cubic-in': cubicIn,
|
||||
'cubic-out': cubicOut,
|
||||
'cubic-in-out': cubicInOut,
|
||||
'exp-in': expIn,
|
||||
'exp-out': expOut,
|
||||
'exp-in-out': expInOut,
|
||||
'quad-in': quadIn,
|
||||
'quad-out': quadOut,
|
||||
'quad-in-out': quadInOut,
|
||||
'sin-in': sinIn,
|
||||
'sin-out': sinOut,
|
||||
'sin-in-out': sinInOut,
|
||||
};
|
||||
|
||||
export type EasingKind = keyof typeof EasingFunctions;
|
||||
export type EasingFunction = EasingKind | ((t: number) => number);
|
||||
|
||||
export function getEasingFn(easing: EasingFunction | undefined): (t: number) => number {
|
||||
if (!easing) return EasingFunctions.linear;
|
||||
return typeof easing === 'function' ? easing : EasingFunctions[easing] ?? EasingFunctions.linear;
|
||||
}
|
||||
@@ -150,10 +150,6 @@ namespace Mat3 {
|
||||
return areEqual(m, _id, typeof eps === 'undefined' ? EPSILON : eps);
|
||||
}
|
||||
|
||||
export function is(a: any): a is Mat3 {
|
||||
return Array.isArray(a) && a.length === 9;
|
||||
}
|
||||
|
||||
export function hasNaN(m: Mat3) {
|
||||
for (let i = 0; i < 9; i++) if (Number.isNaN(m[i])) return true;
|
||||
return false;
|
||||
|
||||
@@ -110,10 +110,6 @@ namespace Mat4 {
|
||||
return areEqual(m, _id, typeof eps === 'undefined' ? EPSILON : eps);
|
||||
}
|
||||
|
||||
export function is(a: any): a is Mat4 {
|
||||
return Array.isArray(a) && a.length === 16;
|
||||
}
|
||||
|
||||
export function hasNaN(m: Mat4) {
|
||||
for (let i = 0; i < 16; i++) if (Number.isNaN(m[i])) return true;
|
||||
return false;
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Vec3 } from './vec3';
|
||||
import { EVD } from '../matrix/evd';
|
||||
import { Matrix } from '../matrix/matrix';
|
||||
|
||||
export interface LeastObstructedDirectionOptions {
|
||||
/** Optional centroid/origin. If omitted, centroid is computed from the provided points. */
|
||||
origin?: Vec3,
|
||||
|
||||
/** Optional Gaussian falloff distance. If omitted, all points have weight 1. */
|
||||
sigma?: number,
|
||||
|
||||
/** Ignore points closer than this to the origin. */
|
||||
minDistance?: number,
|
||||
}
|
||||
|
||||
function eachPosition(points: ReadonlyArray<Vec3> | { x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number> }, callback: (x: number, y: number, z: number) => void) {
|
||||
if (Array.isArray(points)) {
|
||||
for (const p of points) {
|
||||
callback(p[0], p[1], p[2]);
|
||||
}
|
||||
} else {
|
||||
const { x, y, z } = points as { x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number> };
|
||||
const n = Math.min(x.length, y.length, z.length);
|
||||
for (let i = 0; i < n; i++) {
|
||||
callback(x[i], y[i], z[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate a visually open camera direction around a selection.
|
||||
*
|
||||
* Geometric intuition:
|
||||
*
|
||||
* The selection centroid is treated as the origin. Each nearby obstruction
|
||||
* point is converted into a unit direction on the sphere around the selection:
|
||||
*
|
||||
* v_i = normalize(p_i - origin)
|
||||
*
|
||||
* We then build the directional second-moment matrix:
|
||||
*
|
||||
* M = sum_i w_i v_i v_i^T
|
||||
*
|
||||
* For any candidate view direction `u`, the quadratic form
|
||||
*
|
||||
* u^T M u
|
||||
*
|
||||
* expands to:
|
||||
*
|
||||
* sum_i w_i (u · v_i)^2
|
||||
*
|
||||
* Since `u · v_i = cos(theta_i)`, this value is large when `u` is aligned
|
||||
* with many obstruction directions and small when `u` is mostly perpendicular
|
||||
* to them. Therefore, the eigenvector of `M` with the smallest eigenvalue is
|
||||
* the axis that is least aligned, in a least-squares sense, with the nearby
|
||||
* obstruction directions.
|
||||
*
|
||||
* This gives an unoriented axis: `u` and `-u` have the same score because the
|
||||
* dot products are squared. To choose the camera-facing side, we compute the
|
||||
* weighted mean obstruction direction:
|
||||
*
|
||||
* m = sum_i w_i v_i
|
||||
*
|
||||
* and return the sign of the axis that points away from this mean direction.
|
||||
*
|
||||
* In short:
|
||||
*
|
||||
* - project nearby points onto a sphere around the selection;
|
||||
* - find the sparsest angular axis using the smallest eigenvector of their
|
||||
* second-moment matrix;
|
||||
* - choose the side of that axis opposite the average obstruction direction.
|
||||
*
|
||||
* This is a fast, deterministic heuristic. It minimizes average squared
|
||||
* angular alignment with nearby points; it is not the exact largest-empty-cone
|
||||
* or maximum-clearance solution.
|
||||
*
|
||||
* The returned vector is a unit direction from the selection centroid toward
|
||||
* the camera.
|
||||
*/
|
||||
export function leastObstructedDirection(
|
||||
points: ReadonlyArray<Vec3> | { x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number> },
|
||||
options: LeastObstructedDirectionOptions = {}
|
||||
): Vec3 | undefined {
|
||||
const origin = options.origin;
|
||||
const minDistance = options.minDistance ?? 1e-6;
|
||||
const minDistanceSq = minDistance * minDistance;
|
||||
|
||||
const sigma = options.sigma;
|
||||
const useWeights = sigma !== void 0 && sigma > 0;
|
||||
const twoSigmaSq = useWeights ? 2 * sigma * sigma : 1;
|
||||
|
||||
// Directional second moment:
|
||||
// M = sum_i w_i v_i v_i^T
|
||||
const evd = EVD.createCache(3);
|
||||
const M = evd.matrix;
|
||||
Matrix.makeZero(M);
|
||||
|
||||
// Weighted mean direction, used only to choose sign.
|
||||
const mean = Vec3.zero();
|
||||
|
||||
let count = 0;
|
||||
let weightSum = 0;
|
||||
|
||||
eachPosition(points, (x_, y_, z_) => {
|
||||
let x = x_, y = y_, z = z_;
|
||||
if (origin) {
|
||||
x -= origin[0];
|
||||
y -= origin[1];
|
||||
z -= origin[2];
|
||||
}
|
||||
|
||||
const dSq = x * x + y * y + z * z;
|
||||
if (dSq <= minDistanceSq) return;
|
||||
|
||||
const d = Math.sqrt(dSq);
|
||||
const invD = 1 / d;
|
||||
|
||||
// Unit obstruction direction v.
|
||||
x *= invD;
|
||||
y *= invD;
|
||||
z *= invD;
|
||||
|
||||
const w = useWeights ? Math.exp(-dSq / twoSigmaSq) : 1;
|
||||
|
||||
// Accumulate symmetric matrix.
|
||||
//
|
||||
// M = [
|
||||
// xx xy xz
|
||||
// xy yy yz
|
||||
// xz yz zz
|
||||
// ]
|
||||
Matrix.add(M, 0, 0, w * x * x);
|
||||
Matrix.add(M, 0, 1, w * x * y);
|
||||
Matrix.add(M, 0, 2, w * x * z);
|
||||
|
||||
Matrix.add(M, 1, 0, w * y * x);
|
||||
Matrix.add(M, 1, 1, w * y * y);
|
||||
Matrix.add(M, 1, 2, w * y * z);
|
||||
|
||||
Matrix.add(M, 2, 0, w * z * x);
|
||||
Matrix.add(M, 2, 1, w * z * y);
|
||||
Matrix.add(M, 2, 2, w * z * z);
|
||||
|
||||
mean[0] += w * x;
|
||||
mean[1] += w * y;
|
||||
mean[2] += w * z;
|
||||
|
||||
count++;
|
||||
weightSum += w;
|
||||
});
|
||||
|
||||
if (count === 0 || weightSum <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
EVD.compute(evd);
|
||||
|
||||
// EVD sorts eigenvalues ascending, so column 0 is the smallest eigenvector.
|
||||
const dir = Vec3.create(
|
||||
Matrix.get(M, 0, 0),
|
||||
Matrix.get(M, 1, 0),
|
||||
Matrix.get(M, 2, 0)
|
||||
);
|
||||
|
||||
if (Vec3.magnitude(dir) < 1e-6) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Vec3.normalize(dir, dir);
|
||||
|
||||
// Pick the less-obstructed side of the axis:
|
||||
// choose the sign opposite the weighted mean obstruction direction.
|
||||
if (Vec3.dot(dir, mean) > 0) {
|
||||
Vec3.scale(dir, dir, -1);
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
@@ -71,10 +71,6 @@ namespace Quat {
|
||||
return out;
|
||||
}
|
||||
|
||||
export function is(a: any): a is Quat {
|
||||
return Array.isArray(a) && a.length === 4;
|
||||
}
|
||||
|
||||
export function setAxisAngle(out: Quat, axis: Vec3, rad: number) {
|
||||
rad = rad * 0.5;
|
||||
const s = Math.sin(rad);
|
||||
|
||||
@@ -58,10 +58,6 @@ namespace Vec2 {
|
||||
return Number.isNaN(a[0]) || Number.isNaN(a[1]);
|
||||
}
|
||||
|
||||
export function is(a: any): a is Vec2 {
|
||||
return Array.isArray(a) && a.length === 2;
|
||||
}
|
||||
|
||||
export function toArray<T extends NumberArray>(a: Vec2, out: T, offset: number) {
|
||||
out[offset + 0] = a[0];
|
||||
out[offset + 1] = a[1];
|
||||
|
||||
@@ -48,10 +48,6 @@ export namespace Vec3 {
|
||||
return out;
|
||||
}
|
||||
|
||||
export function is(a: any): a is Vec3 {
|
||||
return Array.isArray(a) && a.length === 3;
|
||||
}
|
||||
|
||||
export function isFinite(a: Vec3): boolean {
|
||||
return _isFinite(a[0]) && _isFinite(a[1]) && _isFinite(a[2]);
|
||||
}
|
||||
|
||||
@@ -71,10 +71,6 @@ namespace Vec4 {
|
||||
return Number.isNaN(a[0]) || Number.isNaN(a[1]) || Number.isNaN(a[2]) || Number.isNaN(a[3]);
|
||||
}
|
||||
|
||||
export function is(a: any): a is Vec4 {
|
||||
return Array.isArray(a) && a.length === 4;
|
||||
}
|
||||
|
||||
export function toArray<T extends NumberArray>(a: Vec4, out: T, offset: number) {
|
||||
out[offset + 0] = a[0];
|
||||
out[offset + 1] = a[1];
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Vec3 } from '../3d/vec3';
|
||||
import { leastObstructedDirection } from '../3d/optimize-direction';
|
||||
|
||||
describe('OptimizeDirection', () => {
|
||||
it('works more or less as expected', () => {
|
||||
const points: Vec3[] = [
|
||||
Vec3.create(1, 0, 0),
|
||||
Vec3.create(-1, 0, 0),
|
||||
Vec3.create(0, 1, 0),
|
||||
Vec3.create(0, -1, 0),
|
||||
Vec3.create(0, 0, 1),
|
||||
];
|
||||
const dir = leastObstructedDirection(points);
|
||||
|
||||
console.log('dir', dir);
|
||||
expect(dir).toBeDefined();
|
||||
expect(dir[0]).toBeCloseTo(0, 6);
|
||||
expect(dir[1]).toBeCloseTo(0, 6);
|
||||
expect(dir[2]).toBeCloseTo(-1, 6);
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2025 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>
|
||||
@@ -572,42 +572,6 @@ export namespace Loci {
|
||||
return Loci(loci.structure, elements);
|
||||
}
|
||||
|
||||
export function extendToRadius(loci: Loci, radius: number): Loci {
|
||||
const elementsByUnit = new Map<number, Set<UnitIndex>>();
|
||||
|
||||
const lookup = loci.structure.lookup3d;
|
||||
const pos = Vec3();
|
||||
forEachLocation(loci, loc => {
|
||||
loc.unit.conformation.position(loc.element, pos);
|
||||
const result = lookup.find(pos[0], pos[1], pos[2], radius);
|
||||
for (let i = 0, il = result.count; i < il; ++i) {
|
||||
const unit = result.units[i];
|
||||
const unitIdx = result.indices[i];
|
||||
let set: Set<UnitIndex> = elementsByUnit.get(unit.id) as Set<UnitIndex>;
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
elementsByUnit.set(unit.id, set);
|
||||
}
|
||||
set.add(unitIdx);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const elements: Element[] = [];
|
||||
for (const [unitId, indexSet] of elementsByUnit.entries()) {
|
||||
const unit = loci.structure.unitMap.get(unitId)!;
|
||||
const indices = Array.from(indexSet) as UnitIndex[];
|
||||
indices.sort((a, b) => a - b);
|
||||
elements.push({ unit, indices: makeIndexSet(indices) });
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'element-loci',
|
||||
structure: loci.structure,
|
||||
elements,
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const boundaryHelper = new BoundaryHelper('98');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 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>
|
||||
@@ -12,11 +12,10 @@ import { GraphicsRenderObject } from '../../mol-gl/render-object';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
|
||||
import { Mat3 } from '../../mol-math/linear-algebra';
|
||||
import { leastObstructedDirection } from '../../mol-math/linear-algebra/3d/optimize-direction';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { PrincipalAxes } from '../../mol-math/linear-algebra/matrix/principal-axes';
|
||||
import { Loci } from '../../mol-model/loci';
|
||||
import { Structure, StructureElement, StructureProperties } from '../../mol-model/structure';
|
||||
import { Structure, StructureElement } from '../../mol-model/structure';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { PluginStateObject } from '../objects';
|
||||
@@ -24,25 +23,15 @@ import { pcaFocus } from './focus-camera/focus-first-residue';
|
||||
import { getFocusSnapshot } from './focus-camera/focus-object';
|
||||
import { changeCameraRotation, structureLayingTransform } from './focus-camera/orient-axes';
|
||||
|
||||
export const DefaultCameraFocusOptions = {
|
||||
// TODO: make this customizable somewhere?
|
||||
const DefaultCameraFocusOptions = {
|
||||
minRadius: 1,
|
||||
extraRadius: 4,
|
||||
durationMs: 250,
|
||||
// When set, zooms out to the current scene bounding sphere before focusing on the target.
|
||||
zoomOut: false,
|
||||
zoomOutOptions: {
|
||||
durationFactor: 3.5,
|
||||
}
|
||||
};
|
||||
|
||||
export const DefaultCameraFocusLociOptions = {
|
||||
...DefaultCameraFocusOptions,
|
||||
optimizeDirection: false,
|
||||
optimizeDirectionUp: 'current' as 'current' | 'default' | Vec3,
|
||||
};
|
||||
export type CameraFocusOptions = typeof DefaultCameraFocusOptions
|
||||
|
||||
export type CameraFocusOptions = typeof DefaultCameraFocusOptions;
|
||||
export type CameraFocusLociOptions = typeof DefaultCameraFocusLociOptions;
|
||||
export class CameraManager {
|
||||
private boundaryHelper = new BoundaryHelper('98');
|
||||
|
||||
@@ -68,7 +57,10 @@ export class CameraManager {
|
||||
this.focusSpheres(spheres, s => s, options);
|
||||
}
|
||||
|
||||
private getFocusSphere(loci: Loci | Loci[]) {
|
||||
focusLoci(loci: Loci | Loci[], options?: Partial<CameraFocusOptions>) {
|
||||
// TODO: allow computation of principal axes here?
|
||||
// perhaps have an optimized function, that does exact axes small Loci and approximate/sampled from big ones?
|
||||
|
||||
let sphere: Sphere3D | undefined;
|
||||
|
||||
if (Array.isArray(loci) && loci.length > 1) {
|
||||
@@ -96,86 +88,11 @@ export class CameraManager {
|
||||
sphere = Loci.getBoundingSphere(this.transformedLoci(loci));
|
||||
}
|
||||
|
||||
return sphere;
|
||||
}
|
||||
|
||||
private focusLociOptimized(loci: Loci | Loci[], options?: Partial<CameraFocusLociOptions>) {
|
||||
const { canvas3d } = this.plugin;
|
||||
if (!canvas3d) return;
|
||||
|
||||
const sphere = this.getFocusSphere(loci);
|
||||
if (!sphere) return;
|
||||
|
||||
const lociArray = Array.isArray(loci) ? loci : [loci];
|
||||
const positions: { x: number[], y: number[], z: number[] } = { x: [], y: [], z: [] };
|
||||
const t = Vec3();
|
||||
|
||||
const { extraRadius, minRadius } = { ...DefaultCameraFocusOptions, ...options };
|
||||
const radius = Math.max(sphere.radius + extraRadius, minRadius);
|
||||
|
||||
if (radius <= 1e-3) {
|
||||
return this.getFocusSphereSnapshot(sphere, options);
|
||||
}
|
||||
|
||||
const entityType = StructureProperties.entity.type;
|
||||
|
||||
for (const l of lociArray) {
|
||||
if (!StructureElement.Loci.is(l)) continue;
|
||||
const extended = StructureElement.Loci.extendToRadius(l, radius);
|
||||
StructureElement.Loci.forEachLocation(extended, loc => {
|
||||
if (entityType(loc) === 'water') return;
|
||||
|
||||
loc.unit.conformation.position(loc.element, t);
|
||||
positions.x.push(t[0]);
|
||||
positions.y.push(t[1]);
|
||||
positions.z.push(t[2]);
|
||||
});
|
||||
}
|
||||
|
||||
if (positions.x.length === 0) {
|
||||
return this.getFocusSphereSnapshot(sphere, options);
|
||||
}
|
||||
|
||||
const direction = leastObstructedDirection(positions, {
|
||||
origin: sphere.center,
|
||||
minDistance: 1e-3,
|
||||
sigma: sphere.radius,
|
||||
});
|
||||
if (!direction) {
|
||||
return this.getFocusSphereSnapshot(sphere, options);
|
||||
}
|
||||
|
||||
Vec3.negate(direction, direction);
|
||||
const upVector = options?.optimizeDirectionUp === 'default'
|
||||
? Vec3.unitY
|
||||
: Vec3.is(options?.optimizeDirectionUp) ? options.optimizeDirectionUp : undefined;
|
||||
if (upVector) {
|
||||
return canvas3d.camera.getInvariantFocus(sphere.center, radius, upVector as Vec3, direction);
|
||||
}
|
||||
return canvas3d.camera.getFocus(sphere.center, radius, undefined, direction);
|
||||
}
|
||||
|
||||
private focusLociBase(loci: Loci | Loci[], options?: Partial<CameraFocusOptions>) {
|
||||
const sphere = this.getFocusSphere(loci);
|
||||
if (sphere) {
|
||||
return this.getFocusSphereSnapshot(sphere, options);
|
||||
this.focusSphere(sphere, options);
|
||||
}
|
||||
}
|
||||
|
||||
focusLoci(loci: Loci | Loci[], options?: Partial<CameraFocusLociOptions>) {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
|
||||
const options_ = { ...DefaultCameraFocusLociOptions, ...options };
|
||||
let snapshot: Partial<Camera.Snapshot> | undefined;
|
||||
if (options_.optimizeDirection) {
|
||||
snapshot = this.focusLociOptimized(loci, options_);
|
||||
} else {
|
||||
snapshot = this.focusLociBase(loci, options_);
|
||||
}
|
||||
|
||||
this.focusSnapshot(snapshot, options_);
|
||||
}
|
||||
|
||||
focusSpheres<T>(xs: ReadonlyArray<T>, sphere: (t: T) => Sphere3D | undefined, options?: Partial<CameraFocusOptions> & { principalAxes?: PrincipalAxes, positionToFlip?: Vec3 }) {
|
||||
const spheres = [];
|
||||
|
||||
@@ -198,59 +115,21 @@ export class CameraManager {
|
||||
this.focusSphere(this.boundaryHelper.getSphere(), options);
|
||||
}
|
||||
|
||||
private getFocusSphereSnapshot(sphere: Sphere3D, options?: Partial<CameraFocusOptions> & { principalAxes?: PrincipalAxes, positionToFlip?: Vec3 }) {
|
||||
const { canvas3d } = this.plugin;
|
||||
if (!canvas3d) return;
|
||||
|
||||
const { extraRadius, minRadius } = { ...DefaultCameraFocusOptions, ...options };
|
||||
const radius = Math.max(sphere.radius + extraRadius, minRadius);
|
||||
|
||||
if (options?.principalAxes) {
|
||||
return pcaFocus(this.plugin, radius, options as { principalAxes: PrincipalAxes, positionToFlip?: Vec3 });
|
||||
} else {
|
||||
return canvas3d.camera.getFocus(sphere.center, radius);
|
||||
}
|
||||
}
|
||||
|
||||
private focusSnapshot(snapshot: Partial<Camera.Snapshot> | undefined, options?: Partial<CameraFocusOptions>) {
|
||||
if (!this.plugin.canvas3d || !snapshot) return;
|
||||
|
||||
const durationMs = options?.durationMs ?? DefaultCameraFocusOptions.durationMs;
|
||||
if (!options?.zoomOut) {
|
||||
this.plugin.canvas3d.requestCameraReset({ snapshot, durationMs });
|
||||
return;
|
||||
}
|
||||
|
||||
const sphere = this.plugin.canvas3d.boundingSphere;
|
||||
const zoomOut = this.getFocusSphereSnapshot(sphere, options) as Camera.Snapshot;
|
||||
const current = this.plugin.canvas3d?.camera.getSnapshot()!;
|
||||
|
||||
const distA = Vec3.distance(current.position, zoomOut.position);
|
||||
const distB = Vec3.distance(zoomOut.position, snapshot.position!);
|
||||
|
||||
const t = distA / (distA + distB);
|
||||
const durationFactor = options?.zoomOutOptions?.durationFactor ?? DefaultCameraFocusOptions.zoomOutOptions.durationFactor;
|
||||
const df = 1 + durationFactor * Math.min(t, 0.5);
|
||||
|
||||
this.plugin.canvas3d.requestCameraReset({
|
||||
snapshot,
|
||||
durationMs: df * durationMs,
|
||||
keyframes: t > 0.05 ? [
|
||||
{ t, snapshot: zoomOut, easing: 'cubic-out' },
|
||||
{ t: 1, snapshot, easing: 'cubic-in' },
|
||||
] : undefined
|
||||
});
|
||||
}
|
||||
|
||||
focusSphere(sphere: Sphere3D, options?: Partial<CameraFocusOptions> & { principalAxes?: PrincipalAxes, positionToFlip?: Vec3 }) {
|
||||
const { canvas3d } = this.plugin;
|
||||
if (!canvas3d) return;
|
||||
|
||||
const snapshot = this.getFocusSphereSnapshot(sphere, options);
|
||||
if (!snapshot) return;
|
||||
const { extraRadius, minRadius, durationMs } = { ...DefaultCameraFocusOptions, ...options };
|
||||
const radius = Math.max(sphere.radius + extraRadius, minRadius);
|
||||
|
||||
this.focusSnapshot(snapshot, options);
|
||||
}
|
||||
if (options?.principalAxes) {
|
||||
const snapshot = pcaFocus(this.plugin, radius, options as { principalAxes: PrincipalAxes, positionToFlip?: Vec3 });
|
||||
this.plugin.canvas3d?.requestCameraReset({ durationMs, snapshot });
|
||||
} else {
|
||||
const snapshot = canvas3d.camera.getFocus(sphere.center, radius);
|
||||
canvas3d.requestCameraReset({ durationMs, snapshot });
|
||||
}
|
||||
}
|
||||
|
||||
/** Focus on a set of plugin state object cells (if `options.targets` is non-empty) or on the whole scene (if `options.targets` is empty). */
|
||||
focusObject(options: PluginState.SnapshotFocusInfo & { minRadius?: number, durationMs?: number }) {
|
||||
@@ -260,7 +139,7 @@ export class CameraManager {
|
||||
targets: options.targets?.map(t => ({ ...t, extraRadius: t.extraRadius ?? DefaultCameraFocusOptions.extraRadius })),
|
||||
minRadius: options.minRadius ?? DefaultCameraFocusOptions.minRadius,
|
||||
});
|
||||
this.focusSnapshot(snapshot, options);
|
||||
this.plugin.canvas3d.requestCameraReset({ snapshot, durationMs: options.durationMs ?? DefaultCameraFocusOptions.durationMs });
|
||||
}
|
||||
|
||||
/** Align PCA axes of `structures` (default: all loaded structures) to the screen axes. */
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2022 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 { OrderedSet, SortedArray } from '../../mol-data/int';
|
||||
@@ -185,23 +184,14 @@ export class StructureFocusControls extends PluginUIComponent<{}, StructureFocus
|
||||
} else {
|
||||
this.plugin.managers.structure.focus.set(f);
|
||||
}
|
||||
this.focusCamera(true);
|
||||
this.focusCamera();
|
||||
};
|
||||
|
||||
focusCamera(optimizeDirection?: boolean) {
|
||||
const { current } = this.plugin.managers.structure.focus;
|
||||
if (!current) return;
|
||||
|
||||
this.plugin.managers.camera.focusLoci(current.loci, {
|
||||
optimizeDirection,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
toggleAction = () => this.setState({ showAction: !this.state.showAction });
|
||||
|
||||
focusCameraClick = () => {
|
||||
this.focusCamera(false);
|
||||
focusCamera = () => {
|
||||
const { current } = this.plugin.managers.structure.focus;
|
||||
if (current) this.plugin.managers.camera.focusLoci(current.loci);
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
@@ -241,7 +231,7 @@ export class StructureFocusControls extends PluginUIComponent<{}, StructureFocus
|
||||
|
||||
return <>
|
||||
<div className='msp-flex-row'>
|
||||
<Button noOverflow onClick={this.focusCameraClick} title={title} onMouseEnter={this.highlightCurrent} onMouseLeave={this.clearHighlights} disabled={this.isDisabled || !current}
|
||||
<Button noOverflow onClick={this.focusCamera} title={title} onMouseEnter={this.highlightCurrent} onMouseLeave={this.clearHighlights} disabled={this.isDisabled || !current}
|
||||
style={{ textAlignLast: current ? 'left' : void 0 }}>
|
||||
{label}
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "CommonJS",
|
||||
"outDir": "lib/commonjs"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user