mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 13:30:24 +08:00
camera focus "zoom out"
This commit is contained in:
@@ -17,7 +17,9 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Fix bugs in ModelServer surroundingLigands endpoint, resulting in omitWater not honored
|
||||
- Fix `Volume` and `Isosurface` getBoundingSphere ignoring instances
|
||||
- Fix SSAO half/quarter resolution textures for multi-scale
|
||||
- Add the option to approximate "least obstructed direction" when focusing camera, accessibe via `PluginContext.managers.camera.focusLoci` with `optimizeDirection` option
|
||||
- Camera improvements
|
||||
- Add the option to approximate "least obstructed direction" when focusing camera, accessibe via `PluginContext.managers.camera.focusLoci` with `optimizeDirection` option
|
||||
- Add the option to "zoom out" to entire scene before focusing camera
|
||||
|
||||
## [v5.9.0] - 2026-05-03
|
||||
- Fix edge case when `PluginSpec.animations` is empty
|
||||
|
||||
@@ -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>
|
||||
@@ -555,6 +555,11 @@ export class Viewer {
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -138,8 +138,8 @@ export class Camera implements ICamera {
|
||||
return changed;
|
||||
}
|
||||
|
||||
setState(snapshot: Partial<Camera.Snapshot>, durationMs?: number) {
|
||||
this.transition.apply(snapshot, durationMs);
|
||||
setState(snapshot: Partial<Camera.Snapshot>, durationMs?: number, options?: { keyframes?: CameraTransitionManager.TransitionKeyframes }) {
|
||||
this.transition.apply(snapshot, durationMs, undefined, options);
|
||||
this.stateChanged.next(snapshot);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 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>
|
||||
*/
|
||||
@@ -20,12 +20,18 @@ class CameraTransitionManager {
|
||||
private durationMs = 0;
|
||||
private _source: Camera.Snapshot = Camera.createDefaultSnapshot();
|
||||
private _target: Camera.Snapshot = Camera.createDefaultSnapshot();
|
||||
private _keyframes: CameraTransitionManager.TransitionKeyframes | 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) {
|
||||
apply(
|
||||
to: Partial<Camera.Snapshot>,
|
||||
durationMs: number = 0,
|
||||
transition?: CameraTransitionManager.TransitionFunc,
|
||||
options?: { keyframes?: CameraTransitionManager.TransitionKeyframes },
|
||||
) {
|
||||
if (!this.inTransition || durationMs > 0) {
|
||||
Camera.copySnapshot(this._source, this.camera.state);
|
||||
}
|
||||
@@ -50,6 +56,7 @@ class CameraTransitionManager {
|
||||
|
||||
this.inTransition = true;
|
||||
this.func = transition || CameraTransitionManager.defaultTransition;
|
||||
this._keyframes = options?.keyframes;
|
||||
|
||||
if (!this.inTransition || durationMs > 0) {
|
||||
this.start = this.t;
|
||||
@@ -76,7 +83,7 @@ class CameraTransitionManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.func(this._current, normalized, this._source, this._target);
|
||||
this.func(this._current, normalized, this._source, this._target, { keyframes: this._keyframes });
|
||||
Camera.copySnapshot(this.camera.state, this._current);
|
||||
}
|
||||
|
||||
@@ -86,7 +93,8 @@ class CameraTransitionManager {
|
||||
}
|
||||
|
||||
namespace CameraTransitionManager {
|
||||
export type TransitionFunc = (out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot) => void
|
||||
export type TransitionKeyframes = { t: number, snapshot: Partial<Camera.Snapshot> }[]
|
||||
export type TransitionFunc = (out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot, options?: { keyframes?: TransitionKeyframes }) => void
|
||||
|
||||
const _rotUp = Quat.identity();
|
||||
const _rotDist = Quat.identity();
|
||||
@@ -94,7 +102,52 @@ namespace CameraTransitionManager {
|
||||
const _sourcePosition = Vec3();
|
||||
const _targetPosition = Vec3();
|
||||
|
||||
export function defaultTransition(out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot): void {
|
||||
|
||||
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?: { keyframes?: TransitionKeyframes }): void {
|
||||
let sourcePartial: Partial<Camera.Snapshot> = source_;
|
||||
let targetPartial: Partial<Camera.Snapshot> = target_;
|
||||
|
||||
let tStart = 0;
|
||||
let tEnd = 1;
|
||||
|
||||
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 = keyframes.length - 1; i >= 0; i--) {
|
||||
const keyframe = keyframes[i];
|
||||
if (t_ <= keyframe.t) {
|
||||
targetPartial = keyframe.snapshot;
|
||||
tEnd = keyframe.t;
|
||||
}
|
||||
if (t_ >= keyframe.t) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const t = (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;
|
||||
|
||||
Camera.copySnapshot(out, target);
|
||||
|
||||
// Rotate up
|
||||
|
||||
@@ -53,6 +53,7 @@ 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';
|
||||
|
||||
export const CameraFogParams = {
|
||||
intensity: PD.Numeric(15, { min: 1, max: 100, step: 1 }),
|
||||
@@ -371,7 +372,7 @@ interface Canvas3D {
|
||||
/** performs handleResize on the next animation frame */
|
||||
requestResize(): void
|
||||
/** Focuses camera on scene's bounding sphere, centered and zoomed. */
|
||||
requestCameraReset(options?: { durationMs?: number, snapshot?: Camera.SnapshotProvider }): void
|
||||
requestCameraReset(options?: { durationMs?: number, snapshot?: Camera.SnapshotProvider, keyframes?: CameraTransitionManager.TransitionKeyframes }): void
|
||||
readonly camera: Camera
|
||||
readonly boundingSphere: Readonly<Sphere3D>
|
||||
readonly boundingSphereVisible: Readonly<Sphere3D>
|
||||
@@ -500,6 +501,7 @@ namespace Canvas3D {
|
||||
let cameraResetRequested = false;
|
||||
let nextCameraResetDuration: number | undefined = void 0;
|
||||
let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0;
|
||||
let nextCameraResetKeyframes: CameraTransitionManager.TransitionKeyframes | undefined = void 0;
|
||||
let resizeRequested = false;
|
||||
|
||||
//
|
||||
@@ -882,11 +884,12 @@ namespace Canvas3D {
|
||||
const focus = camera.getFocus(center, radius);
|
||||
const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot;
|
||||
const snapshot = next ? { ...focus, ...next } : focus;
|
||||
camera.setState({ ...snapshot, radiusMax: getSceneRadius() }, duration);
|
||||
camera.setState({ ...snapshot, radiusMax: getSceneRadius() }, duration, { keyframes: nextCameraResetKeyframes });
|
||||
}
|
||||
|
||||
nextCameraResetDuration = void 0;
|
||||
nextCameraResetSnapshot = void 0;
|
||||
nextCameraResetKeyframes = void 0;
|
||||
cameraResetRequested = false;
|
||||
}
|
||||
|
||||
@@ -1253,6 +1256,7 @@ namespace Canvas3D {
|
||||
requestCameraReset: options => {
|
||||
nextCameraResetDuration = options?.durationMs;
|
||||
nextCameraResetSnapshot = options?.snapshot;
|
||||
nextCameraResetKeyframes = options?.keyframes;
|
||||
cameraResetRequested = true;
|
||||
},
|
||||
camera,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { CameraTransitionManager } from '../../mol-canvas3d/camera/transition';
|
||||
import { GraphicsRenderObject } from '../../mol-gl/render-object';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
|
||||
@@ -32,7 +33,11 @@ const DefaultCameraFocusOptions = {
|
||||
};
|
||||
|
||||
export type CameraFocusOptions = typeof DefaultCameraFocusOptions
|
||||
export type CameraFocusLociOptions = CameraFocusOptions & { optimizeDirection?: boolean, optimizeDirectionUp?: Vec3 }
|
||||
export type CameraFocusLociOptions = CameraFocusOptions & {
|
||||
optimizeDirection?: boolean,
|
||||
optimizeDirectionUp?: Vec3,
|
||||
zoomOut?: boolean,
|
||||
}
|
||||
|
||||
export class CameraManager {
|
||||
private boundaryHelper = new BoundaryHelper('98');
|
||||
@@ -101,12 +106,11 @@ export class CameraManager {
|
||||
const positions: { x: number[], y: number[], z: number[] } = { x: [], y: [], z: [] };
|
||||
const t = Vec3();
|
||||
|
||||
const { extraRadius, minRadius, durationMs } = { ...DefaultCameraFocusOptions, ...options };
|
||||
const { extraRadius, minRadius } = { ...DefaultCameraFocusOptions, ...options };
|
||||
const radius = Math.max(sphere.radius + extraRadius, minRadius);
|
||||
|
||||
if (radius <= 1e-3) {
|
||||
this.focusSphere(sphere, options);
|
||||
return;
|
||||
return this.getFocusSphereSnapshot(sphere, options);
|
||||
}
|
||||
|
||||
const entityType = StructureProperties.entity.type;
|
||||
@@ -125,8 +129,7 @@ export class CameraManager {
|
||||
}
|
||||
|
||||
if (positions.x.length === 0) {
|
||||
this.focusSphere(sphere, options);
|
||||
return;
|
||||
return this.getFocusSphereSnapshot(sphere, options);
|
||||
}
|
||||
|
||||
const direction = leastObstructedDirection(positions, {
|
||||
@@ -135,31 +138,59 @@ export class CameraManager {
|
||||
sigma: sphere.radius,
|
||||
});
|
||||
if (!direction) {
|
||||
this.focusSphere(sphere, options);
|
||||
return;
|
||||
return this.getFocusSphereSnapshot(sphere, options);
|
||||
}
|
||||
|
||||
Vec3.negate(direction, direction);
|
||||
const snapshot = canvas3d.camera.getInvariantFocus(sphere.center, radius, options?.up ?? Vec3.unitY, direction);
|
||||
canvas3d.requestCameraReset({ durationMs, snapshot });
|
||||
return canvas3d.camera.getInvariantFocus(sphere.center, radius, options?.up ?? Vec3.unitY, direction);
|
||||
}
|
||||
|
||||
private focusLociBase(loci: Loci | Loci[], options?: Partial<CameraFocusOptions>) {
|
||||
const sphere = this.getFocusSphere(loci);
|
||||
if (sphere) {
|
||||
this.focusSphere(sphere, options);
|
||||
return this.getFocusSphereSnapshot(sphere, options);
|
||||
}
|
||||
}
|
||||
|
||||
focusLoci(loci: Loci | Loci[], options?: Partial<CameraFocusLociOptions>) {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
|
||||
let snapshot: Partial<Camera.Snapshot> | undefined;
|
||||
if (options?.optimizeDirection) {
|
||||
this.focusLociOptimized(loci, {
|
||||
snapshot = this.focusLociOptimized(loci, {
|
||||
...options,
|
||||
up: options.optimizeDirectionUp,
|
||||
});
|
||||
} else {
|
||||
this.focusLociBase(loci, options);
|
||||
snapshot = this.focusLociBase(loci, options);
|
||||
}
|
||||
|
||||
if (!snapshot) return;
|
||||
|
||||
const durationMs = options?.durationMs ?? DefaultCameraFocusOptions.durationMs;
|
||||
|
||||
if (options?.zoomOut) {
|
||||
const sphere = this.plugin.canvas3d.boundingSphere;
|
||||
const mid = this.getFocusSphereSnapshot(sphere, options) as Camera.Snapshot;
|
||||
const current = this.plugin.canvas3d?.camera.getSnapshot()!;
|
||||
|
||||
const distA = Vec3.distance(current.position, mid.position);
|
||||
const distB = Vec3.distance(mid.position, snapshot.position!);
|
||||
|
||||
const t = distA / (distA + distB);
|
||||
const timeFactor = 1 + 3 * Math.min(t, 0.5);
|
||||
|
||||
this.plugin.canvas3d?.requestCameraReset({
|
||||
snapshot,
|
||||
durationMs: timeFactor * durationMs,
|
||||
keyframes: [
|
||||
{ t, snapshot: mid },
|
||||
]
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.plugin.canvas3d.requestCameraReset({ snapshot, durationMs });
|
||||
}
|
||||
|
||||
focusSpheres<T>(xs: ReadonlyArray<T>, sphere: (t: T) => Sphere3D | undefined, options?: Partial<CameraFocusOptions> & { principalAxes?: PrincipalAxes, positionToFlip?: Vec3 }) {
|
||||
@@ -184,21 +215,29 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
focusSphere(sphere: Sphere3D, options?: Partial<CameraFocusOptions> & { principalAxes?: PrincipalAxes, positionToFlip?: Vec3 }) {
|
||||
const { canvas3d } = this.plugin;
|
||||
if (!canvas3d) return;
|
||||
|
||||
const { extraRadius, minRadius, durationMs } = { ...DefaultCameraFocusOptions, ...options };
|
||||
const radius = Math.max(sphere.radius + extraRadius, minRadius);
|
||||
const snapshot = this.getFocusSphereSnapshot(sphere, options);
|
||||
if (!snapshot) return;
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
canvas3d.requestCameraReset({ durationMs: options?.durationMs ?? DefaultCameraFocusOptions.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 }) {
|
||||
|
||||
Reference in New Issue
Block a user