UNPKG

molstar

Version:

A comprehensive macromolecular library.

217 lines (216 loc) 10.3 kB
/** * Copyright (c) 2018-2024 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 Jason Pattle <jpattle.exscientia.co.uk> */ import { Loci } from '../../../mol-model/loci'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { PluginBehavior } from '../behavior'; import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer'; import { Binding } from '../../../mol-util/binding'; import { PluginCommands } from '../../commands'; import { CameraHelperAxis, isCameraAxesLoci } from '../../../mol-canvas3d/helper/camera-helper'; import { Vec3 } from '../../../mol-math/linear-algebra'; const B = ButtonsType; const M = ModifiersKeys; const Trigger = Binding.Trigger; const Key = Binding.TriggerKey; export const DefaultClickResetCameraOnEmpty = Binding([ Trigger(B.Flag.Primary, M.create()), Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true })) ], 'Reset camera focus', 'Click on nothing using ${triggers}'); export const DefaultClickResetCameraOnEmptySelectMode = Binding([ Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true })) ], 'Reset camera focus (Selection Mode)', 'Click on nothing using ${triggers}'); export const DefaultFocusLociBindings = { clickCenterFocus: Binding([ Trigger(B.Flag.Primary, M.create()), Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true })) ], 'Camera center and focus', 'Click element using ${triggers}'), clickCenterFocusSelectMode: Binding([ Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true })) ], 'Camera center and focus (Selection Mode)', 'Click element using ${triggers}'), clickResetCameraOnEmpty: DefaultClickResetCameraOnEmpty, clickResetCameraOnEmptySelectMode: DefaultClickResetCameraOnEmptySelectMode, }; const FocusLociParams = { minRadius: PD.Numeric(8, { min: 1, max: 50, step: 1 }), extraRadius: PD.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the bounding-sphere radius of the Loci' }), durationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'Camera transition duration' }), bindings: PD.Value(DefaultFocusLociBindings, { isHidden: true }), }; export const FocusLoci = PluginBehavior.create({ name: 'camera-focus-loci', category: 'interaction', ctor: class extends PluginBehavior.Handler { register() { this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => { var _a, _b; if (!this.ctx.canvas3d) return; const binding = this.ctx.selectionMode ? this.params.bindings.clickCenterFocusSelectMode : this.params.bindings.clickCenterFocus; const resetBinding = this.ctx.selectionMode ? ((_a = this.params.bindings.clickResetCameraOnEmptySelectMode) !== null && _a !== void 0 ? _a : DefaultClickResetCameraOnEmptySelectMode) : ((_b = this.params.bindings.clickResetCameraOnEmpty) !== null && _b !== void 0 ? _b : DefaultClickResetCameraOnEmpty); if (Loci.isEmpty(current.loci) && Binding.match(resetBinding, button, modifiers)) { PluginCommands.Camera.Reset(this.ctx, {}); return; } if (Binding.match(binding, button, modifiers)) { const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity); this.ctx.managers.camera.focusLoci(loci, this.params); } }); } }, params: () => FocusLociParams, display: { name: 'Camera Focus Loci on Canvas' } }); export const CameraAxisHelper = PluginBehavior.create({ name: 'camera-axis-helper', category: 'interaction', ctor: class extends PluginBehavior.Handler { register() { let lastPlane = CameraHelperAxis.None; let state = 0; this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current }) => { if (!this.ctx.canvas3d || !isCameraAxesLoci(current.loci)) return; const axis = current.loci.elements[0].groupId; if (axis === CameraHelperAxis.None) { lastPlane = CameraHelperAxis.None; state = 0; return; } const { camera } = this.ctx.canvas3d; let dir, up; if (axis >= CameraHelperAxis.X && axis <= CameraHelperAxis.Z) { lastPlane = CameraHelperAxis.None; state = 0; const d = Vec3.sub(Vec3(), camera.target, camera.position); const c = Vec3.cross(Vec3(), d, camera.up); up = Vec3(); up[axis - 1] = 1; dir = Vec3.cross(Vec3(), up, c); if (Vec3.magnitude(dir) === 0) dir = d; } else { if (lastPlane === axis) { state = (state + 1) % 2; } else { lastPlane = axis; state = 0; } if (axis === CameraHelperAxis.XY) { up = state ? Vec3.unitX : Vec3.unitY; dir = Vec3.negUnitZ; } else if (axis === CameraHelperAxis.XZ) { up = state ? Vec3.unitX : Vec3.unitZ; dir = Vec3.negUnitY; } else { up = state ? Vec3.unitY : Vec3.unitZ; dir = Vec3.negUnitX; } } this.ctx.canvas3d.requestCameraReset({ snapshot: (scene, camera) => camera.getInvariantFocus(scene.boundingSphereVisible.center, scene.boundingSphereVisible.radius, up, dir) }); }); } }, params: () => ({}), display: { name: 'Camera Axis Helper' } }); const DefaultCameraControlsBindings = { keySpinAnimation: Binding([Key('I')], 'Spin Animation', 'Press ${triggers}'), keyRockAnimation: Binding([Key('O')], 'Rock Animation', 'Press ${triggers}'), keyToggleFlyMode: Binding([Key('Space', M.create({ shift: true }))], 'Toggle Fly Mode', 'Press ${triggers}'), keyResetView: Binding([Key('T')], 'Reset View', 'Press ${triggers}'), keyGlobalIllumination: Binding([Key('G')], 'Global Illumination', 'Press ${triggers}'), }; const CameraControlsParams = { bindings: PD.Value(DefaultCameraControlsBindings, { isHidden: true }), }; export const CameraControls = PluginBehavior.create({ name: 'camera-controls', category: 'interaction', ctor: class extends PluginBehavior.Handler { register() { this.subscribeObservable(this.ctx.behaviors.interaction.key, ({ code, key, modifiers }) => { var _a; if (!this.ctx.canvas3d) return; // include defaults for backwards state compatibility const b = { ...DefaultCameraControlsBindings, ...this.params.bindings }; const tp = this.ctx.canvas3d.props.trackball; const ip = this.ctx.canvas3d.props.illumination; if (Binding.matchKey(b.keySpinAnimation, code, modifiers, key)) { const name = tp.animate.name !== 'spin' ? 'spin' : 'off'; if (name === 'off') { this.ctx.canvas3d.setProps({ trackball: { animate: { name, params: {} } } }); } else { this.ctx.canvas3d.setProps({ trackball: { animate: { name, params: { speed: 1 } } } }); } } if (Binding.matchKey(b.keyRockAnimation, code, modifiers, key)) { const name = tp.animate.name !== 'rock' ? 'rock' : 'off'; if (name === 'off') { this.ctx.canvas3d.setProps({ trackball: { animate: { name, params: {} } } }); } else { this.ctx.canvas3d.setProps({ trackball: { animate: { name, params: { speed: 0.3, angle: 10 } } } }); } } if (Binding.matchKey(b.keyToggleFlyMode, code, modifiers, key)) { const flyMode = !tp.flyMode; this.ctx.canvas3d.setProps({ trackball: { flyMode } }); if ((_a = this.ctx.canvas3dContext) === null || _a === void 0 ? void 0 : _a.canvas) { this.ctx.canvas3dContext.canvas.style.cursor = flyMode ? 'crosshair' : 'unset'; } } if (Binding.matchKey(b.keyResetView, code, modifiers, key)) { PluginCommands.Camera.Reset(this.ctx, {}); } if (Binding.matchKey(b.keyGlobalIllumination, code, modifiers, key)) { this.ctx.canvas3d.setProps({ illumination: { ...ip, enabled: !ip.enabled, } }); } }); } }, params: () => CameraControlsParams, display: { name: 'Camera Controls on Canvas' } });