BaseCameraPointersInput.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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 { Camera } from "../../Cameras/camera";
  6. import { ICameraInput } from "../../Cameras/cameraInputsManager";
  7. import { PointerInfo, PointerEventTypes, PointerTouch } from "../../Events/pointerEvents";
  8. /**
  9. * Base class for Camera Pointer Inputs.
  10. * See FollowCameraPointersInput in src/Cameras/Inputs/followCameraPointersInput.ts
  11. * for example usage.
  12. */
  13. export abstract class BaseCameraPointersInput implements ICameraInput<Camera> {
  14. /**
  15. * Defines the camera the input is attached to.
  16. */
  17. public abstract camera: Camera;
  18. /**
  19. * Whether keyboard modifier keys are pressed at time of last mouse event.
  20. */
  21. protected _altKey: boolean;
  22. protected _ctrlKey: boolean;
  23. protected _metaKey: boolean;
  24. protected _shiftKey: boolean;
  25. /**
  26. * Which mouse buttons were pressed at time of last mouse event.
  27. * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
  28. */
  29. protected _buttonsPressed: number;
  30. /**
  31. * Defines the buttons associated with the input to handle camera move.
  32. */
  33. @serialize()
  34. public buttons = [0, 1, 2];
  35. /**
  36. * Attach the input controls to a specific dom element to get the input from.
  37. * @param element Defines the element the controls should be listened from
  38. * @param noPreventDefault Defines whether event caught by the controls should call preventdefault() (https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
  39. */
  40. public attachControl(element: HTMLElement, noPreventDefault?: boolean): void {
  41. var engine = this.camera.getEngine();
  42. var previousPinchSquaredDistance = 0;
  43. var previousMultiTouchPanPosition: Nullable<PointerTouch> = null;
  44. this.pointA = null;
  45. this.pointB = null;
  46. this._altKey = false;
  47. this._ctrlKey = false;
  48. this._metaKey = false;
  49. this._shiftKey = false;
  50. this._buttonsPressed = 0;
  51. this._pointerInput = (p, s) => {
  52. var evt = <PointerEvent>p.event;
  53. let isTouch = evt.pointerType === "touch";
  54. if (engine.isInVRExclusivePointerMode) {
  55. return;
  56. }
  57. if (p.type !== PointerEventTypes.POINTERMOVE &&
  58. this.buttons.indexOf(evt.button) === -1) {
  59. return;
  60. }
  61. let srcElement = <HTMLElement>(evt.srcElement || evt.target);
  62. this._altKey = evt.altKey;
  63. this._ctrlKey = evt.ctrlKey;
  64. this._metaKey = evt.metaKey;
  65. this._shiftKey = evt.shiftKey;
  66. this._buttonsPressed = evt.buttons;
  67. if (engine.isPointerLock) {
  68. var offsetX = evt.movementX ||
  69. evt.mozMovementX ||
  70. evt.webkitMovementX ||
  71. evt.msMovementX ||
  72. 0;
  73. var offsetY = evt.movementY ||
  74. evt.mozMovementY ||
  75. evt.webkitMovementY ||
  76. evt.msMovementY ||
  77. 0;
  78. this.onTouch(null, offsetX, offsetY);
  79. this.pointA = null;
  80. this.pointB = null;
  81. } else if (p.type === PointerEventTypes.POINTERDOWN && srcElement) {
  82. try {
  83. srcElement.setPointerCapture(evt.pointerId);
  84. } catch (e) {
  85. //Nothing to do with the error. Execution will continue.
  86. }
  87. if (this.pointA === null) {
  88. this.pointA = {x: evt.clientX,
  89. y: evt.clientY,
  90. pointerId: evt.pointerId,
  91. type: evt.pointerType };
  92. } else if (this.pointB === null) {
  93. this.pointB = {x: evt.clientX,
  94. y: evt.clientY,
  95. pointerId: evt.pointerId,
  96. type: evt.pointerType };
  97. }
  98. this.onButtonDown(evt);
  99. if (!noPreventDefault) {
  100. evt.preventDefault();
  101. element.focus();
  102. }
  103. } else if (p.type === PointerEventTypes.POINTERDOUBLETAP) {
  104. this.onDoubleTap(evt.pointerType);
  105. } else if (p.type === PointerEventTypes.POINTERUP && srcElement) {
  106. try {
  107. srcElement.releasePointerCapture(evt.pointerId);
  108. } catch (e) {
  109. //Nothing to do with the error.
  110. }
  111. if (!isTouch) {
  112. this.pointB = null; // Mouse and pen are mono pointer
  113. }
  114. //would be better to use pointers.remove(evt.pointerId) for multitouch gestures,
  115. //but emptying completely pointers collection is required to fix a bug on iPhone :
  116. //when changing orientation while pinching camera,
  117. //one pointer stay pressed forever if we don't release all pointers
  118. //will be ok to put back pointers.remove(evt.pointerId); when iPhone bug corrected
  119. if (engine._badOS) {
  120. this.pointA = this.pointB = null;
  121. } else {
  122. //only remove the impacted pointer in case of multitouch allowing on most
  123. //platforms switching from rotate to zoom and pan seamlessly.
  124. if (this.pointB && this.pointA && this.pointA.pointerId == evt.pointerId) {
  125. this.pointA = this.pointB;
  126. this.pointB = null;
  127. } else if (this.pointA && this.pointB &&
  128. this.pointB.pointerId == evt.pointerId) {
  129. this.pointB = null;
  130. } else {
  131. this.pointA = this.pointB = null;
  132. }
  133. }
  134. if (previousPinchSquaredDistance !== 0 || previousMultiTouchPanPosition) {
  135. // Previous pinch data is populated but a button has been lifted
  136. // so pinch has ended.
  137. this.onMultiTouch(
  138. this.pointA,
  139. this.pointB,
  140. previousPinchSquaredDistance,
  141. 0, // pinchSquaredDistance
  142. previousMultiTouchPanPosition,
  143. null // multiTouchPanPosition
  144. );
  145. previousPinchSquaredDistance = 0;
  146. previousMultiTouchPanPosition = null;
  147. }
  148. this.onButtonUp(evt);
  149. if (!noPreventDefault) {
  150. evt.preventDefault();
  151. }
  152. } else if (p.type === PointerEventTypes.POINTERMOVE) {
  153. if (!noPreventDefault) {
  154. evt.preventDefault();
  155. }
  156. // One button down
  157. if (this.pointA && this.pointB === null) {
  158. var offsetX = evt.clientX - this.pointA.x;
  159. var offsetY = evt.clientY - this.pointA.y;
  160. this.onTouch(this.pointA, offsetX, offsetY);
  161. this.pointA.x = evt.clientX;
  162. this.pointA.y = evt.clientY;
  163. }
  164. // Two buttons down: pinch
  165. else if (this.pointA && this.pointB) {
  166. var ed = (this.pointA.pointerId === evt.pointerId) ?
  167. this.pointA : this.pointB;
  168. ed.x = evt.clientX;
  169. ed.y = evt.clientY;
  170. var distX = this.pointA.x - this.pointB.x;
  171. var distY = this.pointA.y - this.pointB.y;
  172. var pinchSquaredDistance = (distX * distX) + (distY * distY);
  173. var multiTouchPanPosition = {x: (this.pointA.x + this.pointB.x) / 2,
  174. y: (this.pointA.y + this.pointB.y) / 2,
  175. pointerId: evt.pointerId,
  176. type: p.type};
  177. this.onMultiTouch(
  178. this.pointA,
  179. this.pointB,
  180. previousPinchSquaredDistance,
  181. pinchSquaredDistance,
  182. previousMultiTouchPanPosition,
  183. multiTouchPanPosition);
  184. previousMultiTouchPanPosition = multiTouchPanPosition;
  185. previousPinchSquaredDistance = pinchSquaredDistance;
  186. }
  187. }
  188. };
  189. this._observer = this.camera.getScene().onPointerObservable.add(
  190. this._pointerInput,
  191. PointerEventTypes.POINTERDOWN | PointerEventTypes.POINTERUP |
  192. PointerEventTypes.POINTERMOVE);
  193. this._onLostFocus = () => {
  194. this.pointA = this.pointB = null;
  195. previousPinchSquaredDistance = 0;
  196. previousMultiTouchPanPosition = null;
  197. this.onLostFocus();
  198. };
  199. element.addEventListener("contextmenu",
  200. <EventListener>this.onContextMenu.bind(this), false);
  201. let hostWindow = this.camera.getScene().getEngine().getHostWindow();
  202. if (hostWindow) {
  203. Tools.RegisterTopRootEvents(hostWindow, [
  204. { name: "blur", handler: this._onLostFocus }
  205. ]);
  206. }
  207. }
  208. /**
  209. * Detach the current controls from the specified dom element.
  210. * @param element Defines the element to stop listening the inputs from
  211. */
  212. public detachControl(element: Nullable<HTMLElement>): void {
  213. if (this._onLostFocus) {
  214. let hostWindow = this.camera.getScene().getEngine().getHostWindow();
  215. if (hostWindow) {
  216. Tools.UnregisterTopRootEvents(hostWindow, [
  217. { name: "blur", handler: this._onLostFocus }
  218. ]);
  219. }
  220. }
  221. if (element && this._observer) {
  222. this.camera.getScene().onPointerObservable.remove(this._observer);
  223. this._observer = null;
  224. if (this.onContextMenu) {
  225. element.removeEventListener("contextmenu", <EventListener>this.onContextMenu);
  226. }
  227. this._onLostFocus = null;
  228. }
  229. this._altKey = false;
  230. this._ctrlKey = false;
  231. this._metaKey = false;
  232. this._shiftKey = false;
  233. this._buttonsPressed = 0;
  234. }
  235. /**
  236. * Gets the class name of the current input.
  237. * @returns the class name
  238. */
  239. public getClassName(): string {
  240. return "BaseCameraPointersInput";
  241. }
  242. /**
  243. * Get the friendly name associated with the input class.
  244. * @returns the input friendly name
  245. */
  246. public getSimpleName(): string {
  247. return "pointers";
  248. }
  249. /**
  250. * Called on pointer POINTERDOUBLETAP event.
  251. * Override this method to provide functionality on POINTERDOUBLETAP event.
  252. */
  253. protected onDoubleTap(type: string) {
  254. }
  255. /**
  256. * Called on pointer POINTERMOVE event if only a single touch is active.
  257. * Override this method to provide functionality.
  258. */
  259. protected onTouch(point: Nullable<PointerTouch>,
  260. offsetX: number,
  261. offsetY: number): void {
  262. }
  263. /**
  264. * Called on pointer POINTERMOVE event if multiple touches are active.
  265. * Override this method to provide functionality.
  266. */
  267. protected onMultiTouch(pointA: Nullable<PointerTouch>,
  268. pointB: Nullable<PointerTouch>,
  269. previousPinchSquaredDistance: number,
  270. pinchSquaredDistance: number,
  271. previousMultiTouchPanPosition: Nullable<PointerTouch>,
  272. multiTouchPanPosition: Nullable<PointerTouch>): void {
  273. }
  274. /**
  275. * Called on JS contextmenu event.
  276. * Override this method to provide functionality.
  277. */
  278. protected onContextMenu(evt: PointerEvent): void {
  279. evt.preventDefault();
  280. }
  281. /**
  282. * Called each time a new POINTERDOWN event occurs. Ie, for each button
  283. * press.
  284. * Override this method to provide functionality.
  285. */
  286. protected onButtonDown(evt: PointerEvent): void {
  287. }
  288. /**
  289. * Called each time a new POINTERUP event occurs. Ie, for each button
  290. * release.
  291. * Override this method to provide functionality.
  292. */
  293. protected onButtonUp(evt: PointerEvent): void {
  294. }
  295. /**
  296. * Called when window becomes inactive.
  297. * Override this method to provide functionality.
  298. */
  299. protected onLostFocus(): void {
  300. }
  301. private _pointerInput: (p: PointerInfo, s: EventState) => void;
  302. private _observer: Nullable<Observer<PointerInfo>>;
  303. private _onLostFocus: Nullable<(e: FocusEvent) => any>;
  304. private pointA: Nullable<PointerTouch>;
  305. private pointB: Nullable<PointerTouch>;
  306. }