BaseCameraPointersInput.ts 13 KB

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