babylon.arcRotateCameraPointersInput.ts 17 KB

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