arcRotateCameraPointersInput.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import { Nullable } from "../../types";
  2. import { serialize } from "../../Misc/decorators";
  3. import { EventState, Observer } from "../../Misc/observable";
  4. import { Tools } from "../../Misc/tools";
  5. import { ArcRotateCamera } from "../../Cameras/arcRotateCamera";
  6. import { ICameraInput, CameraInputTypes } from "../../Cameras/cameraInputsManager";
  7. import { PointerInfo, PointerEventTypes } from "../../Events/pointerEvents";
  8. /**
  9. * Manage the pointers inputs to control an arc rotate camera.
  10. * @see http://doc.babylonjs.com/how_to/customizing_camera_inputs
  11. */
  12. export class ArcRotateCameraPointersInput implements ICameraInput<ArcRotateCamera> {
  13. /**
  14. * Defines the camera the input is attached to.
  15. */
  16. public camera: ArcRotateCamera;
  17. /**
  18. * Defines the buttons associated with the input to handle camera move.
  19. */
  20. @serialize()
  21. public buttons = [0, 1, 2];
  22. /**
  23. * Defines the pointer angular sensibility along the X axis or how fast is the camera rotating.
  24. */
  25. @serialize()
  26. public angularSensibilityX = 1000.0;
  27. /**
  28. * Defines the pointer angular sensibility along the Y axis or how fast is the camera rotating.
  29. */
  30. @serialize()
  31. public angularSensibilityY = 1000.0;
  32. /**
  33. * Defines the pointer pinch precision or how fast is the camera zooming.
  34. */
  35. @serialize()
  36. public pinchPrecision = 12.0;
  37. /**
  38. * pinchDeltaPercentage will be used instead of pinchPrecision if different from 0.
  39. * It defines the percentage of current camera.radius to use as delta when pinch zoom is used.
  40. */
  41. @serialize()
  42. public pinchDeltaPercentage = 0;
  43. /**
  44. * Defines the pointer panning sensibility or how fast is the camera moving.
  45. */
  46. @serialize()
  47. public panningSensibility: number = 1000.0;
  48. /**
  49. * Defines whether panning (2 fingers swipe) is enabled through multitouch.
  50. */
  51. @serialize()
  52. public multiTouchPanning: boolean = true;
  53. /**
  54. * Defines whether panning is enabled for both pan (2 fingers swipe) and zoom (pinch) through multitouch.
  55. */
  56. @serialize()
  57. public multiTouchPanAndZoom: boolean = true;
  58. /**
  59. * Revers pinch action direction.
  60. */
  61. public pinchInwards = true;
  62. private _isPanClick: boolean = false;
  63. private _pointerInput: (p: PointerInfo, s: EventState) => void;
  64. private _observer: Nullable<Observer<PointerInfo>>;
  65. private _onMouseMove: Nullable<(e: MouseEvent) => any>;
  66. private _onGestureStart: Nullable<(e: PointerEvent) => void>;
  67. private _onGesture: Nullable<(e: MSGestureEvent) => void>;
  68. private _MSGestureHandler: Nullable<MSGesture>;
  69. private _onLostFocus: Nullable<(e: FocusEvent) => any>;
  70. private _onContextMenu: Nullable<(e: Event) => void>;
  71. /**
  72. * Attach the input controls to a specific dom element to get the input from.
  73. * @param element Defines the element the controls should be listened from
  74. * @param noPreventDefault Defines whether event caught by the controls should call preventdefault() (https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
  75. */
  76. public attachControl(element: HTMLElement, noPreventDefault?: boolean): void {
  77. var engine = this.camera.getEngine();
  78. var cacheSoloPointer: Nullable<{ x: number, y: number, pointerId: number, type: any }>; // cache pointer object for better perf on camera rotation
  79. var pointA: Nullable<{ x: number, y: number, pointerId: number, type: any }> = null;
  80. var pointB: Nullable<{ x: number, y: number, pointerId: number, type: any }> = null;
  81. var previousPinchSquaredDistance = 0;
  82. var initialDistance = 0;
  83. var twoFingerActivityCount = 0;
  84. var previousMultiTouchPanPosition: { x: number, y: number, isPaning: boolean, isPinching: boolean } = { x: 0, y: 0, isPaning: false, isPinching: false };
  85. this._pointerInput = (p, s) => {
  86. var evt = <PointerEvent>p.event;
  87. let isTouch = (<any>p.event).pointerType === "touch";
  88. if (engine.isInVRExclusivePointerMode) {
  89. return;
  90. }
  91. if (p.type !== PointerEventTypes.POINTERMOVE && this.buttons.indexOf(evt.button) === -1) {
  92. return;
  93. }
  94. let srcElement = <HTMLElement>(evt.srcElement || evt.target);
  95. if (p.type === PointerEventTypes.POINTERDOWN && srcElement) {
  96. try {
  97. srcElement.setPointerCapture(evt.pointerId);
  98. } catch (e) {
  99. //Nothing to do with the error. Execution will continue.
  100. }
  101. // Manage panning with pan button click
  102. this._isPanClick = evt.button === this.camera._panningMouseButton;
  103. // manage pointers
  104. cacheSoloPointer = { x: evt.clientX, y: evt.clientY, pointerId: evt.pointerId, type: evt.pointerType };
  105. if (pointA === null) {
  106. pointA = cacheSoloPointer;
  107. }
  108. else if (pointB === null) {
  109. pointB = cacheSoloPointer;
  110. }
  111. if (!noPreventDefault) {
  112. evt.preventDefault();
  113. element.focus();
  114. }
  115. }
  116. else if (p.type === PointerEventTypes.POINTERDOUBLETAP) {
  117. if (this.camera.useInputToRestoreState) {
  118. this.camera.restoreState();
  119. }
  120. }
  121. else if (p.type === PointerEventTypes.POINTERUP && srcElement) {
  122. try {
  123. srcElement.releasePointerCapture(evt.pointerId);
  124. } catch (e) {
  125. //Nothing to do with the error.
  126. }
  127. cacheSoloPointer = null;
  128. previousPinchSquaredDistance = 0;
  129. previousMultiTouchPanPosition.isPaning = false;
  130. previousMultiTouchPanPosition.isPinching = false;
  131. twoFingerActivityCount = 0;
  132. initialDistance = 0;
  133. if (!isTouch) {
  134. pointB = null; // Mouse and pen are mono pointer
  135. }
  136. //would be better to use pointers.remove(evt.pointerId) for multitouch gestures,
  137. //but emptying completly pointers collection is required to fix a bug on iPhone :
  138. //when changing orientation while pinching camera, one pointer stay pressed forever if we don't release all pointers
  139. //will be ok to put back pointers.remove(evt.pointerId); when iPhone bug corrected
  140. if (engine._badOS) {
  141. pointA = pointB = null;
  142. }
  143. else {
  144. //only remove the impacted pointer in case of multitouch allowing on most
  145. //platforms switching from rotate to zoom and pan seamlessly.
  146. if (pointB && pointA && pointA.pointerId == evt.pointerId) {
  147. pointA = pointB;
  148. pointB = null;
  149. cacheSoloPointer = { x: pointA.x, y: pointA.y, pointerId: pointA.pointerId, type: evt.pointerType };
  150. }
  151. else if (pointA && pointB && pointB.pointerId == evt.pointerId) {
  152. pointB = null;
  153. cacheSoloPointer = { x: pointA.x, y: pointA.y, pointerId: pointA.pointerId, type: evt.pointerType };
  154. }
  155. else {
  156. pointA = pointB = null;
  157. }
  158. }
  159. if (!noPreventDefault) {
  160. evt.preventDefault();
  161. }
  162. } else if (p.type === PointerEventTypes.POINTERMOVE) {
  163. if (!noPreventDefault) {
  164. evt.preventDefault();
  165. }
  166. // One button down
  167. if (pointA && pointB === null && cacheSoloPointer) {
  168. if (this.panningSensibility !== 0 &&
  169. ((evt.ctrlKey && this.camera._useCtrlForPanning) || this._isPanClick)) {
  170. this.camera.inertialPanningX += -(evt.clientX - cacheSoloPointer.x) / this.panningSensibility;
  171. this.camera.inertialPanningY += (evt.clientY - cacheSoloPointer.y) / this.panningSensibility;
  172. } else {
  173. var offsetX = evt.clientX - cacheSoloPointer.x;
  174. var offsetY = evt.clientY - cacheSoloPointer.y;
  175. this.camera.inertialAlphaOffset -= offsetX / this.angularSensibilityX;
  176. this.camera.inertialBetaOffset -= offsetY / this.angularSensibilityY;
  177. }
  178. cacheSoloPointer.x = evt.clientX;
  179. cacheSoloPointer.y = evt.clientY;
  180. }
  181. // Two buttons down: pinch/pan
  182. else if (pointA && pointB) {
  183. //if (noPreventDefault) { evt.preventDefault(); } //if pinch gesture, could be useful to force preventDefault to avoid html page scroll/zoom in some mobile browsers
  184. var ed = (pointA.pointerId === evt.pointerId) ? pointA : pointB;
  185. ed.x = evt.clientX;
  186. ed.y = evt.clientY;
  187. var direction = this.pinchInwards ? 1 : -1;
  188. var distX = pointA.x - pointB.x;
  189. var distY = pointA.y - pointB.y;
  190. var pinchSquaredDistance = (distX * distX) + (distY * distY);
  191. var pinchDistance = Math.sqrt(pinchSquaredDistance);
  192. if (previousPinchSquaredDistance === 0) {
  193. initialDistance = pinchDistance;
  194. previousPinchSquaredDistance = pinchSquaredDistance;
  195. previousMultiTouchPanPosition.x = (pointA.x + pointB.x) / 2;
  196. previousMultiTouchPanPosition.y = (pointA.y + pointB.y) / 2;
  197. return;
  198. }
  199. if (this.multiTouchPanAndZoom) {
  200. if (this.pinchDeltaPercentage) {
  201. this.camera.inertialRadiusOffset += ((pinchSquaredDistance - previousPinchSquaredDistance) * 0.001) * this.camera.radius * this.pinchDeltaPercentage;
  202. } else {
  203. this.camera.inertialRadiusOffset += (pinchSquaredDistance - previousPinchSquaredDistance) /
  204. (this.pinchPrecision *
  205. ((this.angularSensibilityX + this.angularSensibilityY) / 2) *
  206. direction);
  207. }
  208. if (this.panningSensibility !== 0) {
  209. var pointersCenterX = (pointA.x + pointB.x) / 2;
  210. var pointersCenterY = (pointA.y + pointB.y) / 2;
  211. var pointersCenterDistX = pointersCenterX - previousMultiTouchPanPosition.x;
  212. var pointersCenterDistY = pointersCenterY - previousMultiTouchPanPosition.y;
  213. previousMultiTouchPanPosition.x = pointersCenterX;
  214. previousMultiTouchPanPosition.y = pointersCenterY;
  215. this.camera.inertialPanningX += -(pointersCenterDistX) / (this.panningSensibility);
  216. this.camera.inertialPanningY += (pointersCenterDistY) / (this.panningSensibility);
  217. }
  218. }
  219. else {
  220. twoFingerActivityCount++;
  221. if (previousMultiTouchPanPosition.isPinching || (twoFingerActivityCount < 20 && Math.abs(pinchDistance - initialDistance) > this.camera.pinchToPanMaxDistance)) {
  222. if (this.pinchDeltaPercentage) {
  223. this.camera.inertialRadiusOffset += ((pinchSquaredDistance - previousPinchSquaredDistance) * 0.001) * this.camera.radius * this.pinchDeltaPercentage;
  224. } else {
  225. this.camera.inertialRadiusOffset += (pinchSquaredDistance - previousPinchSquaredDistance) /
  226. (this.pinchPrecision *
  227. ((this.angularSensibilityX + this.angularSensibilityY) / 2) *
  228. direction);
  229. }
  230. previousMultiTouchPanPosition.isPaning = false;
  231. previousMultiTouchPanPosition.isPinching = true;
  232. }
  233. else {
  234. if (cacheSoloPointer && cacheSoloPointer.pointerId === ed.pointerId && this.panningSensibility !== 0 && this.multiTouchPanning) {
  235. if (!previousMultiTouchPanPosition.isPaning) {
  236. previousMultiTouchPanPosition.isPaning = true;
  237. previousMultiTouchPanPosition.isPinching = false;
  238. previousMultiTouchPanPosition.x = ed.x;
  239. previousMultiTouchPanPosition.y = ed.y;
  240. return;
  241. }
  242. this.camera.inertialPanningX += -(ed.x - previousMultiTouchPanPosition.x) / (this.panningSensibility);
  243. this.camera.inertialPanningY += (ed.y - previousMultiTouchPanPosition.y) / (this.panningSensibility);
  244. }
  245. }
  246. if (cacheSoloPointer && cacheSoloPointer.pointerId === evt.pointerId) {
  247. previousMultiTouchPanPosition.x = ed.x;
  248. previousMultiTouchPanPosition.y = ed.y;
  249. }
  250. }
  251. previousPinchSquaredDistance = pinchSquaredDistance;
  252. }
  253. }
  254. };
  255. this._observer = this.camera.getScene().onPointerObservable.add(this._pointerInput, PointerEventTypes.POINTERDOWN | PointerEventTypes.POINTERUP | PointerEventTypes.POINTERMOVE | PointerEventTypes.POINTERDOUBLETAP);
  256. this._onContextMenu = (evt) => {
  257. evt.preventDefault();
  258. };
  259. if (!this.camera._useCtrlForPanning) {
  260. element.addEventListener("contextmenu", this._onContextMenu, false);
  261. }
  262. this._onLostFocus = () => {
  263. //this._keys = [];
  264. pointA = pointB = null;
  265. previousPinchSquaredDistance = 0;
  266. previousMultiTouchPanPosition.isPaning = false;
  267. previousMultiTouchPanPosition.isPinching = false;
  268. twoFingerActivityCount = 0;
  269. cacheSoloPointer = null;
  270. initialDistance = 0;
  271. };
  272. this._onMouseMove = (evt) => {
  273. if (!engine.isPointerLock) {
  274. return;
  275. }
  276. var offsetX = evt.movementX || evt.mozMovementX || evt.webkitMovementX || evt.msMovementX || 0;
  277. var offsetY = evt.movementY || evt.mozMovementY || evt.webkitMovementY || evt.msMovementY || 0;
  278. this.camera.inertialAlphaOffset -= offsetX / this.angularSensibilityX;
  279. this.camera.inertialBetaOffset -= offsetY / this.angularSensibilityY;
  280. if (!noPreventDefault) {
  281. evt.preventDefault();
  282. }
  283. };
  284. this._onGestureStart = (e) => {
  285. if (window.MSGesture === undefined) {
  286. return;
  287. }
  288. if (!this._MSGestureHandler) {
  289. this._MSGestureHandler = new MSGesture();
  290. this._MSGestureHandler.target = element;
  291. }
  292. this._MSGestureHandler.addPointer(e.pointerId);
  293. };
  294. this._onGesture = (e) => {
  295. this.camera.radius *= e.scale;
  296. if (e.preventDefault) {
  297. if (!noPreventDefault) {
  298. e.stopPropagation();
  299. e.preventDefault();
  300. }
  301. }
  302. };
  303. element.addEventListener("mousemove", this._onMouseMove, false);
  304. element.addEventListener("MSPointerDown", <EventListener>this._onGestureStart, false);
  305. element.addEventListener("MSGestureChange", <EventListener>this._onGesture, false);
  306. Tools.RegisterTopRootEvents([
  307. { name: "blur", handler: this._onLostFocus }
  308. ]);
  309. }
  310. /**
  311. * Detach the current controls from the specified dom element.
  312. * @param element Defines the element to stop listening the inputs from
  313. */
  314. public detachControl(element: Nullable<HTMLElement>): void {
  315. if (this._onLostFocus) {
  316. Tools.UnregisterTopRootEvents([
  317. { name: "blur", handler: this._onLostFocus }
  318. ]);
  319. }
  320. if (element && this._observer) {
  321. this.camera.getScene().onPointerObservable.remove(this._observer);
  322. this._observer = null;
  323. if (this._onContextMenu) {
  324. element.removeEventListener("contextmenu", this._onContextMenu);
  325. }
  326. if (this._onMouseMove) {
  327. element.removeEventListener("mousemove", this._onMouseMove);
  328. }
  329. if (this._onGestureStart) {
  330. element.removeEventListener("MSPointerDown", <EventListener>this._onGestureStart);
  331. }
  332. if (this._onGesture) {
  333. element.removeEventListener("MSGestureChange", <EventListener>this._onGesture);
  334. }
  335. this._isPanClick = false;
  336. this.pinchInwards = true;
  337. this._onMouseMove = null;
  338. this._onGestureStart = null;
  339. this._onGesture = null;
  340. this._MSGestureHandler = null;
  341. this._onLostFocus = null;
  342. this._onContextMenu = null;
  343. }
  344. }
  345. /**
  346. * Gets the class name of the current intput.
  347. * @returns the class name
  348. */
  349. public getClassName(): string {
  350. return "ArcRotateCameraPointersInput";
  351. }
  352. /**
  353. * Get the friendly name associated with the input class.
  354. * @returns the input friendly name
  355. */
  356. public getSimpleName(): string {
  357. return "pointers";
  358. }
  359. }
  360. (<any>CameraInputTypes)["ArcRotateCameraPointersInput"] = ArcRotateCameraPointersInput;