Skip to content
Snippets Groups Projects
Commit e157993a authored by Alexander Rose's avatar Alexander Rose
Browse files

Merge branch 'master' of https://github.com/molstar/molstar into pr/MadCatX/779

parents 6c7c9afc 2d0b17d9
Branches restore-vertex-array-per-program
No related tags found
No related merge requests found
...@@ -7,9 +7,11 @@ Note that since we don't clearly distinguish between a public and private interf ...@@ -7,9 +7,11 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased] ## [Unreleased]
- Avoid `renderMarkingDepth` for fully transparent renderables - Avoid `renderMarkingDepth` for fully transparent renderables
- Remove `camera.far` doubeling workaround - Remove `camera.far` doubling workaround
- Add `ModifiersKeys.areNone` helper function - Add `ModifiersKeys.areNone` helper function
- Fix rendering issues caused by VAO reuse - Fix rendering issues caused by VAO reuse
- Add "Zoom All", "Orient Axes", "Reset Axes" buttons to the "Reset Camera" button
- Improve trackball move-state handling when key bindings use modifiers
## [v3.33.0] - 2023-04-02 ## [v3.33.0] - 2023-04-02
......
...@@ -45,10 +45,10 @@ export const DefaultTrackballBindings = { ...@@ -45,10 +45,10 @@ export const DefaultTrackballBindings = {
keyMoveDown: Binding([Key('KeyF')], 'Move down', 'Press ${triggers}'), keyMoveDown: Binding([Key('KeyF')], 'Move down', 'Press ${triggers}'),
keyRollLeft: Binding([Key('KeyQ')], 'Roll left', 'Press ${triggers}'), keyRollLeft: Binding([Key('KeyQ')], 'Roll left', 'Press ${triggers}'),
keyRollRight: Binding([Key('KeyE')], 'Roll right', 'Press ${triggers}'), keyRollRight: Binding([Key('KeyE')], 'Roll right', 'Press ${triggers}'),
keyPitchUp: Binding([Key('ArrowUp')], 'Pitch up', 'Press ${triggers}'), keyPitchUp: Binding([Key('ArrowUp', M.create({ shift: true }))], 'Pitch up', 'Press ${triggers}'),
keyPitchDown: Binding([Key('ArrowDown')], 'Pitch down', 'Press ${triggers}'), keyPitchDown: Binding([Key('ArrowDown', M.create({ shift: true }))], 'Pitch down', 'Press ${triggers}'),
keyYawLeft: Binding([Key('ArrowLeft')], 'Yaw left', 'Press ${triggers}'), keyYawLeft: Binding([Key('ArrowLeft', M.create({ shift: true }))], 'Yaw left', 'Press ${triggers}'),
keyYawRight: Binding([Key('ArrowRight')], 'Yaw right', 'Press ${triggers}'), keyYawRight: Binding([Key('ArrowRight', M.create({ shift: true }))], 'Yaw right', 'Press ${triggers}'),
boostMove: Binding([Key('ShiftLeft')], 'Boost move', 'Press ${triggers}'), boostMove: Binding([Key('ShiftLeft')], 'Boost move', 'Press ${triggers}'),
enablePointerLock: Binding([Key('Space', M.create({ control: true }))], 'Enable pointer lock', 'Press ${triggers}'), enablePointerLock: Binding([Key('Space', M.create({ control: true }))], 'Enable pointer lock', 'Press ${triggers}'),
...@@ -679,6 +679,42 @@ namespace TrackballControls { ...@@ -679,6 +679,42 @@ namespace TrackballControls {
function onKeyUp({ modifiers, code, x, y }: KeyInput) { function onKeyUp({ modifiers, code, x, y }: KeyInput) {
if (outsideViewport(x, y)) return; if (outsideViewport(x, y)) return;
let isModifierCode = false;
if (code.startsWith('Alt')) {
isModifierCode = true;
modifiers.alt = true;
} else if (code.startsWith('Shift')) {
isModifierCode = true;
modifiers.shift = true;
} else if (code.startsWith('Control')) {
isModifierCode = true;
modifiers.control = true;
} else if (code.startsWith('Meta')) {
isModifierCode = true;
modifiers.meta = true;
}
const codes = [];
if (isModifierCode) {
if (keyState.moveForward) codes.push(b.keyMoveForward.triggers[0]?.code || '');
if (keyState.moveBack) codes.push(b.keyMoveBack.triggers[0]?.code || '');
if (keyState.moveLeft) codes.push(b.keyMoveLeft.triggers[0]?.code || '');
if (keyState.moveRight) codes.push(b.keyMoveRight.triggers[0]?.code || '');
if (keyState.moveUp) codes.push(b.keyMoveUp.triggers[0]?.code || '');
if (keyState.moveDown) codes.push(b.keyMoveDown.triggers[0]?.code || '');
if (keyState.rollLeft) codes.push(b.keyRollLeft.triggers[0]?.code || '');
if (keyState.rollRight) codes.push(b.keyRollRight.triggers[0]?.code || '');
if (keyState.pitchUp) codes.push(b.keyPitchUp.triggers[0]?.code || '');
if (keyState.pitchDown) codes.push(b.keyPitchDown.triggers[0]?.code || '');
if (keyState.yawLeft) codes.push(b.keyYawLeft.triggers[0]?.code || '');
if (keyState.yawRight) codes.push(b.keyYawRight.triggers[0]?.code || '');
} else {
codes.push(code);
}
for (const code of codes) {
if (Binding.matchKey(b.keyMoveForward, code, modifiers)) { if (Binding.matchKey(b.keyMoveForward, code, modifiers)) {
keyState.moveForward = 0; keyState.moveForward = 0;
} else if (Binding.matchKey(b.keyMoveBack, code, modifiers)) { } else if (Binding.matchKey(b.keyMoveBack, code, modifiers)) {
...@@ -704,6 +740,7 @@ namespace TrackballControls { ...@@ -704,6 +740,7 @@ namespace TrackballControls {
} else if (Binding.matchKey(b.keyYawRight, code, modifiers)) { } else if (Binding.matchKey(b.keyYawRight, code, modifiers)) {
keyState.yawRight = 0; keyState.yawRight = 0;
} }
}
if (Binding.matchKey(b.boostMove, code, modifiers)) { if (Binding.matchKey(b.boostMove, code, modifiers)) {
keyState.boostMove = 0; keyState.boostMove = 0;
...@@ -742,7 +779,7 @@ namespace TrackballControls { ...@@ -742,7 +779,7 @@ namespace TrackballControls {
} }
} }
function onLeave() { function unsetKeyState() {
keyState.moveForward = 0; keyState.moveForward = 0;
keyState.moveBack = 0; keyState.moveBack = 0;
keyState.moveLeft = 0; keyState.moveLeft = 0;
...@@ -758,6 +795,10 @@ namespace TrackballControls { ...@@ -758,6 +795,10 @@ namespace TrackballControls {
keyState.boostMove = 0; keyState.boostMove = 0;
} }
function onLeave() {
unsetKeyState();
}
function dispose() { function dispose() {
if (disposed) return; if (disposed) return;
disposed = true; disposed = true;
......
...@@ -454,6 +454,14 @@ namespace Mat3 { ...@@ -454,6 +454,14 @@ namespace Mat3 {
} }
export const Identity: ReadonlyMat3 = identity(); export const Identity: ReadonlyMat3 = identity();
/** Return the Frobenius inner product of two matrices (= dot product of the flattened matrices).
* Can be used as a measure of similarity between two rotation matrices. */
export function innerProduct(a: Mat3, b: Mat3) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
+ a[3] * b[3] + a[4] * b[4] + a[5] * b[5]
+ a[6] * b[6] + a[7] * b[7] + a[8] * b[8];
}
} }
export { Mat3 }; export { Mat3 };
\ No newline at end of file
...@@ -4,18 +4,22 @@ ...@@ -4,18 +4,22 @@
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Ke Ma <mark.ma@rcsb.org> * @author Ke Ma <mark.ma@rcsb.org>
* @author Adam Midlik <midlik@gmail.com>
*/ */
import { Sphere3D } from '../../mol-math/geometry';
import { PluginContext } from '../../mol-plugin/context';
import { PrincipalAxes } from '../../mol-math/linear-algebra/matrix/principal-axes';
import { Camera } from '../../mol-canvas3d/camera'; import { Camera } from '../../mol-canvas3d/camera';
import { Loci } from '../../mol-model/loci';
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
import { GraphicsRenderObject } from '../../mol-gl/render-object'; import { GraphicsRenderObject } from '../../mol-gl/render-object';
import { StructureElement } from '../../mol-model/structure'; import { Sphere3D } from '../../mol-math/geometry';
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
import { Mat3 } from '../../mol-math/linear-algebra';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3'; 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 } from '../../mol-model/structure';
import { PluginContext } from '../../mol-plugin/context';
import { PluginStateObject } from '../objects';
import { pcaFocus } from './focus-camera/focus-first-residue'; import { pcaFocus } from './focus-camera/focus-first-residue';
import { changeCameraRotation, structureLayingTransform } from './focus-camera/orient-axes';
// TODO: make this customizable somewhere? // TODO: make this customizable somewhere?
const DefaultCameraFocusOptions = { const DefaultCameraFocusOptions = {
...@@ -125,6 +129,26 @@ export class CameraManager { ...@@ -125,6 +129,26 @@ export class CameraManager {
} }
} }
/** Align PCA axes of `structures` (default: all loaded structures) to the screen axes. */
orientAxes(structures?: Structure[], durationMs?: number) {
if (!this.plugin.canvas3d) return;
if (!structures) {
const structCells = this.plugin.state.data.selectQ(q => q.ofType(PluginStateObject.Molecule.Structure));
const rootStructCells = structCells.filter(cell => cell.obj && !cell.transform.transformer.definition.isDecorator && !cell.obj.data.parent);
structures = rootStructCells.map(cell => cell.obj?.data).filter(struct => !!struct) as Structure[];
}
const { rotation } = structureLayingTransform(structures);
const newSnapshot = changeCameraRotation(this.plugin.canvas3d.camera.getSnapshot(), rotation);
this.setSnapshot(newSnapshot, durationMs);
}
/** Align Cartesian axes to the screen axes (X right, Y up). */
resetAxes(durationMs?: number) {
if (!this.plugin.canvas3d) return;
const newSnapshot = changeCameraRotation(this.plugin.canvas3d.camera.getSnapshot(), Mat3.Identity);
this.setSnapshot(newSnapshot, durationMs);
}
setSnapshot(snapshot: Partial<Camera.Snapshot>, durationMs?: number) { setSnapshot(snapshot: Partial<Camera.Snapshot>, durationMs?: number) {
// TODO: setState and requestCameraReset are very similar now: unify them? // TODO: setState and requestCameraReset are very similar now: unify them?
this.plugin.canvas3d?.requestCameraReset({ snapshot, durationMs }); this.plugin.canvas3d?.requestCameraReset({ snapshot, durationMs });
......
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Camera } from '../../../mol-canvas3d/camera';
import { Mat3, Vec3 } from '../../../mol-math/linear-algebra';
import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
import { Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
/** Minimum number of atoms necessary for running PCA.
* If enough atoms cannot be selected, XYZ axes will be used instead of PCA axes. */
const MIN_ATOMS_FOR_PCA = 3;
/** Rotation matrices for the basic rotations by 90 degrees */
export const ROTATION_MATRICES = {
// The order of elements in the matrices in column-wise (F-style)
identity: Mat3.create(1, 0, 0, 0, 1, 0, 0, 0, 1),
rotX90: Mat3.create(1, 0, 0, 0, 0, 1, 0, -1, 0),
rotY90: Mat3.create(0, 0, -1, 0, 1, 0, 1, 0, 0),
rotZ90: Mat3.create(0, 1, 0, -1, 0, 0, 0, 0, 1),
rotX270: Mat3.create(1, 0, 0, 0, 0, -1, 0, 1, 0),
rotY270: Mat3.create(0, 0, 1, 0, 1, 0, -1, 0, 0),
rotZ270: Mat3.create(0, -1, 0, 1, 0, 0, 0, 0, 1),
rotX180: Mat3.create(1, 0, 0, 0, -1, 0, 0, 0, -1),
rotY180: Mat3.create(-1, 0, 0, 0, 1, 0, 0, 0, -1),
rotZ180: Mat3.create(-1, 0, 0, 0, -1, 0, 0, 0, 1),
};
/** Return transformation which will align the PCA axes of an atomic structure
* (or multiple structures) to the Cartesian axes x, y, z
* (transformed = rotation * (coords - origin)).
*
* There are always 4 equally good rotations to do this (4 flips).
* If `referenceRotation` is provided, select the one nearest to `referenceRotation`.
* Otherwise use arbitrary rules to ensure the orientation after transform does not depend on the original orientation.
*/
export function structureLayingTransform(structures: Structure[], referenceRotation?: Mat3): { rotation: Mat3, origin: Vec3 } {
const coords = smartSelectCoords(structures, MIN_ATOMS_FOR_PCA);
return layingTransform(coords, referenceRotation);
}
/** Return transformation which will align the PCA axes of a sequence
* of points to the Cartesian axes x, y, z
* (transformed = rotation * (coords - origin)).
*
* `coords` is a flattened array of 3D coordinates (i.e. the first 3 values are x, y, and z of the first point etc.).
*
* There are always 4 equally good rotations to do this (4 flips).
* If `referenceRotation` is provided, select the one nearest to `referenceRotation`.
* Otherwise use arbitrary rules to ensure the orientation after transform does not depend on the original orientation.
*/
export function layingTransform(coords: number[], referenceRotation?: Mat3): { rotation: Mat3, origin: Vec3 } {
if (coords.length === 0) {
console.warn('Skipping PCA, no atoms');
return { rotation: ROTATION_MATRICES.identity, origin: Vec3.zero() };
}
const axes = PrincipalAxes.calculateMomentsAxes(coords);
const normAxes = PrincipalAxes.calculateNormalizedAxes(axes);
const R = mat3FromRows(normAxes.dirA, normAxes.dirB, normAxes.dirC);
avoidMirrorRotation(R); // The SVD implementation seems to always provide proper rotation, but just to be sure
const flip = referenceRotation ? minimalFlip(R, referenceRotation) : canonicalFlip(coords, R, axes.origin);
Mat3.mul(R, flip, R);
return { rotation: R, origin: normAxes.origin };
}
/** Try these selection strategies until having at least `minAtoms` atoms:
* 1. only trace atoms (e.g. C-alpha and O3')
* 2. all non-hydrogen atoms with exception of water (HOH)
* 3. all atoms
* Return the coordinates in a flattened array (in triples).
* If the total number of atoms is less than `minAtoms`, return only those. */
function smartSelectCoords(structures: Structure[], minAtoms: number): number[] {
let coords: number[];
coords = selectCoords(structures, { onlyTrace: true });
if (coords.length >= 3 * minAtoms) return coords;
coords = selectCoords(structures, { skipHydrogens: true, skipWater: true });
if (coords.length >= 3 * minAtoms) return coords;
coords = selectCoords(structures, {});
return coords;
}
/** Select coordinates of atoms in `structures` as a flattened array (in triples).
* If `onlyTrace`, include only trace atoms (CA, O3');
* if `skipHydrogens`, skip all hydrogen atoms;
* if `skipWater`, skip all water residues. */
function selectCoords(structures: Structure[], options: { onlyTrace?: boolean, skipHydrogens?: boolean, skipWater?: boolean }): number[] {
const { onlyTrace, skipHydrogens, skipWater } = options;
const { x, y, z, type_symbol, label_comp_id } = StructureProperties.atom;
const coords: number[] = [];
for (const struct of structures) {
const loc = StructureElement.Location.create(struct);
for (const unit of struct.units) {
loc.unit = unit;
const elements = onlyTrace ? unit.polymerElements : unit.elements;
for (let i = 0; i < elements.length; i++) {
loc.element = elements[i];
if (skipHydrogens && type_symbol(loc) === 'H') continue;
if (skipWater && label_comp_id(loc) === 'HOH') continue;
coords.push(x(loc), y(loc), z(loc));
}
}
}
return coords;
}
/** Return a flip around XYZ axes which minimizes the difference between flip*rotation and referenceRotation. */
function minimalFlip(rotation: Mat3, referenceRotation: Mat3): Mat3 {
let bestFlip = ROTATION_MATRICES.identity;
let bestScore = 0; // there will always be at least one positive score
const aux = Mat3();
for (const flip of [ROTATION_MATRICES.identity, ROTATION_MATRICES.rotX180, ROTATION_MATRICES.rotY180, ROTATION_MATRICES.rotZ180]) {
const score = Mat3.innerProduct(Mat3.mul(aux, flip, rotation), referenceRotation);
if (score > bestScore) {
bestFlip = flip;
bestScore = score;
}
}
return bestFlip;
}
/** Return a rotation matrix (flip) that should be applied to `coords` (after being rotated by `rotation`)
* to ensure a deterministic "canonical" rotation.
* There are 4 flips to choose from (one identity and three 180-degree rotations around the X, Y, and Z axes).
* One of these 4 possible results is selected so that:
* 1) starting and ending coordinates tend to be more in front (z > 0), middle more behind (z < 0).
* 2) starting coordinates tend to be more left-top (x < y), ending more right-bottom (x > y).
* These rules are arbitrary, but try to avoid ties for at least some basic symmetries.
* Provided `origin` parameter MUST be the mean of the coordinates, otherwise it will not work!
*/
function canonicalFlip(coords: number[], rotation: Mat3, origin: Vec3): Mat3 {
const pcaX = Vec3.create(Mat3.getValue(rotation, 0, 0), Mat3.getValue(rotation, 0, 1), Mat3.getValue(rotation, 0, 2));
const pcaY = Vec3.create(Mat3.getValue(rotation, 1, 0), Mat3.getValue(rotation, 1, 1), Mat3.getValue(rotation, 1, 2));
const pcaZ = Vec3.create(Mat3.getValue(rotation, 2, 0), Mat3.getValue(rotation, 2, 1), Mat3.getValue(rotation, 2, 2));
const n = Math.floor(coords.length / 3);
const v = Vec3();
let xCum = 0;
let yCum = 0;
let zCum = 0;
for (let i = 0; i < n; i++) {
Vec3.fromArray(v, coords, 3 * i);
Vec3.sub(v, v, origin);
xCum += i * Vec3.dot(v, pcaX);
yCum += i * Vec3.dot(v, pcaY);
zCum += veeSlope(i, n) * Vec3.dot(v, pcaZ);
// Thanks to subtracting `origin` from `coords` the slope functions `i` and `veeSlope(i, n)`
// don't have to have zero sum (can be shifted up or down):
// sum{(slope[i]+shift)*(coords[i]-origin).PCA} =
// = sum{slope[i]*coords[i].PCA - slope[i]*origin.PCA + shift*coords[i].PCA - shift*origin.PCA} =
// = sum{slope[i]*(coords[i]-origin).PCA} + shift*sum{coords[i]-origin}.PCA =
// = sum{slope[i]*(coords[i]-origin).PCA}
}
const wrongFrontBack = zCum < 0;
const wrongLeftTopRightBottom = wrongFrontBack ? xCum + yCum < 0 : xCum - yCum < 0;
if (wrongLeftTopRightBottom && wrongFrontBack) {
return ROTATION_MATRICES.rotY180; // flip around Y = around X then Z
} else if (wrongFrontBack) {
return ROTATION_MATRICES.rotX180; // flip around X
} else if (wrongLeftTopRightBottom) {
return ROTATION_MATRICES.rotZ180; // flip around Z
} else {
return ROTATION_MATRICES.identity; // do not flip
}
}
/** Auxiliary function defined for i in [0, n), linearly decreasing from 0 to n/2
* and then increasing back from n/2 to n, resembling letter V. */
function veeSlope(i: number, n: number) {
const mid = Math.floor(n / 2);
if (i < mid) {
if (n % 2) return mid - i;
else return mid - i - 1;
} else {
return i - mid;
}
}
function mat3FromRows(row0: Vec3, row1: Vec3, row2: Vec3): Mat3 {
const m = Mat3();
Mat3.setValue(m, 0, 0, row0[0]);
Mat3.setValue(m, 0, 1, row0[1]);
Mat3.setValue(m, 0, 2, row0[2]);
Mat3.setValue(m, 1, 0, row1[0]);
Mat3.setValue(m, 1, 1, row1[1]);
Mat3.setValue(m, 1, 2, row1[2]);
Mat3.setValue(m, 2, 0, row2[0]);
Mat3.setValue(m, 2, 1, row2[1]);
Mat3.setValue(m, 2, 2, row2[2]);
return m;
}
/** Check if a rotation matrix includes mirroring and invert Z axis in such case, to ensure a proper rotation (in-place). */
function avoidMirrorRotation(rot: Mat3) {
if (Mat3.determinant(rot) < 0) {
Mat3.setValue(rot, 2, 0, -Mat3.getValue(rot, 2, 0));
Mat3.setValue(rot, 2, 1, -Mat3.getValue(rot, 2, 1));
Mat3.setValue(rot, 2, 2, -Mat3.getValue(rot, 2, 2));
}
}
/** Return a new camera snapshot with the same target and camera distance from the target as `old`
* but with diferent orientation.
* The actual rotation applied to the camera is the inverse of `rotation`,
* which creates the same effect as if `rotation` were applied to the whole scene without moving the camera.
* The rotation is relative to the default camera orientation (not to the current orientation). */
export function changeCameraRotation(old: Camera.Snapshot, rotation: Mat3): Camera.Snapshot {
const cameraRotation = Mat3.invert(Mat3(), rotation);
const dist = Vec3.distance(old.position, old.target);
const relPosition = Vec3.transformMat3(Vec3(), Vec3.create(0, 0, dist), cameraRotation);
const newUp = Vec3.transformMat3(Vec3(), Vec3.create(0, 1, 0), cameraRotation);
const newPosition = Vec3.add(Vec3(), old.target, relPosition);
return { ...old, position: newPosition, up: newUp };
}
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { throttleTime } from 'rxjs';
import { Canvas3DParams } from '../mol-canvas3d/canvas3d'; import { Canvas3DParams } from '../mol-canvas3d/canvas3d';
import { PluginCommands } from '../mol-plugin/commands'; import { PluginCommands } from '../mol-plugin/commands';
import { LeftPanelTabName } from '../mol-plugin/layout'; import { LeftPanelTabName } from '../mol-plugin/layout';
...@@ -13,12 +14,12 @@ import { StateTransform } from '../mol-state'; ...@@ -13,12 +14,12 @@ import { StateTransform } from '../mol-state';
import { ParamDefinition as PD } from '../mol-util/param-definition'; import { ParamDefinition as PD } from '../mol-util/param-definition';
import { PluginUIComponent } from './base'; import { PluginUIComponent } from './base';
import { IconButton, SectionHeader } from './controls/common'; import { IconButton, SectionHeader } from './controls/common';
import { AccountTreeOutlinedSvg, DeleteOutlinedSvg, HelpOutlineSvg, HomeOutlinedSvg, SaveOutlinedSvg, TuneSvg } from './controls/icons';
import { ParameterControls } from './controls/parameters'; import { ParameterControls } from './controls/parameters';
import { StateObjectActions } from './state/actions'; import { StateObjectActions } from './state/actions';
import { RemoteStateSnapshots, StateSnapshots } from './state/snapshots'; import { RemoteStateSnapshots, StateSnapshots } from './state/snapshots';
import { StateTree } from './state/tree'; import { StateTree } from './state/tree';
import { HelpContent } from './viewport/help'; import { HelpContent } from './viewport/help';
import { HomeOutlinedSvg, AccountTreeOutlinedSvg, TuneSvg, HelpOutlineSvg, SaveOutlinedSvg, DeleteOutlinedSvg } from './controls/icons';
export class CustomImportControls extends PluginUIComponent<{ initiallyCollapsed?: boolean }> { export class CustomImportControls extends PluginUIComponent<{ initiallyCollapsed?: boolean }> {
componentDidMount() { componentDidMount() {
...@@ -142,7 +143,7 @@ class FullSettings extends PluginUIComponent { ...@@ -142,7 +143,7 @@ class FullSettings extends PluginUIComponent {
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate()); this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
if (this.plugin.canvas3d) { if (this.plugin.canvas3d) {
this.subscribe(this.plugin.canvas3d.camera.stateChanged, state => { this.subscribe(this.plugin.canvas3d.camera.stateChanged.pipe(throttleTime(500, undefined, { leading: true, trailing: true })), state => {
if (state.radiusMax !== undefined || state.radius !== undefined) { if (state.radiusMax !== undefined || state.radius !== undefined) {
this.forceUpdate(); this.forceUpdate();
} }
......
...@@ -82,6 +82,33 @@ ...@@ -82,6 +82,33 @@
height: 100%; height: 100%;
} }
.msp-hover-box-wrapper {
position: relative;
.msp-hover-box-body {
visibility: hidden;
position: absolute;
right: $row-height + 4px;
top: 0;
width: 100px;
background-color: $default-background;
}
.msp-hover-box-spacer {
visibility: hidden;
position: absolute;
right: $row-height;
top: 0;
width: 4px;
height: $row-height;
}
&:hover .msp-hover-box-body,
&:hover .msp-hover-box-spacer {
visibility: visible;
}
}
.msp-viewport-controls-panel { .msp-viewport-controls-panel {
width: 290px; width: 290px;
top: 0; top: 0;
......
...@@ -3,14 +3,16 @@ ...@@ -3,14 +3,16 @@
* *
* @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
*/ */
import * as React from 'react'; import * as React from 'react';
import { throttleTime } from 'rxjs';
import { PluginCommands } from '../mol-plugin/commands'; import { PluginCommands } from '../mol-plugin/commands';
import { PluginConfig } from '../mol-plugin/config'; import { PluginConfig } from '../mol-plugin/config';
import { ParamDefinition as PD } from '../mol-util/param-definition'; import { ParamDefinition as PD } from '../mol-util/param-definition';
import { PluginUIComponent } from './base'; import { PluginUIComponent } from './base';
import { ControlGroup, IconButton } from './controls/common'; import { Button, ControlGroup, IconButton } from './controls/common';
import { AutorenewSvg, BuildOutlinedSvg, CameraOutlinedSvg, CloseSvg, FullscreenSvg, TuneSvg } from './controls/icons'; import { AutorenewSvg, BuildOutlinedSvg, CameraOutlinedSvg, CloseSvg, FullscreenSvg, TuneSvg } from './controls/icons';
import { ToggleSelectionModeButton } from './structure/selection'; import { ToggleSelectionModeButton } from './structure/selection';
import { ViewportCanvas } from './viewport/canvas'; import { ViewportCanvas } from './viewport/canvas';
...@@ -19,19 +21,23 @@ import { SimpleSettingsControl } from './viewport/simple-settings'; ...@@ -19,19 +21,23 @@ import { SimpleSettingsControl } from './viewport/simple-settings';
interface ViewportControlsState { interface ViewportControlsState {
isSettingsExpanded: boolean, isSettingsExpanded: boolean,
isScreenshotExpanded: boolean isScreenshotExpanded: boolean,
isCameraResetEnabled: boolean
} }
interface ViewportControlsProps { interface ViewportControlsProps {
} }
export class ViewportControls extends PluginUIComponent<ViewportControlsProps, ViewportControlsState> { export class ViewportControls extends PluginUIComponent<ViewportControlsProps, ViewportControlsState> {
private allCollapsedState: ViewportControlsState = { private allCollapsedState = {
isSettingsExpanded: false, isSettingsExpanded: false,
isScreenshotExpanded: false isScreenshotExpanded: false,
}; };
state = { ...this.allCollapsedState } as ViewportControlsState; state: ViewportControlsState = {
...this.allCollapsedState,
isCameraResetEnabled: true,
};
resetCamera = () => { resetCamera = () => {
PluginCommands.Camera.Reset(this.plugin, {}); PluginCommands.Camera.Reset(this.plugin, {});
...@@ -39,7 +45,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V ...@@ -39,7 +45,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
private toggle(panel: keyof ViewportControlsState) { private toggle(panel: keyof ViewportControlsState) {
return (e?: React.MouseEvent<HTMLButtonElement>) => { return (e?: React.MouseEvent<HTMLButtonElement>) => {
this.setState({ ...this.allCollapsedState, [panel]: !this.state[panel] }); this.setState(old => ({ ...old, ...this.allCollapsedState, [panel]: !this.state[panel] }));
e?.currentTarget.blur(); e?.currentTarget.blur();
}; };
} }
...@@ -67,9 +73,19 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V ...@@ -67,9 +73,19 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
this.plugin.helpers.viewportScreenshot?.download(); this.plugin.helpers.viewportScreenshot?.download();
}; };
enableCameraReset = (enable: boolean) => {
this.setState(old => ({ ...old, isCameraResetEnabled: enable }));
};
componentDidMount() { componentDidMount() {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate()); this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate()); this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
if (this.plugin.canvas3d) {
this.subscribe(
this.plugin.canvas3d.camera.stateChanged.pipe(throttleTime(500, undefined, { leading: true, trailing: true })),
snapshot => this.enableCameraReset(snapshot.radius !== 0 && snapshot.radiusMax !== 0)
);
}
} }
icon(icon: React.FC, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) { icon(icon: React.FC, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) {
...@@ -79,9 +95,29 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V ...@@ -79,9 +95,29 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
render() { render() {
return <div className={'msp-viewport-controls'}> return <div className={'msp-viewport-controls'}>
<div className='msp-viewport-controls-buttons'> <div className='msp-viewport-controls-buttons'>
<div> <div className='msp-hover-box-wrapper'>
<div className='msp-semi-transparent-background' /> <div className='msp-semi-transparent-background' />
{this.icon(AutorenewSvg, this.resetCamera, 'Reset Camera')} {this.icon(AutorenewSvg, this.resetCamera, 'Reset Zoom')}
<div className='msp-hover-box-body'>
<div className='msp-flex-column'>
<div className='msp-flex-row'>
<Button onClick={() => this.resetCamera()} disabled={!this.state.isCameraResetEnabled} title='Set camera zoom to fit the visible scene into view'>
Reset Zoom
</Button>
</div>
<div className='msp-flex-row'>
<Button onClick={() => PluginCommands.Camera.OrientAxes(this.plugin)} disabled={!this.state.isCameraResetEnabled} title='Align principal component axes of the loaded structures to the screen axes (“lay flat”)'>
Orient Axes
</Button>
</div>
<div className='msp-flex-row'>
<Button onClick={() => PluginCommands.Camera.ResetAxes(this.plugin)} disabled={!this.state.isCameraResetEnabled} title='Align Cartesian axes to the screen axes'>
Reset Axes
</Button>
</div>
</div>
</div>
<div className='msp-hover-box-spacer'></div>
</div> </div>
<div> <div>
<div className='msp-semi-transparent-background' /> <div className='msp-semi-transparent-background' />
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
*/ */
import { produce } from 'immer'; import { produce } from 'immer';
import { throttleTime } from 'rxjs';
import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d'; import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
import { PluginCommands } from '../../mol-plugin/commands'; import { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig } from '../../mol-plugin/config'; import { PluginConfig } from '../../mol-plugin/config';
...@@ -26,7 +27,7 @@ export class SimpleSettingsControl extends PluginUIComponent { ...@@ -26,7 +27,7 @@ export class SimpleSettingsControl extends PluginUIComponent {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate()); this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
this.subscribe(this.plugin.canvas3d!.camera.stateChanged, state => { this.subscribe(this.plugin.canvas3d!.camera.stateChanged.pipe(throttleTime(500, undefined, { leading: true, trailing: true })), state => {
if (state.radiusMax !== undefined || state.radius !== undefined) { if (state.radiusMax !== undefined || state.radius !== undefined) {
this.forceUpdate(); this.forceUpdate();
} }
......
/** /**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* *
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
*/ */
import { PluginContext } from '../../../mol-plugin/context'; import { PluginContext } from '../../../mol-plugin/context';
...@@ -11,6 +12,8 @@ export function registerDefault(ctx: PluginContext) { ...@@ -11,6 +12,8 @@ export function registerDefault(ctx: PluginContext) {
Reset(ctx); Reset(ctx);
Focus(ctx); Focus(ctx);
SetSnapshot(ctx); SetSnapshot(ctx);
OrientAxes(ctx);
ResetAxes(ctx);
} }
export function Reset(ctx: PluginContext) { export function Reset(ctx: PluginContext) {
...@@ -31,3 +34,15 @@ export function Focus(ctx: PluginContext) { ...@@ -31,3 +34,15 @@ export function Focus(ctx: PluginContext) {
ctx.events.canvas3d.settingsUpdated.next(void 0); ctx.events.canvas3d.settingsUpdated.next(void 0);
}); });
} }
export function OrientAxes(ctx: PluginContext) {
PluginCommands.Camera.OrientAxes.subscribe(ctx, ({ structures, durationMs }) => {
ctx.managers.camera.orientAxes(structures, durationMs);
});
}
export function ResetAxes(ctx: PluginContext) {
PluginCommands.Camera.ResetAxes.subscribe(ctx, ({ durationMs }) => {
ctx.managers.camera.resetAxes(durationMs);
});
}
/** /**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* *
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
*/ */
import { Camera } from '../mol-canvas3d/camera'; import { Camera } from '../mol-canvas3d/camera';
...@@ -10,7 +11,7 @@ import { PluginCommand } from './command'; ...@@ -10,7 +11,7 @@ import { PluginCommand } from './command';
import { StateTransform, State, StateAction } from '../mol-state'; import { StateTransform, State, StateAction } from '../mol-state';
import { Canvas3DProps } from '../mol-canvas3d/canvas3d'; import { Canvas3DProps } from '../mol-canvas3d/canvas3d';
import { PluginLayoutStateProps } from './layout'; import { PluginLayoutStateProps } from './layout';
import { StructureElement } from '../mol-model/structure'; import { Structure, StructureElement } from '../mol-model/structure';
import { PluginState } from './state'; import { PluginState } from './state';
import { PluginToast } from './util/toast'; import { PluginToast } from './util/toast';
import { Vec3 } from '../mol-math/linear-algebra'; import { Vec3 } from '../mol-math/linear-algebra';
...@@ -62,7 +63,9 @@ export const PluginCommands = { ...@@ -62,7 +63,9 @@ export const PluginCommands = {
Camera: { Camera: {
Reset: PluginCommand<{ durationMs?: number, snapshot?: Partial<Camera.Snapshot> }>(), Reset: PluginCommand<{ durationMs?: number, snapshot?: Partial<Camera.Snapshot> }>(),
SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(), SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(),
Focus: PluginCommand<{ center: Vec3, radius: number, durationMs?: number }>() Focus: PluginCommand<{ center: Vec3, radius: number, durationMs?: number }>(),
OrientAxes: PluginCommand<{ structures?: Structure[], durationMs?: number }>(),
ResetAxes: PluginCommand<{ durationMs?: number }>(),
}, },
Canvas3D: { Canvas3D: {
SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void) }>(), SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void) }>(),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment