trackball.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889
  1. /**
  2. * Copyright (c) 2018-2023 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, GestureInput, KeyInput, MoveInput } from '../../mol-util/input/input-observer';
  13. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  14. import { Camera } from '../camera';
  15. import { absMax, degToRad } from '../../mol-math/misc';
  16. import { Binding } from '../../mol-util/binding';
  17. import { Scene } from '../../mol-gl/scene';
  18. const B = ButtonsType;
  19. const M = ModifiersKeys;
  20. const Trigger = Binding.Trigger;
  21. const Key = Binding.TriggerKey;
  22. export const DefaultTrackballBindings = {
  23. dragRotate: Binding([Trigger(B.Flag.Primary, M.create())], 'Rotate', 'Drag using ${triggers}'),
  24. dragRotateZ: Binding([Trigger(B.Flag.Primary, M.create({ shift: true, control: true }))], 'Rotate around z-axis (roll)', 'Drag using ${triggers}'),
  25. dragPan: Binding([
  26. Trigger(B.Flag.Secondary, M.create()),
  27. Trigger(B.Flag.Primary, M.create({ control: true }))
  28. ], 'Pan', 'Drag using ${triggers}'),
  29. dragZoom: Binding.Empty,
  30. dragFocus: Binding([Trigger(B.Flag.Forth, M.create())], 'Focus', 'Drag using ${triggers}'),
  31. dragFocusZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Focus and zoom', 'Drag using ${triggers}'),
  32. scrollZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Zoom', 'Scroll using ${triggers}'),
  33. scrollFocus: Binding([Trigger(B.Flag.Auxilary, M.create({ shift: true }))], 'Clip', 'Scroll using ${triggers}'),
  34. scrollFocusZoom: Binding.Empty,
  35. keyMoveForward: Binding([Key('KeyW')], 'Move forward', 'Press ${triggers}'),
  36. keyMoveBack: Binding([Key('KeyS')], 'Move back', 'Press ${triggers}'),
  37. keyMoveLeft: Binding([Key('KeyA')], 'Move left', 'Press ${triggers}'),
  38. keyMoveRight: Binding([Key('KeyD')], 'Move right', 'Press ${triggers}'),
  39. keyMoveUp: Binding([Key('KeyR')], 'Move up', 'Press ${triggers}'),
  40. keyMoveDown: Binding([Key('KeyF')], 'Move down', 'Press ${triggers}'),
  41. keyRollLeft: Binding([Key('KeyQ')], 'Roll left', 'Press ${triggers}'),
  42. keyRollRight: Binding([Key('KeyE')], 'Roll right', 'Press ${triggers}'),
  43. keyPitchUp: Binding([Key('ArrowUp', M.create({ shift: true }))], 'Pitch up', 'Press ${triggers}'),
  44. keyPitchDown: Binding([Key('ArrowDown', M.create({ shift: true }))], 'Pitch down', 'Press ${triggers}'),
  45. keyYawLeft: Binding([Key('ArrowLeft', M.create({ shift: true }))], 'Yaw left', 'Press ${triggers}'),
  46. keyYawRight: Binding([Key('ArrowRight', M.create({ shift: true }))], 'Yaw right', 'Press ${triggers}'),
  47. boostMove: Binding([Key('ShiftLeft')], 'Boost move', 'Press ${triggers}'),
  48. enablePointerLock: Binding([Key('Space', M.create({ control: true }))], 'Enable pointer lock', 'Press ${triggers}'),
  49. };
  50. export const TrackballControlsParams = {
  51. noScroll: PD.Boolean(true, { isHidden: true }),
  52. rotateSpeed: PD.Numeric(5.0, { min: 1, max: 10, step: 1 }),
  53. zoomSpeed: PD.Numeric(7.0, { min: 1, max: 15, step: 1 }),
  54. panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
  55. moveSpeed: PD.Numeric(0.75, { min: 0.1, max: 3, step: 0.1 }),
  56. boostMoveFactor: PD.Numeric(5.0, { min: 0.1, max: 10, step: 0.1 }),
  57. flyMode: PD.Boolean(false),
  58. animate: PD.MappedStatic('off', {
  59. off: PD.EmptyGroup(),
  60. spin: PD.Group({
  61. speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
  62. }, { description: 'Spin the 3D scene around the x-axis in view space' }),
  63. rock: PD.Group({
  64. speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }),
  65. angle: PD.Numeric(10, { min: 0, max: 90, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
  66. }, { description: 'Rock the 3D scene around the x-axis in view space' })
  67. }),
  68. staticMoving: PD.Boolean(true, { isHidden: true }),
  69. dynamicDampingFactor: PD.Numeric(0.2, {}, { isHidden: true }),
  70. minDistance: PD.Numeric(0.01, {}, { isHidden: true }),
  71. maxDistance: PD.Numeric(1e150, {}, { isHidden: true }),
  72. gestureScaleFactor: PD.Numeric(1, {}, { isHidden: true }),
  73. maxWheelDelta: PD.Numeric(0.02, {}, { isHidden: true }),
  74. bindings: PD.Value(DefaultTrackballBindings, { isHidden: true }),
  75. /**
  76. * minDistance = minDistanceFactor * boundingSphere.radius + minDistancePadding
  77. * maxDistance = max(maxDistanceFactor * boundingSphere.radius, maxDistanceMin)
  78. */
  79. autoAdjustMinMaxDistance: PD.MappedStatic('on', {
  80. off: PD.EmptyGroup(),
  81. on: PD.Group({
  82. minDistanceFactor: PD.Numeric(0),
  83. minDistancePadding: PD.Numeric(5),
  84. maxDistanceFactor: PD.Numeric(10),
  85. maxDistanceMin: PD.Numeric(20)
  86. })
  87. }, { isHidden: true })
  88. };
  89. export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
  90. export { TrackballControls };
  91. interface TrackballControls {
  92. readonly viewport: Viewport
  93. readonly isAnimating: boolean
  94. readonly isMoving: boolean
  95. readonly props: Readonly<TrackballControlsProps>
  96. setProps: (props: Partial<TrackballControlsProps>) => void
  97. start: (t: number) => void
  98. update: (t: number) => void
  99. reset: () => void
  100. dispose: () => void
  101. }
  102. namespace TrackballControls {
  103. export function create(input: InputObserver, camera: Camera, scene: Scene, props: Partial<TrackballControlsProps> = {}): TrackballControls {
  104. const p: TrackballControlsProps = {
  105. ...PD.getDefaultValues(TrackballControlsParams),
  106. ...props,
  107. // include default bindings for backwards state compatibility
  108. bindings: { ...DefaultTrackballBindings, ...props.bindings }
  109. };
  110. const b = p.bindings;
  111. const viewport = Viewport.clone(camera.viewport);
  112. let disposed = false;
  113. const dragSub = input.drag.subscribe(onDrag);
  114. const interactionEndSub = input.interactionEnd.subscribe(onInteractionEnd);
  115. const wheelSub = input.wheel.subscribe(onWheel);
  116. const pinchSub = input.pinch.subscribe(onPinch);
  117. const gestureSub = input.gesture.subscribe(onGesture);
  118. const keyDownSub = input.keyDown.subscribe(onKeyDown);
  119. const keyUpSub = input.keyUp.subscribe(onKeyUp);
  120. const moveSub = input.move.subscribe(onMove);
  121. const lockSub = input.lock.subscribe(onLock);
  122. const leaveSub = input.leave.subscribe(onLeave);
  123. let _isInteracting = false;
  124. // For internal use
  125. const lastPosition = Vec3();
  126. const _eye = Vec3();
  127. const _rotPrev = Vec2();
  128. const _rotCurr = Vec2();
  129. const _rotLastAxis = Vec3();
  130. let _rotLastAngle = 0;
  131. const _rollPrev = Vec2();
  132. const _rollCurr = Vec2();
  133. let _rollLastAngle = 0;
  134. let _pitchLastAngle = 0;
  135. let _yawLastAngle = 0;
  136. const _zoomStart = Vec2();
  137. const _zoomEnd = Vec2();
  138. const _focusStart = Vec2();
  139. const _focusEnd = Vec2();
  140. const _panStart = Vec2();
  141. const _panEnd = Vec2();
  142. // Initial values for reseting
  143. const target0 = Vec3.clone(camera.target);
  144. const position0 = Vec3.clone(camera.position);
  145. const up0 = Vec3.clone(camera.up);
  146. const mouseOnScreenVec2 = Vec2();
  147. function getMouseOnScreen(pageX: number, pageY: number) {
  148. return Vec2.set(
  149. mouseOnScreenVec2,
  150. (pageX - viewport.x) / viewport.width,
  151. (pageY - viewport.y) / viewport.height
  152. );
  153. }
  154. const mouseOnCircleVec2 = Vec2();
  155. function getMouseOnCircle(pageX: number, pageY: number) {
  156. return Vec2.set(
  157. mouseOnCircleVec2,
  158. (pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5),
  159. (viewport.height + 2 * (viewport.y - pageY)) / viewport.width // viewport.width intentional
  160. );
  161. }
  162. function getRotateFactor() {
  163. const aspectRatio = input.width / input.height;
  164. return p.rotateSpeed * input.pixelRatio * aspectRatio;
  165. }
  166. const rotAxis = Vec3();
  167. const rotQuat = Quat();
  168. const rotEyeDir = Vec3();
  169. const rotObjUpDir = Vec3();
  170. const rotObjSideDir = Vec3();
  171. const rotMoveDir = Vec3();
  172. function rotateCamera() {
  173. const dx = _rotCurr[0] - _rotPrev[0];
  174. const dy = _rotCurr[1] - _rotPrev[1];
  175. Vec3.set(rotMoveDir, dx, dy, 0);
  176. const angle = Vec3.magnitude(rotMoveDir) * getRotateFactor();
  177. if (angle) {
  178. Vec3.sub(_eye, camera.position, camera.target);
  179. Vec3.normalize(rotEyeDir, _eye);
  180. Vec3.normalize(rotObjUpDir, camera.up);
  181. Vec3.normalize(rotObjSideDir, Vec3.cross(rotObjSideDir, rotObjUpDir, rotEyeDir));
  182. Vec3.setMagnitude(rotObjUpDir, rotObjUpDir, dy);
  183. Vec3.setMagnitude(rotObjSideDir, rotObjSideDir, dx);
  184. Vec3.add(rotMoveDir, rotObjUpDir, rotObjSideDir);
  185. Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye));
  186. Quat.setAxisAngle(rotQuat, rotAxis, angle);
  187. Vec3.transformQuat(_eye, _eye, rotQuat);
  188. Vec3.transformQuat(camera.up, camera.up, rotQuat);
  189. Vec3.copy(_rotLastAxis, rotAxis);
  190. _rotLastAngle = angle;
  191. } else if (!p.staticMoving && _rotLastAngle) {
  192. _rotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
  193. Vec3.sub(_eye, camera.position, camera.target);
  194. Quat.setAxisAngle(rotQuat, _rotLastAxis, _rotLastAngle);
  195. Vec3.transformQuat(_eye, _eye, rotQuat);
  196. Vec3.transformQuat(camera.up, camera.up, rotQuat);
  197. }
  198. Vec2.copy(_rotPrev, _rotCurr);
  199. }
  200. const rollQuat = Quat();
  201. const rollDir = Vec3();
  202. function rollCamera() {
  203. const k = (keyState.rollRight - keyState.rollLeft) / 45;
  204. const dx = (_rollCurr[0] - _rollPrev[0]) * -Math.sign(_rollCurr[1]);
  205. const dy = (_rollCurr[1] - _rollPrev[1]) * -Math.sign(_rollCurr[0]);
  206. const angle = -p.rotateSpeed * (-dx + dy) + k;
  207. if (angle) {
  208. Vec3.normalize(rollDir, _eye);
  209. Quat.setAxisAngle(rollQuat, rollDir, angle);
  210. Vec3.transformQuat(camera.up, camera.up, rollQuat);
  211. _rollLastAngle = angle;
  212. } else if (!p.staticMoving && _rollLastAngle) {
  213. _rollLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
  214. Vec3.normalize(rollDir, _eye);
  215. Quat.setAxisAngle(rollQuat, rollDir, _rollLastAngle);
  216. Vec3.transformQuat(camera.up, camera.up, rollQuat);
  217. }
  218. Vec2.copy(_rollPrev, _rollCurr);
  219. }
  220. const pitchQuat = Quat();
  221. const pitchDir = Vec3();
  222. function pitchCamera() {
  223. const m = (keyState.pitchUp - keyState.pitchDown) / (p.flyMode ? 360 : 90);
  224. const angle = -p.rotateSpeed * m;
  225. if (angle) {
  226. Vec3.cross(pitchDir, _eye, camera.up);
  227. Vec3.normalize(pitchDir, pitchDir);
  228. Quat.setAxisAngle(pitchQuat, pitchDir, angle);
  229. Vec3.transformQuat(_eye, _eye, pitchQuat);
  230. Vec3.transformQuat(camera.up, camera.up, pitchQuat);
  231. _pitchLastAngle = angle;
  232. } else if (!p.staticMoving && _pitchLastAngle) {
  233. _pitchLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
  234. Vec3.cross(pitchDir, _eye, camera.up);
  235. Vec3.normalize(pitchDir, pitchDir);
  236. Quat.setAxisAngle(pitchQuat, pitchDir, _pitchLastAngle);
  237. Vec3.transformQuat(_eye, _eye, pitchQuat);
  238. Vec3.transformQuat(camera.up, camera.up, pitchQuat);
  239. }
  240. }
  241. const yawQuat = Quat();
  242. const yawDir = Vec3();
  243. function yawCamera() {
  244. const m = (keyState.yawRight - keyState.yawLeft) / (p.flyMode ? 360 : 90);
  245. const angle = -p.rotateSpeed * m;
  246. if (angle) {
  247. Vec3.normalize(yawDir, camera.up);
  248. Quat.setAxisAngle(yawQuat, yawDir, angle);
  249. Vec3.transformQuat(_eye, _eye, yawQuat);
  250. Vec3.transformQuat(camera.up, camera.up, yawQuat);
  251. _yawLastAngle = angle;
  252. } else if (!p.staticMoving && _yawLastAngle) {
  253. _yawLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
  254. Vec3.normalize(yawDir, camera.up);
  255. Quat.setAxisAngle(yawQuat, yawDir, _yawLastAngle);
  256. Vec3.transformQuat(_eye, _eye, yawQuat);
  257. Vec3.transformQuat(camera.up, camera.up, yawQuat);
  258. }
  259. }
  260. function zoomCamera() {
  261. const factor = 1.0 + (_zoomEnd[1] - _zoomStart[1]) * p.zoomSpeed;
  262. if (factor !== 1.0 && factor > 0.0) {
  263. Vec3.scale(_eye, _eye, factor);
  264. }
  265. if (p.staticMoving) {
  266. Vec2.copy(_zoomStart, _zoomEnd);
  267. } else {
  268. _zoomStart[1] += (_zoomEnd[1] - _zoomStart[1]) * p.dynamicDampingFactor;
  269. }
  270. }
  271. function focusCamera() {
  272. const factor = (_focusEnd[1] - _focusStart[1]) * p.zoomSpeed;
  273. if (factor !== 0.0) {
  274. const radius = Math.max(1, camera.state.radius + camera.state.radius * factor);
  275. camera.setState({ radius });
  276. }
  277. if (p.staticMoving) {
  278. Vec2.copy(_focusStart, _focusEnd);
  279. } else {
  280. _focusStart[1] += (_focusEnd[1] - _focusStart[1]) * p.dynamicDampingFactor;
  281. }
  282. }
  283. const panMouseChange = Vec2();
  284. const panObjUp = Vec3();
  285. const panOffset = Vec3();
  286. function panCamera() {
  287. Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, _panEnd), _panStart);
  288. if (Vec2.squaredMagnitude(panMouseChange)) {
  289. const factor = input.pixelRatio * p.panSpeed;
  290. panMouseChange[0] *= (1 / camera.zoom) * camera.viewport.width * factor;
  291. panMouseChange[1] *= (1 / camera.zoom) * camera.viewport.height * factor;
  292. Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), camera.up);
  293. Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0]);
  294. Vec3.setMagnitude(panObjUp, camera.up, panMouseChange[1]);
  295. Vec3.add(panOffset, panOffset, panObjUp);
  296. Vec3.add(camera.position, camera.position, panOffset);
  297. Vec3.add(camera.target, camera.target, panOffset);
  298. if (p.staticMoving) {
  299. Vec2.copy(_panStart, _panEnd);
  300. } else {
  301. Vec2.sub(panMouseChange, _panEnd, _panStart);
  302. Vec2.scale(panMouseChange, panMouseChange, p.dynamicDampingFactor);
  303. Vec2.add(_panStart, _panStart, panMouseChange);
  304. }
  305. }
  306. }
  307. const keyState = {
  308. moveUp: 0, moveDown: 0, moveLeft: 0, moveRight: 0, moveForward: 0, moveBack: 0,
  309. pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0,
  310. boostMove: 0,
  311. };
  312. const moveDir = Vec3();
  313. const moveEye = Vec3();
  314. function moveCamera(deltaT: number) {
  315. Vec3.sub(moveEye, camera.position, camera.target);
  316. const minDistance = Math.max(camera.state.minNear, p.minDistance);
  317. Vec3.setMagnitude(moveEye, moveEye, minDistance);
  318. const moveSpeed = deltaT * (60 / 1000) * p.moveSpeed * (keyState.boostMove === 1 ? p.boostMoveFactor : 1);
  319. if (keyState.moveForward === 1) {
  320. Vec3.normalize(moveDir, moveEye);
  321. Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
  322. if (p.flyMode || input.pointerLock) {
  323. Vec3.sub(camera.target, camera.position, moveEye);
  324. }
  325. }
  326. if (keyState.moveBack === 1) {
  327. Vec3.normalize(moveDir, moveEye);
  328. Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
  329. if (p.flyMode || input.pointerLock) {
  330. Vec3.sub(camera.target, camera.position, moveEye);
  331. }
  332. }
  333. if (keyState.moveLeft === 1) {
  334. Vec3.cross(moveDir, moveEye, camera.up);
  335. Vec3.normalize(moveDir, moveDir);
  336. if (p.flyMode || input.pointerLock) {
  337. Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
  338. Vec3.sub(camera.target, camera.position, moveEye);
  339. } else {
  340. Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
  341. Vec3.sub(camera.target, camera.position, _eye);
  342. }
  343. }
  344. if (keyState.moveRight === 1) {
  345. Vec3.cross(moveDir, moveEye, camera.up);
  346. Vec3.normalize(moveDir, moveDir);
  347. if (p.flyMode || input.pointerLock) {
  348. Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
  349. Vec3.sub(camera.target, camera.position, moveEye);
  350. } else {
  351. Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
  352. Vec3.sub(camera.target, camera.position, _eye);
  353. }
  354. }
  355. if (keyState.moveUp === 1) {
  356. Vec3.normalize(moveDir, camera.up);
  357. if (p.flyMode || input.pointerLock) {
  358. Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
  359. Vec3.sub(camera.target, camera.position, moveEye);
  360. } else {
  361. Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
  362. Vec3.sub(camera.target, camera.position, _eye);
  363. }
  364. }
  365. if (keyState.moveDown === 1) {
  366. Vec3.normalize(moveDir, camera.up);
  367. if (p.flyMode || input.pointerLock) {
  368. Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
  369. Vec3.sub(camera.target, camera.position, moveEye);
  370. } else {
  371. Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
  372. Vec3.sub(camera.target, camera.position, _eye);
  373. }
  374. }
  375. if (p.flyMode || input.pointerLock) {
  376. const cameraDistance = Vec3.distance(camera.position, scene.boundingSphereVisible.center);
  377. camera.setState({ minFar: cameraDistance + scene.boundingSphereVisible.radius });
  378. }
  379. }
  380. /**
  381. * Ensure the distance between object and target is within the min/max distance
  382. * and not too large compared to `camera.state.radiusMax`
  383. */
  384. function checkDistances() {
  385. const maxDistance = Math.min(Math.max(camera.state.radiusMax * 1000, 0.01), p.maxDistance);
  386. if (Vec3.squaredMagnitude(_eye) > maxDistance * maxDistance) {
  387. Vec3.setMagnitude(_eye, _eye, maxDistance);
  388. Vec3.add(camera.position, camera.target, _eye);
  389. Vec2.copy(_zoomStart, _zoomEnd);
  390. Vec2.copy(_focusStart, _focusEnd);
  391. }
  392. if (Vec3.squaredMagnitude(_eye) < p.minDistance * p.minDistance) {
  393. Vec3.setMagnitude(_eye, _eye, p.minDistance);
  394. Vec3.add(camera.position, camera.target, _eye);
  395. Vec2.copy(_zoomStart, _zoomEnd);
  396. Vec2.copy(_focusStart, _focusEnd);
  397. }
  398. }
  399. function outsideViewport(x: number, y: number) {
  400. x *= input.pixelRatio;
  401. y *= input.pixelRatio;
  402. return (
  403. x > viewport.x + viewport.width ||
  404. input.height - y > viewport.y + viewport.height ||
  405. x < viewport.x ||
  406. input.height - y < viewport.y
  407. );
  408. }
  409. let lastUpdated = -1;
  410. /** Update the object's position, direction and up vectors */
  411. function update(t: number) {
  412. if (lastUpdated === t) return;
  413. const deltaT = t - lastUpdated;
  414. if (lastUpdated > 0) {
  415. if (p.animate.name === 'spin') spin(deltaT);
  416. else if (p.animate.name === 'rock') rock(deltaT);
  417. }
  418. Vec3.sub(_eye, camera.position, camera.target);
  419. rotateCamera();
  420. rollCamera();
  421. pitchCamera();
  422. yawCamera();
  423. zoomCamera();
  424. focusCamera();
  425. panCamera();
  426. Vec3.add(camera.position, camera.target, _eye);
  427. checkDistances();
  428. if (lastUpdated > 0) {
  429. // clamp the maximum step size at 15 frames to avoid too big jumps
  430. // TODO: make this a parameter?
  431. moveCamera(Math.min(deltaT, 15 * 1000 / 60));
  432. }
  433. Vec3.sub(_eye, camera.position, camera.target);
  434. checkDistances();
  435. if (Vec3.squaredDistance(lastPosition, camera.position) > EPSILON) {
  436. Vec3.copy(lastPosition, camera.position);
  437. }
  438. lastUpdated = t;
  439. }
  440. /** Reset object's vectors and the target vector to their initial values */
  441. function reset() {
  442. Vec3.copy(camera.target, target0);
  443. Vec3.copy(camera.position, position0);
  444. Vec3.copy(camera.up, up0);
  445. Vec3.sub(_eye, camera.position, camera.target);
  446. Vec3.copy(lastPosition, camera.position);
  447. }
  448. // listeners
  449. function onDrag({ x, y, pageX, pageY, buttons, modifiers, isStart }: DragInput) {
  450. const isOutside = outsideViewport(x, y);
  451. if (isStart && isOutside) return;
  452. if (!isStart && !_isInteracting) return;
  453. _isInteracting = true;
  454. resetRock(); // start rocking from the center after interactions
  455. const dragRotate = Binding.match(b.dragRotate, buttons, modifiers);
  456. const dragRotateZ = Binding.match(b.dragRotateZ, buttons, modifiers);
  457. const dragPan = Binding.match(b.dragPan, buttons, modifiers);
  458. const dragZoom = Binding.match(b.dragZoom, buttons, modifiers);
  459. const dragFocus = Binding.match(b.dragFocus, buttons, modifiers);
  460. const dragFocusZoom = Binding.match(b.dragFocusZoom, buttons, modifiers);
  461. getMouseOnCircle(pageX, pageY);
  462. getMouseOnScreen(pageX, pageY);
  463. const pr = input.pixelRatio;
  464. const vx = (x * pr - viewport.width / 2 - viewport.x) / viewport.width;
  465. const vy = -(input.height - y * pr - viewport.height / 2 - viewport.y) / viewport.height;
  466. if (isStart) {
  467. if (dragRotate) {
  468. Vec2.copy(_rotCurr, mouseOnCircleVec2);
  469. Vec2.copy(_rotPrev, _rotCurr);
  470. }
  471. if (dragRotateZ) {
  472. Vec2.set(_rollCurr, vx, vy);
  473. Vec2.copy(_rollPrev, _rollCurr);
  474. }
  475. if (dragZoom || dragFocusZoom) {
  476. Vec2.copy(_zoomStart, mouseOnScreenVec2);
  477. Vec2.copy(_zoomEnd, _zoomStart);
  478. }
  479. if (dragFocus) {
  480. Vec2.copy(_focusStart, mouseOnScreenVec2);
  481. Vec2.copy(_focusEnd, _focusStart);
  482. }
  483. if (dragPan) {
  484. Vec2.copy(_panStart, mouseOnScreenVec2);
  485. Vec2.copy(_panEnd, _panStart);
  486. }
  487. }
  488. if (dragRotate) Vec2.copy(_rotCurr, mouseOnCircleVec2);
  489. if (dragRotateZ) Vec2.set(_rollCurr, vx, vy);
  490. if (dragZoom || dragFocusZoom) Vec2.copy(_zoomEnd, mouseOnScreenVec2);
  491. if (dragFocus) Vec2.copy(_focusEnd, mouseOnScreenVec2);
  492. if (dragFocusZoom) {
  493. const dist = Vec3.distance(camera.state.position, camera.state.target);
  494. camera.setState({ radius: dist / 5 });
  495. }
  496. if (dragPan) Vec2.copy(_panEnd, mouseOnScreenVec2);
  497. }
  498. function onInteractionEnd() {
  499. _isInteracting = false;
  500. }
  501. function onWheel({ x, y, spinX, spinY, dz, buttons, modifiers }: WheelInput) {
  502. if (outsideViewport(x, y)) return;
  503. let delta = absMax(spinX * 0.075, spinY * 0.075, dz * 0.0001);
  504. if (delta < -p.maxWheelDelta) delta = -p.maxWheelDelta;
  505. else if (delta > p.maxWheelDelta) delta = p.maxWheelDelta;
  506. if (Binding.match(b.scrollZoom, buttons, modifiers)) {
  507. _zoomEnd[1] += delta;
  508. }
  509. if (Binding.match(b.scrollFocus, buttons, modifiers)) {
  510. _focusEnd[1] += delta;
  511. }
  512. }
  513. function onPinch({ fractionDelta, buttons, modifiers }: PinchInput) {
  514. if (Binding.match(b.scrollZoom, buttons, modifiers)) {
  515. _isInteracting = true;
  516. _zoomEnd[1] += p.gestureScaleFactor * fractionDelta;
  517. }
  518. }
  519. function onGesture({ deltaScale }: GestureInput) {
  520. _isInteracting = true;
  521. _zoomEnd[1] += p.gestureScaleFactor * deltaScale;
  522. }
  523. function onMove({ movementX, movementY }: MoveInput) {
  524. if (!input.pointerLock || movementX === undefined || movementY === undefined) return;
  525. const cx = viewport.width * 0.5 - viewport.x;
  526. const cy = viewport.height * 0.5 - viewport.y;
  527. Vec2.copy(_rotPrev, getMouseOnCircle(cx, cy));
  528. Vec2.copy(_rotCurr, getMouseOnCircle(movementX + cx, movementY + cy));
  529. }
  530. function onKeyDown({ modifiers, code, key, x, y }: KeyInput) {
  531. if (outsideViewport(x, y)) return;
  532. if (Binding.matchKey(b.keyMoveForward, code, modifiers, key)) {
  533. keyState.moveForward = 1;
  534. } else if (Binding.matchKey(b.keyMoveBack, code, modifiers, key)) {
  535. keyState.moveBack = 1;
  536. } else if (Binding.matchKey(b.keyMoveLeft, code, modifiers, key)) {
  537. keyState.moveLeft = 1;
  538. } else if (Binding.matchKey(b.keyMoveRight, code, modifiers, key)) {
  539. keyState.moveRight = 1;
  540. } else if (Binding.matchKey(b.keyMoveUp, code, modifiers, key)) {
  541. keyState.moveUp = 1;
  542. } else if (Binding.matchKey(b.keyMoveDown, code, modifiers, key)) {
  543. keyState.moveDown = 1;
  544. } else if (Binding.matchKey(b.keyRollLeft, code, modifiers, key)) {
  545. keyState.rollLeft = 1;
  546. } else if (Binding.matchKey(b.keyRollRight, code, modifiers, key)) {
  547. keyState.rollRight = 1;
  548. } else if (Binding.matchKey(b.keyPitchUp, code, modifiers, key)) {
  549. keyState.pitchUp = 1;
  550. } else if (Binding.matchKey(b.keyPitchDown, code, modifiers, key)) {
  551. keyState.pitchDown = 1;
  552. } else if (Binding.matchKey(b.keyYawLeft, code, modifiers, key)) {
  553. keyState.yawLeft = 1;
  554. } else if (Binding.matchKey(b.keyYawRight, code, modifiers, key)) {
  555. keyState.yawRight = 1;
  556. }
  557. if (Binding.matchKey(b.boostMove, code, modifiers, key)) {
  558. keyState.boostMove = 1;
  559. }
  560. if (Binding.matchKey(b.enablePointerLock, code, modifiers, key)) {
  561. input.requestPointerLock(viewport);
  562. }
  563. }
  564. function onKeyUp({ modifiers, code, key, x, y }: KeyInput) {
  565. if (outsideViewport(x, y)) return;
  566. let isModifierCode = false;
  567. if (code.startsWith('Alt')) {
  568. isModifierCode = true;
  569. modifiers.alt = true;
  570. } else if (code.startsWith('Shift')) {
  571. isModifierCode = true;
  572. modifiers.shift = true;
  573. } else if (code.startsWith('Control')) {
  574. isModifierCode = true;
  575. modifiers.control = true;
  576. } else if (code.startsWith('Meta')) {
  577. isModifierCode = true;
  578. modifiers.meta = true;
  579. }
  580. const codes = [];
  581. if (isModifierCode) {
  582. if (keyState.moveForward) codes.push(b.keyMoveForward.triggers[0]?.code || '');
  583. if (keyState.moveBack) codes.push(b.keyMoveBack.triggers[0]?.code || '');
  584. if (keyState.moveLeft) codes.push(b.keyMoveLeft.triggers[0]?.code || '');
  585. if (keyState.moveRight) codes.push(b.keyMoveRight.triggers[0]?.code || '');
  586. if (keyState.moveUp) codes.push(b.keyMoveUp.triggers[0]?.code || '');
  587. if (keyState.moveDown) codes.push(b.keyMoveDown.triggers[0]?.code || '');
  588. if (keyState.rollLeft) codes.push(b.keyRollLeft.triggers[0]?.code || '');
  589. if (keyState.rollRight) codes.push(b.keyRollRight.triggers[0]?.code || '');
  590. if (keyState.pitchUp) codes.push(b.keyPitchUp.triggers[0]?.code || '');
  591. if (keyState.pitchDown) codes.push(b.keyPitchDown.triggers[0]?.code || '');
  592. if (keyState.yawLeft) codes.push(b.keyYawLeft.triggers[0]?.code || '');
  593. if (keyState.yawRight) codes.push(b.keyYawRight.triggers[0]?.code || '');
  594. } else {
  595. codes.push(code);
  596. }
  597. for (const code of codes) {
  598. if (Binding.matchKey(b.keyMoveForward, code, modifiers, key)) {
  599. keyState.moveForward = 0;
  600. } else if (Binding.matchKey(b.keyMoveBack, code, modifiers, key)) {
  601. keyState.moveBack = 0;
  602. } else if (Binding.matchKey(b.keyMoveLeft, code, modifiers, key)) {
  603. keyState.moveLeft = 0;
  604. } else if (Binding.matchKey(b.keyMoveRight, code, modifiers, key)) {
  605. keyState.moveRight = 0;
  606. } else if (Binding.matchKey(b.keyMoveUp, code, modifiers, key)) {
  607. keyState.moveUp = 0;
  608. } else if (Binding.matchKey(b.keyMoveDown, code, modifiers, key)) {
  609. keyState.moveDown = 0;
  610. } else if (Binding.matchKey(b.keyRollLeft, code, modifiers, key)) {
  611. keyState.rollLeft = 0;
  612. } else if (Binding.matchKey(b.keyRollRight, code, modifiers, key)) {
  613. keyState.rollRight = 0;
  614. } else if (Binding.matchKey(b.keyPitchUp, code, modifiers, key)) {
  615. keyState.pitchUp = 0;
  616. } else if (Binding.matchKey(b.keyPitchDown, code, modifiers, key)) {
  617. keyState.pitchDown = 0;
  618. } else if (Binding.matchKey(b.keyYawLeft, code, modifiers, key)) {
  619. keyState.yawLeft = 0;
  620. } else if (Binding.matchKey(b.keyYawRight, code, modifiers, key)) {
  621. keyState.yawRight = 0;
  622. }
  623. }
  624. if (Binding.matchKey(b.boostMove, code, modifiers, key)) {
  625. keyState.boostMove = 0;
  626. }
  627. }
  628. function initCameraMove() {
  629. Vec3.sub(moveEye, camera.position, camera.target);
  630. const minDistance = Math.max(camera.state.minNear, p.minDistance);
  631. Vec3.setMagnitude(moveEye, moveEye, minDistance);
  632. Vec3.sub(camera.target, camera.position, moveEye);
  633. const cameraDistance = Vec3.distance(camera.position, scene.boundingSphereVisible.center);
  634. camera.setState({ minFar: cameraDistance + scene.boundingSphereVisible.radius });
  635. }
  636. function resetCameraMove() {
  637. const { center, radius } = scene.boundingSphereVisible;
  638. const cameraDistance = Vec3.distance(camera.position, center);
  639. if (cameraDistance > radius) {
  640. const focus = camera.getFocus(center, radius);
  641. camera.setState({ ...focus, minFar: 0 });
  642. } else {
  643. camera.setState({
  644. minFar: 0,
  645. radius: scene.boundingSphereVisible.radius,
  646. });
  647. }
  648. }
  649. function onLock(isLocked: boolean) {
  650. if (isLocked) {
  651. initCameraMove();
  652. } else {
  653. resetCameraMove();
  654. }
  655. }
  656. function unsetKeyState() {
  657. keyState.moveForward = 0;
  658. keyState.moveBack = 0;
  659. keyState.moveLeft = 0;
  660. keyState.moveRight = 0;
  661. keyState.moveUp = 0;
  662. keyState.moveDown = 0;
  663. keyState.rollLeft = 0;
  664. keyState.rollRight = 0;
  665. keyState.pitchUp = 0;
  666. keyState.pitchDown = 0;
  667. keyState.yawLeft = 0;
  668. keyState.yawRight = 0;
  669. keyState.boostMove = 0;
  670. }
  671. function onLeave() {
  672. unsetKeyState();
  673. }
  674. function dispose() {
  675. if (disposed) return;
  676. disposed = true;
  677. dragSub.unsubscribe();
  678. wheelSub.unsubscribe();
  679. pinchSub.unsubscribe();
  680. gestureSub.unsubscribe();
  681. interactionEndSub.unsubscribe();
  682. keyDownSub.unsubscribe();
  683. keyUpSub.unsubscribe();
  684. moveSub.unsubscribe();
  685. lockSub.unsubscribe();
  686. leaveSub.unsubscribe();
  687. }
  688. const _spinSpeed = Vec2.create(0.005, 0);
  689. function spin(deltaT: number) {
  690. if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
  691. const frameSpeed = p.animate.params.speed / 1000;
  692. _spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
  693. Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
  694. }
  695. let _rockPhase = 0;
  696. const _rockSpeed = Vec2.create(0.005, 0);
  697. function rock(deltaT: number) {
  698. if (p.animate.name !== 'rock' || p.animate.params.speed === 0 || _isInteracting) return;
  699. const dt = deltaT / 1000 * p.animate.params.speed;
  700. const maxAngle = degToRad(p.animate.params.angle) / getRotateFactor();
  701. const angleA = Math.sin(_rockPhase * Math.PI * 2) * maxAngle;
  702. const angleB = Math.sin((_rockPhase + dt) * Math.PI * 2) * maxAngle;
  703. _rockSpeed[0] = angleB - angleA;
  704. Vec2.add(_rotCurr, _rotPrev, _rockSpeed);
  705. _rockPhase += dt;
  706. if (_rockPhase >= 1) {
  707. _rockPhase = 0;
  708. }
  709. }
  710. function resetRock() {
  711. _rockPhase = 0;
  712. }
  713. function start(t: number) {
  714. lastUpdated = -1;
  715. update(t);
  716. }
  717. return {
  718. viewport,
  719. get isAnimating() { return p.animate.name !== 'off'; },
  720. get isMoving() {
  721. return (
  722. keyState.moveForward === 1 || keyState.moveBack === 1 ||
  723. keyState.moveLeft === 1 || keyState.moveRight === 1 ||
  724. keyState.moveUp === 1 || keyState.moveDown === 1 ||
  725. keyState.rollLeft === 1 || keyState.rollRight === 1 ||
  726. keyState.pitchUp === 1 || keyState.pitchDown === 1 ||
  727. keyState.yawLeft === 1 || keyState.yawRight === 1
  728. );
  729. },
  730. get props() { return p as Readonly<TrackballControlsProps>; },
  731. setProps: (props: Partial<TrackballControlsProps>) => {
  732. if (props.animate?.name === 'rock' && p.animate.name !== 'rock') {
  733. resetRock(); // start rocking from the center
  734. }
  735. if (props.flyMode !== undefined && props.flyMode !== p.flyMode) {
  736. if (props.flyMode) {
  737. initCameraMove();
  738. } else {
  739. resetCameraMove();
  740. }
  741. }
  742. Object.assign(p, props);
  743. Object.assign(b, props.bindings);
  744. },
  745. start,
  746. update,
  747. reset,
  748. dispose
  749. };
  750. }
  751. }