Skip to content
Snippets Groups Projects
Unverified Commit 0b2889bb authored by Alexander Rose's avatar Alexander Rose Committed by GitHub
Browse files

Merge branch 'master' into ntc-tube-missing-atoms

parents 1cf6cbf8 2994caf4
No related branches found
No related tags found
No related merge requests found
Showing
with 454 additions and 69 deletions
......@@ -7,9 +7,12 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
- Avoid `renderMarkingDepth` for fully transparent renderables
- Remove `camera.far` doubeling workaround
- Remove `camera.far` doubling workaround
- Add `ModifiersKeys.areNone` helper function
- Do not render NtC tube segments unless all required atoms are present in the structure
- 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
......
......@@ -45,10 +45,10 @@ export const DefaultTrackballBindings = {
keyMoveDown: Binding([Key('KeyF')], 'Move down', 'Press ${triggers}'),
keyRollLeft: Binding([Key('KeyQ')], 'Roll left', 'Press ${triggers}'),
keyRollRight: Binding([Key('KeyE')], 'Roll right', 'Press ${triggers}'),
keyPitchUp: Binding([Key('ArrowUp')], 'Pitch up', 'Press ${triggers}'),
keyPitchDown: Binding([Key('ArrowDown')], 'Pitch down', 'Press ${triggers}'),
keyYawLeft: Binding([Key('ArrowLeft')], 'Yaw left', 'Press ${triggers}'),
keyYawRight: Binding([Key('ArrowRight')], 'Yaw right', 'Press ${triggers}'),
keyPitchUp: Binding([Key('ArrowUp', M.create({ shift: true }))], 'Pitch up', 'Press ${triggers}'),
keyPitchDown: Binding([Key('ArrowDown', M.create({ shift: true }))], 'Pitch down', 'Press ${triggers}'),
keyYawLeft: Binding([Key('ArrowLeft', M.create({ shift: true }))], 'Yaw left', 'Press ${triggers}'),
keyYawRight: Binding([Key('ArrowRight', M.create({ shift: true }))], 'Yaw right', 'Press ${triggers}'),
boostMove: Binding([Key('ShiftLeft')], 'Boost move', 'Press ${triggers}'),
enablePointerLock: Binding([Key('Space', M.create({ control: true }))], 'Enable pointer lock', 'Press ${triggers}'),
......@@ -679,6 +679,42 @@ namespace TrackballControls {
function onKeyUp({ modifiers, code, x, y }: KeyInput) {
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)) {
keyState.moveForward = 0;
} else if (Binding.matchKey(b.keyMoveBack, code, modifiers)) {
......@@ -704,6 +740,7 @@ namespace TrackballControls {
} else if (Binding.matchKey(b.keyYawRight, code, modifiers)) {
keyState.yawRight = 0;
}
}
if (Binding.matchKey(b.boostMove, code, modifiers)) {
keyState.boostMove = 0;
......@@ -742,7 +779,7 @@ namespace TrackballControls {
}
}
function onLeave() {
function unsetKeyState() {
keyState.moveForward = 0;
keyState.moveBack = 0;
keyState.moveLeft = 0;
......@@ -758,6 +795,10 @@ namespace TrackballControls {
keyState.boostMove = 0;
}
function onLeave() {
unsetKeyState();
}
function dispose() {
if (disposed) return;
disposed = true;
......
......@@ -53,7 +53,7 @@ describe('renderer', () => {
scene.commit();
expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 4 : 5);
expect(ctx.stats.resourceCounts.texture).toBe(9);
expect(ctx.stats.resourceCounts.vertexArray).toBe(ctx.extensions.vertexArrayObject ? 1 : 0);
expect(ctx.stats.resourceCounts.vertexArray).toBe(ctx.extensions.vertexArrayObject ? 6 : 0);
expect(ctx.stats.resourceCounts.program).toBe(6);
expect(ctx.stats.resourceCounts.shader).toBe(12);
......
......@@ -144,7 +144,10 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
elementsBuffer = resources.elements(elements.ref.value);
}
let vertexArray: VertexArray | null = vertexArrayObject ? resources.vertexArray(programs, attributeBuffers, elementsBuffer) : null;
const vertexArrays: Record<string, VertexArray | null> = {};
for (const k of renderVariants) {
vertexArrays[k] = vertexArrayObject ? resources.vertexArray(programs[k], attributeBuffers, elementsBuffer) : null;
}
let drawCount: number = values.drawCount.ref.value;
let instanceCount: number = values.instanceCount.ref.value;
......@@ -170,6 +173,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
program.setUniforms(uniformValueEntries);
program.bindTextures(textures, sharedTexturesCount);
} else {
const vertexArray = vertexArrays[variant];
if (program.id !== state.currentProgramId || program.id !== currentProgramId ||
materialId === -1 || materialId !== state.currentMaterialId
) {
......@@ -291,9 +295,12 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
}
if (valueChanges.attributes || valueChanges.defines || valueChanges.elements) {
// console.log('program/defines or buffers changed, update vao');
// console.log('program/defines or buffers changed, update vaos');
for (const k of renderVariants) {
const vertexArray = vertexArrays[k];
if (vertexArray) vertexArray.destroy();
vertexArray = vertexArrayObject ? resources.vertexArray(programs, attributeBuffers, elementsBuffer) : null;
vertexArrays[k] = vertexArrayObject ? resources.vertexArray(programs[k], attributeBuffers, elementsBuffer) : null;
}
}
for (let i = 0, il = textures.length; i < il; ++i) {
......@@ -341,8 +348,9 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
if (!destroyed) {
for (const k of renderVariants) {
programs[k].destroy();
}
const vertexArray = vertexArrays[k];
if (vertexArray) vertexArray.destroy();
}
textures.forEach(([k, texture]) => {
// lifetime of textures with kind 'texture' is defined externally
if (schema[k].kind !== 'texture') texture.destroy();
......
......@@ -4,7 +4,7 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ProgramProps, createProgram, Program, Programs } from './program';
import { ProgramProps, createProgram, Program } from './program';
import { ShaderType, createShader, Shader, ShaderProps } from './shader';
import { GLRenderingContext } from './compat';
import { Framebuffer, createFramebuffer } from './framebuffer';
......@@ -60,7 +60,7 @@ export interface WebGLResources {
shader: (type: ShaderType, source: string) => Shader
texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => Texture,
cubeTexture: (faces: CubeFaces, mipmaps: boolean, onload?: () => void) => Texture,
vertexArray: (programs: Programs, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => VertexArray,
vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => VertexArray,
getByteCounts: () => ByteCounts
......@@ -142,8 +142,8 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
cubeTexture: (faces: CubeFaces, mipmaps: boolean, onload?: () => void) => {
return wrap('cubeTexture', createCubeTexture(gl, faces, mipmaps, onload));
},
vertexArray: (programs: Programs, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => {
return wrap('vertexArray', createVertexArray(gl, extensions, programs, attributeBuffers, elementsBuffer));
vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => {
return wrap('vertexArray', createVertexArray(gl, extensions, program, attributeBuffers, elementsBuffer));
},
getByteCounts: () => {
......
......@@ -4,7 +4,7 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Programs } from './program';
import { Program } from './program';
import { ElementsBuffer, AttributeBuffers } from './buffer';
import { WebGLExtensions } from './extensions';
import { idFactory } from '../../mol-util/id-factory';
......@@ -41,7 +41,7 @@ export interface VertexArray {
destroy: () => void
}
export function createVertexArray(gl: GLRenderingContext, extensions: WebGLExtensions, programs: Programs, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer): VertexArray {
export function createVertexArray(gl: GLRenderingContext, extensions: WebGLExtensions, program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer): VertexArray {
const id = getNextVertexArrayId();
let vertexArray = getVertexArray(extensions);
let vertexArrayObject = getVertexArrayObject(extensions);
......@@ -49,7 +49,7 @@ export function createVertexArray(gl: GLRenderingContext, extensions: WebGLExten
function update() {
vertexArrayObject.bindVertexArray(vertexArray);
if (elementsBuffer) elementsBuffer.bind();
for (const p of Object.values(programs)) p.bindAttributes(attributeBuffers);
program.bindAttributes(attributeBuffers);
vertexArrayObject.bindVertexArray(null);
}
......
......@@ -454,6 +454,14 @@ namespace Mat3 {
}
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 };
\ No newline at end of file
......@@ -4,18 +4,22 @@
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @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 { Loci } from '../../mol-model/loci';
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
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 { 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 { changeCameraRotation, structureLayingTransform } from './focus-camera/orient-axes';
// TODO: make this customizable somewhere?
const DefaultCameraFocusOptions = {
......@@ -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) {
// TODO: setState and requestCameraReset are very similar now: unify them?
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 @@
*/
import * as React from 'react';
import { throttleTime } from 'rxjs';
import { Canvas3DParams } from '../mol-canvas3d/canvas3d';
import { PluginCommands } from '../mol-plugin/commands';
import { LeftPanelTabName } from '../mol-plugin/layout';
......@@ -13,12 +14,12 @@ import { StateTransform } from '../mol-state';
import { ParamDefinition as PD } from '../mol-util/param-definition';
import { PluginUIComponent } from './base';
import { IconButton, SectionHeader } from './controls/common';
import { AccountTreeOutlinedSvg, DeleteOutlinedSvg, HelpOutlineSvg, HomeOutlinedSvg, SaveOutlinedSvg, TuneSvg } from './controls/icons';
import { ParameterControls } from './controls/parameters';
import { StateObjectActions } from './state/actions';
import { RemoteStateSnapshots, StateSnapshots } from './state/snapshots';
import { StateTree } from './state/tree';
import { HelpContent } from './viewport/help';
import { HomeOutlinedSvg, AccountTreeOutlinedSvg, TuneSvg, HelpOutlineSvg, SaveOutlinedSvg, DeleteOutlinedSvg } from './controls/icons';
export class CustomImportControls extends PluginUIComponent<{ initiallyCollapsed?: boolean }> {
componentDidMount() {
......@@ -142,7 +143,7 @@ class FullSettings extends PluginUIComponent {
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
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) {
this.forceUpdate();
}
......
......@@ -82,6 +82,33 @@
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 {
width: 290px;
top: 0;
......
......@@ -3,14 +3,16 @@
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
*/
import * as React from 'react';
import { throttleTime } from 'rxjs';
import { PluginCommands } from '../mol-plugin/commands';
import { PluginConfig } from '../mol-plugin/config';
import { ParamDefinition as PD } from '../mol-util/param-definition';
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 { ToggleSelectionModeButton } from './structure/selection';
import { ViewportCanvas } from './viewport/canvas';
......@@ -19,19 +21,23 @@ import { SimpleSettingsControl } from './viewport/simple-settings';
interface ViewportControlsState {
isSettingsExpanded: boolean,
isScreenshotExpanded: boolean
isScreenshotExpanded: boolean,
isCameraResetEnabled: boolean
}
interface ViewportControlsProps {
}
export class ViewportControls extends PluginUIComponent<ViewportControlsProps, ViewportControlsState> {
private allCollapsedState: ViewportControlsState = {
private allCollapsedState = {
isSettingsExpanded: false,
isScreenshotExpanded: false
isScreenshotExpanded: false,
};
state = { ...this.allCollapsedState } as ViewportControlsState;
state: ViewportControlsState = {
...this.allCollapsedState,
isCameraResetEnabled: true,
};
resetCamera = () => {
PluginCommands.Camera.Reset(this.plugin, {});
......@@ -39,7 +45,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
private toggle(panel: keyof ViewportControlsState) {
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();
};
}
......@@ -67,9 +73,19 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
this.plugin.helpers.viewportScreenshot?.download();
};
enableCameraReset = (enable: boolean) => {
this.setState(old => ({ ...old, isCameraResetEnabled: enable }));
};
componentDidMount() {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => 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) {
......@@ -79,9 +95,29 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
render() {
return <div className={'msp-viewport-controls'}>
<div className='msp-viewport-controls-buttons'>
<div>
<div className='msp-hover-box-wrapper'>
<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 className='msp-semi-transparent-background' />
......
......@@ -6,6 +6,7 @@
*/
import { produce } from 'immer';
import { throttleTime } from 'rxjs';
import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig } from '../../mol-plugin/config';
......@@ -26,7 +27,7 @@ export class SimpleSettingsControl extends PluginUIComponent {
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) {
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 Adam Midlik <midlik@gmail.com>
*/
import { PluginContext } from '../../../mol-plugin/context';
......@@ -11,6 +12,8 @@ export function registerDefault(ctx: PluginContext) {
Reset(ctx);
Focus(ctx);
SetSnapshot(ctx);
OrientAxes(ctx);
ResetAxes(ctx);
}
export function Reset(ctx: PluginContext) {
......@@ -31,3 +34,15 @@ export function Focus(ctx: PluginContext) {
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 Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
*/
import { Camera } from '../mol-canvas3d/camera';
......@@ -10,7 +11,7 @@ import { PluginCommand } from './command';
import { StateTransform, State, StateAction } from '../mol-state';
import { Canvas3DProps } from '../mol-canvas3d/canvas3d';
import { PluginLayoutStateProps } from './layout';
import { StructureElement } from '../mol-model/structure';
import { Structure, StructureElement } from '../mol-model/structure';
import { PluginState } from './state';
import { PluginToast } from './util/toast';
import { Vec3 } from '../mol-math/linear-algebra';
......@@ -62,7 +63,9 @@ export const PluginCommands = {
Camera: {
Reset: PluginCommand<{ durationMs?: number, snapshot?: Partial<Camera.Snapshot> }>(),
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: {
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