/** * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose */ /* * This code has been modified from https://github.com/mrdoob/three.js/, * copyright (c) 2010-2018 three.js authors. MIT License */ import { Quat, Vec2, Vec3, EPSILON } from 'mol-math/linear-algebra'; import { cameraLookAt, Viewport } from '../camera/util'; import InputObserver, { DragInput, WheelInput, ButtonsFlag, PinchInput } from 'mol-util/input/input-observer'; import { Object3D } from 'mol-gl/object3d'; export const DefaultTrackballControlsProps = { noScroll: true, target: [0, 0, 0] as Vec3, rotateSpeed: 3.0, zoomSpeed: 4.0, panSpeed: 0.8, staticMoving: true, dynamicDampingFactor: 0.2, minDistance: 0, maxDistance: Infinity } export type TrackballControlsProps = Partial interface TrackballControls { viewport: Viewport target: Vec3 dynamicDampingFactor: number rotateSpeed: number zoomSpeed: number panSpeed: number update: () => void reset: () => void dispose: () => void } namespace TrackballControls { export function create (input: InputObserver, object: Object3D, props: TrackballControlsProps = {}): TrackballControls { const p = { ...DefaultTrackballControlsProps, ...props } const viewport: Viewport = { x: 0, y: 0, width: 0, height: 0 } const target: Vec3 = Vec3.clone(p.target) let { rotateSpeed, zoomSpeed, panSpeed } = p let { staticMoving, dynamicDampingFactor } = p let { minDistance, maxDistance } = p let disposed = false const dragSub = input.drag.subscribe(onDrag) const wheelSub = input.wheel.subscribe(onWheel) const pinchSub = input.pinch.subscribe(onPinch) // For internal use const lastPosition = Vec3.zero() const _eye = Vec3.zero() const _movePrev = Vec2.zero() const _moveCurr = Vec2.zero() const _lastAxis = Vec3.zero() let _lastAngle = 0 const _zoomStart = Vec2.zero() const _zoomEnd = Vec2.zero() let _touchZoomDistanceStart = 0 let _touchZoomDistanceEnd = 0 const _panStart = Vec2.zero() const _panEnd = Vec2.zero() // Initial values for reseting const target0 = Vec3.clone(target) const position0 = Vec3.clone(object.position) const up0 = Vec3.clone(object.up) const mouseOnScreenVec2 = Vec2.zero() function getMouseOnScreen(pageX: number, pageY: number) { Vec2.set( mouseOnScreenVec2, (pageX - viewport.x) / viewport.width, (pageY - viewport.y) / viewport.height ); return mouseOnScreenVec2; } const mouseOnCircleVec2 = Vec2.zero() function getMouseOnCircle(pageX: number, pageY: number) { Vec2.set( mouseOnCircleVec2, ((pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5)), ((viewport.height + 2 * (viewport.y - pageY)) / viewport.width) // screen.width intentional ); return mouseOnCircleVec2; } const rotAxis = Vec3.zero() const rotQuat = Quat.zero() const rotEyeDir = Vec3.zero() const rotObjUpDir = Vec3.zero() const rotObjSideDir = Vec3.zero() const rotMoveDir = Vec3.zero() function rotateCamera() { Vec3.set(rotMoveDir, _moveCurr[0] - _movePrev[0], _moveCurr[1] - _movePrev[1], 0); let angle = Vec3.magnitude(rotMoveDir); if (angle) { Vec3.copy(_eye, object.position) Vec3.sub(_eye, _eye, target) Vec3.normalize(rotEyeDir, Vec3.copy(rotEyeDir, _eye)) Vec3.normalize(rotObjUpDir, Vec3.copy(rotObjUpDir, object.up)) Vec3.normalize(rotObjSideDir, Vec3.cross(rotObjSideDir, rotObjUpDir, rotEyeDir)) Vec3.setMagnitude(rotObjUpDir, rotObjUpDir, _moveCurr[1] - _movePrev[1]) Vec3.setMagnitude(rotObjSideDir, rotObjSideDir, _moveCurr[0] - _movePrev[0]) Vec3.add(rotMoveDir, Vec3.copy(rotMoveDir, rotObjUpDir), rotObjSideDir) Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye)) angle *= rotateSpeed; Quat.setAxisAngle(rotQuat, rotAxis, angle ) Vec3.transformQuat(_eye, _eye, rotQuat) Vec3.transformQuat(object.up, object.up, rotQuat) Vec3.copy(_lastAxis, rotAxis) _lastAngle = angle; } else if (!staticMoving && _lastAngle) { _lastAngle *= Math.sqrt(1.0 - dynamicDampingFactor); Vec3.sub(_eye, Vec3.copy(_eye, object.position), target) Quat.setAxisAngle(rotQuat, _lastAxis, _lastAngle) Vec3.transformQuat(_eye, _eye, rotQuat) Vec3.transformQuat(object.up, object.up, rotQuat) } Vec2.copy(_movePrev, _moveCurr) } function zoomCamera () { const factor = 1.0 + (_zoomEnd[1] - _zoomStart[1]) * zoomSpeed if (factor !== 1.0 && factor > 0.0) { Vec3.scale(_eye, _eye, factor) } if (staticMoving) { Vec2.copy(_zoomStart, _zoomEnd) } else { _zoomStart[1] += (_zoomEnd[1] - _zoomStart[1]) * dynamicDampingFactor } } const panMouseChange = Vec2.zero() const panObjUp = Vec3.zero() const panOffset = Vec3.zero() function panCamera() { Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, _panEnd), _panStart) if (Vec2.squaredMagnitude(panMouseChange)) { Vec2.scale(panMouseChange, panMouseChange, Vec3.magnitude(_eye) * panSpeed) Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), object.up) Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0]) Vec3.setMagnitude(panObjUp, object.up, panMouseChange[1]) Vec3.add(panOffset, panOffset, panObjUp) Vec3.add(object.position, object.position, panOffset) Vec3.add(target, target, panOffset) if (staticMoving) { Vec2.copy(_panStart, _panEnd) } else { Vec2.sub(panMouseChange, _panEnd, _panStart) Vec2.scale(panMouseChange, panMouseChange, dynamicDampingFactor) Vec2.add(_panStart, _panStart, panMouseChange) } } } /** Ensure the distance between object and target is within the min/max distance */ function checkDistances() { if (Vec3.squaredMagnitude(_eye) > maxDistance * maxDistance) { Vec3.setMagnitude(_eye, _eye, maxDistance) Vec3.add(object.position, target, _eye) Vec2.copy(_zoomStart, _zoomEnd) } if (Vec3.squaredMagnitude(_eye) < minDistance * minDistance) { Vec3.setMagnitude(_eye, _eye, minDistance) Vec3.add(object.position, target, _eye) Vec2.copy(_zoomStart, _zoomEnd) } } /** Update the object's position, direction and up vectors */ function update() { Vec3.sub(_eye, object.position, target) rotateCamera() zoomCamera() panCamera() Vec3.add(object.position, target, _eye) checkDistances() cameraLookAt(object.position, object.up, object.direction, target) if (Vec3.squaredDistance(lastPosition, object.position) > EPSILON.Value) { Vec3.copy(lastPosition, object.position) } } /** Reset object's vectors and the target vector to their initial values */ function reset() { Vec3.copy(target, target0) Vec3.copy(object.position, position0) Vec3.copy(object.up, up0) Vec3.sub(_eye, object.position, target) cameraLookAt(object.position, object.up, object.direction, target) Vec3.copy(lastPosition, object.position) } // listeners function onDrag({ pageX, pageY, buttons, modifiers, isStart }: DragInput) { if (isStart) { if (buttons === ButtonsFlag.Primary) { Vec2.copy(_moveCurr, getMouseOnCircle(pageX, pageY)) Vec2.copy(_movePrev, _moveCurr) } else if (buttons === ButtonsFlag.Auxilary) { Vec2.copy(_zoomStart, getMouseOnScreen(pageX, pageY)) Vec2.copy(_zoomEnd, _zoomStart) } else if (buttons === ButtonsFlag.Secondary) { Vec2.copy(_panStart, getMouseOnScreen(pageX, pageY)) Vec2.copy(_panEnd, _panStart) } } if (buttons === ButtonsFlag.Primary) { Vec2.copy(_movePrev, _moveCurr) Vec2.copy(_moveCurr, getMouseOnCircle(pageX, pageY)) } else if (buttons === ButtonsFlag.Auxilary) { Vec2.copy(_zoomEnd, getMouseOnScreen(pageX, pageY)) } else if (buttons === ButtonsFlag.Secondary) { Vec2.copy(_panEnd, getMouseOnScreen(pageX, pageY)) } } function onWheel({ dy }: WheelInput) { _zoomStart[1] -= dy } function onPinch({ distance, isStart }: PinchInput) { if (isStart) { _touchZoomDistanceStart = distance } _touchZoomDistanceEnd = distance const factor = (_touchZoomDistanceStart / _touchZoomDistanceEnd) * zoomSpeed _touchZoomDistanceStart = _touchZoomDistanceEnd; Vec3.scale(_eye, _eye, factor) } function dispose() { if (disposed) return disposed = true dragSub.unsubscribe() wheelSub.unsubscribe() pinchSub.unsubscribe() } // force an update at start update(); return { viewport, target, get dynamicDampingFactor() { return dynamicDampingFactor }, set dynamicDampingFactor(value: number ) { dynamicDampingFactor = value }, get rotateSpeed() { return rotateSpeed }, set rotateSpeed(value: number ) { rotateSpeed = value }, get zoomSpeed() { return zoomSpeed }, set zoomSpeed(value: number ) { zoomSpeed = value }, get panSpeed() { return panSpeed }, set panSpeed(value: number ) { panSpeed = value }, update, reset, dispose } } } export default TrackballControls