trackball.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /**
  2. * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author David Sehnal <david.sehnal@gmail.com>
  6. *
  7. * This code has been modified from https://github.com/mrdoob/three.js/,
  8. * copyright (c) 2010-2018 three.js authors. MIT License
  9. */
  10. import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
  11. import { Viewport } from '../camera/util';
  12. import InputObserver, { DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys } from '../../mol-util/input/input-observer';
  13. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  14. import { Camera } from '../camera';
  15. import { absMax } from '../../mol-math/misc';
  16. import { Binding } from '../../mol-util/binding';
  17. const B = ButtonsType;
  18. const M = ModifiersKeys;
  19. const Trigger = Binding.Trigger;
  20. export const DefaultTrackballBindings = {
  21. dragRotate: Binding([Trigger(B.Flag.Primary, M.create())], 'Rotate', 'Drag using ${triggers}'),
  22. dragRotateZ: Binding([Trigger(B.Flag.Primary, M.create({ shift: true }))], 'Rotate around z-axis', 'Drag using ${triggers}'),
  23. dragPan: Binding([Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true }))], 'Pan', 'Drag using ${triggers}'),
  24. dragZoom: Binding.Empty,
  25. dragFocus: Binding([Trigger(B.Flag.Forth, M.create())], 'Focus', 'Drag using ${triggers}'),
  26. dragFocusZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Focus and zoom', 'Drag using ${triggers}'),
  27. scrollZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Zoom', 'Scroll using ${triggers}'),
  28. scrollFocus: Binding([Trigger(B.Flag.Auxilary, M.create({ shift: true }))], 'Clip', 'Scroll using ${triggers}'),
  29. scrollFocusZoom: Binding.Empty,
  30. };
  31. export const TrackballControlsParams = {
  32. noScroll: PD.Boolean(true, { isHidden: true }),
  33. rotateSpeed: PD.Numeric(3.0, { min: 0.1, max: 10, step: 0.1 }),
  34. zoomSpeed: PD.Numeric(6.0, { min: 0.1, max: 10, step: 0.1 }),
  35. panSpeed: PD.Numeric(0.8, { min: 0.1, max: 5, step: 0.1 }),
  36. spin: PD.Boolean(false, { description: 'Spin the 3D scene around the x-axis in view space' }),
  37. spinSpeed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
  38. staticMoving: PD.Boolean(true, { isHidden: true }),
  39. dynamicDampingFactor: PD.Numeric(0.2, {}, { isHidden: true }),
  40. minDistance: PD.Numeric(0.01, {}, { isHidden: true }),
  41. maxDistance: PD.Numeric(1e150, {}, { isHidden: true }),
  42. bindings: PD.Value(DefaultTrackballBindings, { isHidden: true })
  43. };
  44. export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
  45. export { TrackballControls };
  46. interface TrackballControls {
  47. viewport: Viewport
  48. readonly props: Readonly<TrackballControlsProps>
  49. setProps: (props: Partial<TrackballControlsProps>) => void
  50. update: (t: number) => void
  51. reset: () => void
  52. dispose: () => void
  53. }
  54. namespace TrackballControls {
  55. export function create(input: InputObserver, camera: Camera, props: Partial<TrackballControlsProps> = {}): TrackballControls {
  56. const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props };
  57. const viewport = Viewport();
  58. let disposed = false;
  59. const dragSub = input.drag.subscribe(onDrag);
  60. const interactionEndSub = input.interactionEnd.subscribe(onInteractionEnd);
  61. const wheelSub = input.wheel.subscribe(onWheel);
  62. const pinchSub = input.pinch.subscribe(onPinch);
  63. let _isInteracting = false;
  64. // For internal use
  65. const lastPosition = Vec3();
  66. const _eye = Vec3();
  67. const _rotPrev = Vec2();
  68. const _rotCurr = Vec2();
  69. const _rotLastAxis = Vec3();
  70. let _rotLastAngle = 0;
  71. const _zRotPrev = Vec2();
  72. const _zRotCurr = Vec2();
  73. let _zRotLastAngle = 0;
  74. const _zoomStart = Vec2();
  75. const _zoomEnd = Vec2();
  76. const _focusStart = Vec2();
  77. const _focusEnd = Vec2();
  78. const _panStart = Vec2();
  79. const _panEnd = Vec2();
  80. // Initial values for reseting
  81. const target0 = Vec3.clone(camera.target);
  82. const position0 = Vec3.clone(camera.position);
  83. const up0 = Vec3.clone(camera.up);
  84. const mouseOnScreenVec2 = Vec2();
  85. function getMouseOnScreen(pageX: number, pageY: number) {
  86. return Vec2.set(
  87. mouseOnScreenVec2,
  88. (pageX - viewport.x) / viewport.width,
  89. (pageY - viewport.y) / viewport.height
  90. );
  91. }
  92. const mouseOnCircleVec2 = Vec2();
  93. function getMouseOnCircle(pageX: number, pageY: number) {
  94. return Vec2.set(
  95. mouseOnCircleVec2,
  96. (pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5),
  97. (viewport.height + 2 * (viewport.y - pageY)) / viewport.width // screen.width intentional
  98. );
  99. }
  100. const rotAxis = Vec3();
  101. const rotQuat = Quat();
  102. const rotEyeDir = Vec3();
  103. const rotObjUpDir = Vec3();
  104. const rotObjSideDir = Vec3();
  105. const rotMoveDir = Vec3();
  106. function rotateCamera() {
  107. const dx = _rotCurr[0] - _rotPrev[0];
  108. const dy = _rotCurr[1] - _rotPrev[1];
  109. Vec3.set(rotMoveDir, dx, dy, 0);
  110. const angle = Vec3.magnitude(rotMoveDir) * p.rotateSpeed;
  111. if (angle) {
  112. Vec3.sub(_eye, camera.position, camera.target);
  113. Vec3.normalize(rotEyeDir, _eye);
  114. Vec3.normalize(rotObjUpDir, camera.up);
  115. Vec3.normalize(rotObjSideDir, Vec3.cross(rotObjSideDir, rotObjUpDir, rotEyeDir));
  116. Vec3.setMagnitude(rotObjUpDir, rotObjUpDir, dy);
  117. Vec3.setMagnitude(rotObjSideDir, rotObjSideDir, dx);
  118. Vec3.add(rotMoveDir, rotObjUpDir, rotObjSideDir);
  119. Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye));
  120. Quat.setAxisAngle(rotQuat, rotAxis, angle);
  121. Vec3.transformQuat(_eye, _eye, rotQuat);
  122. Vec3.transformQuat(camera.up, camera.up, rotQuat);
  123. Vec3.copy(_rotLastAxis, rotAxis);
  124. _rotLastAngle = angle;
  125. } else if (!p.staticMoving && _rotLastAngle) {
  126. _rotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
  127. Vec3.sub(_eye, camera.position, camera.target);
  128. Quat.setAxisAngle(rotQuat, _rotLastAxis, _rotLastAngle);
  129. Vec3.transformQuat(_eye, _eye, rotQuat);
  130. Vec3.transformQuat(camera.up, camera.up, rotQuat);
  131. }
  132. Vec2.copy(_rotPrev, _rotCurr);
  133. }
  134. const zRotQuat = Quat();
  135. function zRotateCamera() {
  136. const dx = _zRotCurr[0] - _zRotPrev[0];
  137. const dy = _zRotCurr[1] - _zRotPrev[1];
  138. const angle = p.rotateSpeed * (-dx + dy) * -0.05;
  139. if (angle) {
  140. Vec3.sub(_eye, camera.position, camera.target);
  141. Quat.setAxisAngle(zRotQuat, _eye, angle);
  142. Vec3.transformQuat(camera.up, camera.up, zRotQuat);
  143. _zRotLastAngle = angle;
  144. } else if (!p.staticMoving && _zRotLastAngle) {
  145. _zRotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
  146. Vec3.sub(_eye, camera.position, camera.target);
  147. Quat.setAxisAngle(zRotQuat, _eye, _zRotLastAngle);
  148. Vec3.transformQuat(camera.up, camera.up, zRotQuat);
  149. }
  150. Vec2.copy(_zRotPrev, _zRotCurr);
  151. }
  152. function zoomCamera() {
  153. const factor = 1.0 + (_zoomEnd[1] - _zoomStart[1]) * p.zoomSpeed;
  154. if (factor !== 1.0 && factor > 0.0) {
  155. Vec3.scale(_eye, _eye, factor);
  156. }
  157. if (p.staticMoving) {
  158. Vec2.copy(_zoomStart, _zoomEnd);
  159. } else {
  160. _zoomStart[1] += (_zoomEnd[1] - _zoomStart[1]) * p.dynamicDampingFactor;
  161. }
  162. }
  163. function focusCamera() {
  164. const factor = (_focusEnd[1] - _focusStart[1]) * p.zoomSpeed;
  165. if (factor !== 0.0) {
  166. const radius = Math.max(1, camera.state.radius + camera.state.radius * factor);
  167. camera.setState({ radius });
  168. }
  169. if (p.staticMoving) {
  170. Vec2.copy(_focusStart, _focusEnd);
  171. } else {
  172. _focusStart[1] += (_focusEnd[1] - _focusStart[1]) * p.dynamicDampingFactor;
  173. }
  174. }
  175. const panMouseChange = Vec2();
  176. const panObjUp = Vec3();
  177. const panOffset = Vec3();
  178. function panCamera() {
  179. Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, _panEnd), _panStart);
  180. if (Vec2.squaredMagnitude(panMouseChange)) {
  181. Vec2.scale(panMouseChange, panMouseChange, Vec3.magnitude(_eye) * p.panSpeed);
  182. Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), camera.up);
  183. Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0]);
  184. Vec3.setMagnitude(panObjUp, camera.up, panMouseChange[1]);
  185. Vec3.add(panOffset, panOffset, panObjUp);
  186. Vec3.add(camera.position, camera.position, panOffset);
  187. Vec3.add(camera.target, camera.target, panOffset);
  188. if (p.staticMoving) {
  189. Vec2.copy(_panStart, _panEnd);
  190. } else {
  191. Vec2.sub(panMouseChange, _panEnd, _panStart);
  192. Vec2.scale(panMouseChange, panMouseChange, p.dynamicDampingFactor);
  193. Vec2.add(_panStart, _panStart, panMouseChange);
  194. }
  195. }
  196. }
  197. /**
  198. * Ensure the distance between object and target is within the min/max distance
  199. * and not too large compared to `camera.state.radiusMax`
  200. */
  201. function checkDistances() {
  202. const maxDistance = Math.min(Math.max(camera.state.radiusMax * 1000, 0.01), p.maxDistance);
  203. if (Vec3.squaredMagnitude(_eye) > maxDistance * maxDistance) {
  204. Vec3.setMagnitude(_eye, _eye, maxDistance);
  205. Vec3.add(camera.position, camera.target, _eye);
  206. Vec2.copy(_zoomStart, _zoomEnd);
  207. Vec2.copy(_focusStart, _focusEnd);
  208. }
  209. if (Vec3.squaredMagnitude(_eye) < p.minDistance * p.minDistance) {
  210. Vec3.setMagnitude(_eye, _eye, p.minDistance);
  211. Vec3.add(camera.position, camera.target, _eye);
  212. Vec2.copy(_zoomStart, _zoomEnd);
  213. Vec2.copy(_focusStart, _focusEnd);
  214. }
  215. }
  216. let lastUpdated = -1;
  217. /** Update the object's position, direction and up vectors */
  218. function update(t: number) {
  219. if (lastUpdated === t) return;
  220. if (p.spin) spin(t - lastUpdated);
  221. Vec3.sub(_eye, camera.position, camera.target);
  222. rotateCamera();
  223. zRotateCamera();
  224. zoomCamera();
  225. focusCamera();
  226. panCamera();
  227. Vec3.add(camera.position, camera.target, _eye);
  228. checkDistances();
  229. if (Vec3.squaredDistance(lastPosition, camera.position) > EPSILON) {
  230. Vec3.copy(lastPosition, camera.position);
  231. }
  232. lastUpdated = t;
  233. }
  234. /** Reset object's vectors and the target vector to their initial values */
  235. function reset() {
  236. Vec3.copy(camera.target, target0);
  237. Vec3.copy(camera.position, position0);
  238. Vec3.copy(camera.up, up0);
  239. Vec3.sub(_eye, camera.position, camera.target);
  240. Vec3.copy(lastPosition, camera.position);
  241. }
  242. // listeners
  243. function onDrag({ pageX, pageY, buttons, modifiers, isStart }: DragInput) {
  244. _isInteracting = true;
  245. const dragRotate = Binding.match(p.bindings.dragRotate, buttons, modifiers);
  246. const dragRotateZ = Binding.match(p.bindings.dragRotateZ, buttons, modifiers);
  247. const dragPan = Binding.match(p.bindings.dragPan, buttons, modifiers);
  248. const dragZoom = Binding.match(p.bindings.dragZoom, buttons, modifiers);
  249. const dragFocus = Binding.match(p.bindings.dragFocus, buttons, modifiers);
  250. const dragFocusZoom = Binding.match(p.bindings.dragFocusZoom, buttons, modifiers);
  251. getMouseOnCircle(pageX, pageY);
  252. getMouseOnScreen(pageX, pageY);
  253. if (isStart) {
  254. if (dragRotate) {
  255. Vec2.copy(_rotCurr, mouseOnCircleVec2);
  256. Vec2.copy(_rotPrev, _rotCurr);
  257. }
  258. if (dragRotateZ) {
  259. Vec2.copy(_zRotCurr, mouseOnCircleVec2);
  260. Vec2.copy(_zRotPrev, _zRotCurr);
  261. }
  262. if (dragZoom || dragFocusZoom) {
  263. Vec2.copy(_zoomStart, mouseOnScreenVec2);
  264. Vec2.copy(_zoomEnd, _zoomStart);
  265. }
  266. if (dragFocus) {
  267. Vec2.copy(_focusStart, mouseOnScreenVec2);
  268. Vec2.copy(_focusEnd, _focusStart);
  269. }
  270. if (dragPan) {
  271. Vec2.copy(_panStart, mouseOnScreenVec2);
  272. Vec2.copy(_panEnd, _panStart);
  273. }
  274. }
  275. if (dragRotate) Vec2.copy(_rotCurr, mouseOnCircleVec2);
  276. if (dragRotateZ) Vec2.copy(_zRotCurr, mouseOnCircleVec2);
  277. if (dragZoom || dragFocusZoom) Vec2.copy(_zoomEnd, mouseOnScreenVec2);
  278. if (dragFocus) Vec2.copy(_focusEnd, mouseOnScreenVec2);
  279. if (dragFocusZoom) {
  280. const dist = Vec3.distance(camera.state.position, camera.state.target);
  281. camera.setState({ radius: dist / 5 });
  282. }
  283. if (dragPan) Vec2.copy(_panEnd, mouseOnScreenVec2);
  284. }
  285. function onInteractionEnd() {
  286. _isInteracting = false;
  287. }
  288. function onWheel({ dx, dy, dz, buttons, modifiers }: WheelInput) {
  289. const delta = absMax(dx, dy, dz);
  290. if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
  291. _zoomEnd[1] += delta * 0.0001;
  292. }
  293. if (Binding.match(p.bindings.scrollFocus, buttons, modifiers)) {
  294. _focusEnd[1] += delta * 0.0001;
  295. }
  296. }
  297. function onPinch({ fraction, buttons, modifiers }: PinchInput) {
  298. if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
  299. _isInteracting = true;
  300. _zoomEnd[1] += (fraction - 1) * 0.1;
  301. }
  302. }
  303. function dispose() {
  304. if (disposed) return;
  305. disposed = true;
  306. dragSub.unsubscribe();
  307. wheelSub.unsubscribe();
  308. pinchSub.unsubscribe();
  309. interactionEndSub.unsubscribe();
  310. }
  311. const _spinSpeed = Vec2.create(0.005, 0);
  312. function spin(deltaT: number) {
  313. const frameSpeed = (p.spinSpeed || 0) / 1000;
  314. _spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
  315. if (!_isInteracting) Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
  316. }
  317. // force an update at start
  318. update(0);
  319. return {
  320. viewport,
  321. get props() { return p as Readonly<TrackballControlsProps>; },
  322. setProps: (props: Partial<TrackballControlsProps>) => {
  323. Object.assign(p, props);
  324. },
  325. update,
  326. reset,
  327. dispose
  328. };
  329. }
  330. }