babylon.framingBehavior.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. module BABYLON {
  2. export class FramingBehavior implements Behavior<ArcRotateCamera> {
  3. public get name(): string {
  4. return "Framing";
  5. }
  6. private _mode = FramingBehavior.FitFrustumSidesMode;
  7. private _radiusScale = 1.0;
  8. private _positionY = 0;
  9. private _defaultElevation = 0.3;
  10. private _elevationReturnTime = 1500;
  11. private _elevationReturnWaitTime = 1000;
  12. private _zoomStopsAnimation = false;
  13. private _framingTime = 1500;
  14. /**
  15. * The easing function used by animations
  16. */
  17. public static EasingFunction = new ExponentialEase();
  18. /**
  19. * The easing mode used by animations
  20. */
  21. public static EasingMode = EasingFunction.EASINGMODE_EASEINOUT;
  22. /**
  23. * Sets the current mode used by the behavior
  24. */
  25. public set mode(mode: number) {
  26. this._mode = mode;
  27. }
  28. /**
  29. * Gets current mode used by the behavior.
  30. */
  31. public get mode(): number {
  32. return this._mode;
  33. }
  34. /**
  35. * Sets the scale applied to the radius (1 by default)
  36. */
  37. public set radiusScale(radius: number) {
  38. this._radiusScale = radius;
  39. }
  40. /**
  41. * Gets the scale applied to the radius
  42. */
  43. public get radiusScale(): number {
  44. return this._radiusScale;
  45. }
  46. /**
  47. * Sets the Y offset of the target mesh from the camera's focus.
  48. */
  49. public set positionY(positionY: number) {
  50. this._positionY = positionY;
  51. }
  52. /**
  53. * Gets the Y offset of the target mesh from the camera's focus.
  54. */
  55. public get positionY(): number {
  56. return this._positionY;
  57. }
  58. /**
  59. * Sets the angle above/below the horizontal plane to return to when the return to default elevation idle
  60. * behaviour is triggered, in radians.
  61. */
  62. public set defaultElevation(elevation: number) {
  63. this._defaultElevation = elevation;
  64. }
  65. /**
  66. * Gets the angle above/below the horizontal plane to return to when the return to default elevation idle
  67. * behaviour is triggered, in radians.
  68. */
  69. public get defaultElevation() {
  70. return this._defaultElevation;
  71. }
  72. /**
  73. * Sets the time (in milliseconds) taken to return to the default beta position.
  74. * Negative value indicates camera should not return to default.
  75. */
  76. public set elevationReturnTime(speed: number) {
  77. this._elevationReturnTime = speed;
  78. }
  79. /**
  80. * Gets the time (in milliseconds) taken to return to the default beta position.
  81. * Negative value indicates camera should not return to default.
  82. */
  83. public get elevationReturnTime(): number {
  84. return this._elevationReturnTime;
  85. }
  86. /**
  87. * Sets the delay (in milliseconds) taken before the camera returns to the default beta position.
  88. */
  89. public set elevationReturnWaitTime(time: number) {
  90. this._elevationReturnWaitTime = time;
  91. }
  92. /**
  93. * Gets the delay (in milliseconds) taken before the camera returns to the default beta position.
  94. */
  95. public get elevationReturnWaitTime(): number {
  96. return this._elevationReturnWaitTime;
  97. }
  98. /**
  99. * Sets the flag that indicates if user zooming should stop animation.
  100. */
  101. public set zoomStopsAnimation(flag: boolean) {
  102. this._zoomStopsAnimation = flag;
  103. }
  104. /**
  105. * Gets the flag that indicates if user zooming should stop animation.
  106. */
  107. public get zoomStopsAnimation(): boolean {
  108. return this._zoomStopsAnimation;
  109. }
  110. /**
  111. * Sets the transition time when framing the mesh, in milliseconds
  112. */
  113. public set framingTime(time: number) {
  114. this._framingTime = time;
  115. }
  116. /**
  117. * Gets the transition time when framing the mesh, in milliseconds
  118. */
  119. public get framingTime() {
  120. return this._framingTime;
  121. }
  122. // Default behavior functions
  123. private _onPrePointerObservableObserver: Observer<PointerInfoPre>;
  124. private _onAfterCheckInputsObserver: Observer<Camera>;
  125. private _onMeshTargetChangedObserver: Observer<AbstractMesh>;
  126. private _attachedCamera: ArcRotateCamera;
  127. private _isPointerDown = false;
  128. private _lastFrameTime: number = null;
  129. private _lastInteractionTime = -Infinity;
  130. public attach(camera: ArcRotateCamera): void {
  131. this._attachedCamera = camera;
  132. let scene = this._attachedCamera.getScene();
  133. FramingBehavior.EasingFunction.setEasingMode(FramingBehavior.EasingMode);
  134. this._onPrePointerObservableObserver = scene.onPrePointerObservable.add((pointerInfoPre) => {
  135. if (pointerInfoPre.type === PointerEventTypes.POINTERDOWN) {
  136. this._isPointerDown = true;
  137. return
  138. }
  139. if (pointerInfoPre.type === PointerEventTypes.POINTERUP) {
  140. this._isPointerDown = false;
  141. }
  142. });
  143. this._onMeshTargetChangedObserver = camera.onMeshTargetChangedObservable.add((mesh) => {
  144. if (mesh) {
  145. this.zoomOnMesh(mesh);
  146. }
  147. });
  148. this._onAfterCheckInputsObserver = camera.onAfterCheckInputsObservable.add(() => {
  149. // Stop the animation if there is user interaction and the animation should stop for this interaction
  150. this._applyUserInteraction();
  151. // Maintain the camera above the ground. If the user pulls the camera beneath the ground plane, lift it
  152. // back to the default position after a given timeout
  153. this._maintainCameraAboveGround();
  154. });
  155. }
  156. public detach(camera: ArcRotateCamera): void {
  157. let scene = this._attachedCamera.getScene();
  158. scene.onPrePointerObservable.remove(this._onPrePointerObservableObserver);
  159. camera.onAfterCheckInputsObservable.remove(this._onAfterCheckInputsObserver);
  160. camera.onMeshTargetChangedObservable.remove(this._onMeshTargetChangedObserver);
  161. }
  162. // Framing control
  163. private _animatables = new Array<Animatable>();
  164. private _betaIsAnimating = false;
  165. private _betaTransition: Animation;
  166. private _radiusTransition: Animation;
  167. private _vectorTransition: Animation;
  168. private _lastFrameRadius = 0;
  169. /**
  170. * Targets the given mesh and updates zoom level accordingly.
  171. * @param mesh The mesh to target.
  172. * @param radius Optional. If a cached radius position already exists, overrides default.
  173. * @param framingPositionY Position on mesh to center camera focus where 0 corresponds bottom of its bounding box and 1, the top
  174. * @param focusOnOriginXZ Determines if the camera should focus on 0 in the X and Z axis instead of the mesh
  175. */
  176. public zoomOnMesh(mesh: AbstractMesh, radius?: number, framingPositionY?: number, focusOnOriginXZ: boolean = false): void {
  177. if (framingPositionY == null) {
  178. framingPositionY = this._positionY;
  179. }
  180. mesh.computeWorldMatrix(true);
  181. let zoomTarget: BABYLON.Vector3;
  182. let center = mesh.getBoundingInfo().boundingSphere.centerWorld;
  183. if (focusOnOriginXZ) {
  184. zoomTarget = new BABYLON.Vector3(0, center.y, 0);
  185. } else {
  186. zoomTarget = center.clone();
  187. }
  188. if (!this._vectorTransition) {
  189. this._vectorTransition = Animation.CreateAnimation("target", Animation.ANIMATIONTYPE_VECTOR3, 60, FramingBehavior.EasingFunction);
  190. }
  191. this._betaIsAnimating = true;
  192. this._animatables.push(Animation.TransitionTo("target", zoomTarget, this._attachedCamera, this._attachedCamera.getScene(),
  193. 60, this._vectorTransition, this._framingTime));
  194. // sets the radius and lower radius bounds
  195. if (radius == null) {
  196. // Small delta ensures camera is not always at lower zoom limit.
  197. let delta = 0.1;
  198. if (this._mode === FramingBehavior.FitFrustumSidesMode) {
  199. let position = this._calculateLowerRadiusFromModelBoundingSphere(mesh);
  200. this._attachedCamera.lowerRadiusLimit = mesh.getBoundingInfo().boundingSphere.radiusWorld + this._attachedCamera.minZ;
  201. radius = position;
  202. } else if (this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
  203. radius = this._calculateLowerRadiusFromModelBoundingSphere(mesh);
  204. this._attachedCamera.lowerRadiusLimit = this._attachedCamera.minZ;
  205. }
  206. }
  207. // transition to new radius
  208. if (!this._radiusTransition) {
  209. this._radiusTransition = Animation.CreateAnimation("radius", Animation.ANIMATIONTYPE_FLOAT, 60, FramingBehavior.EasingFunction);
  210. }
  211. this._animatables.push(Animation.TransitionTo("radius", radius, this._attachedCamera, this._attachedCamera.getScene(),
  212. 60, this._radiusTransition, this._framingTime));
  213. }
  214. /**
  215. * Calculates the lowest radius for the camera based on the bounding box of the mesh.
  216. * @param mesh The mesh on which to base the calculation. mesh boundingInfo used to estimate necessary
  217. * frustum width.
  218. * @return The minimum distance from the primary mesh's center point at which the camera must be kept in order
  219. * to fully enclose the mesh in the viewing frustum.
  220. */
  221. protected _calculateLowerRadiusFromModelBoundingSphere(mesh: AbstractMesh): number {
  222. let boxVectorGlobalDiagonal = mesh.getBoundingInfo().diagonalLength;
  223. let frustumSlope: BABYLON.Vector2 = this._getFrustumSlope();
  224. // Formula for setting distance
  225. // (Good explanation: http://stackoverflow.com/questions/2866350/move-camera-to-fit-3d-scene)
  226. let radiusWithoutFraming = boxVectorGlobalDiagonal * 0.5;
  227. // Horizon distance
  228. let radius = radiusWithoutFraming * this._radiusScale;
  229. let distanceForHorizontalFrustum = radius * Math.sqrt(1.0 + 1.0 / (frustumSlope.x * frustumSlope.x));
  230. let distanceForVerticalFrustum = radius * Math.sqrt(1.0 + 1.0 / (frustumSlope.y * frustumSlope.y));
  231. let distance = Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum);
  232. let camera = this._attachedCamera;
  233. if (camera.lowerRadiusLimit && this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
  234. // Don't exceed the requested limit
  235. distance = distance < camera.lowerRadiusLimit ? camera.lowerRadiusLimit : distance;
  236. }
  237. // Don't exceed the upper radius limit
  238. if (camera.upperRadiusLimit) {
  239. distance = distance > camera.upperRadiusLimit ? camera.upperRadiusLimit : distance;
  240. }
  241. return distance;
  242. }
  243. /**
  244. * Keeps the camera above the ground plane. If the user pulls the camera below the ground plane, the camera
  245. * is automatically returned to its default position (expected to be above ground plane).
  246. */
  247. private _maintainCameraAboveGround(): void {
  248. let timeSinceInteraction = Tools.Now - this._lastInteractionTime;
  249. let defaultBeta = Math.PI * 0.5 - this._defaultElevation;
  250. let limitBeta = Math.PI * 0.5;
  251. // Bring the camera back up if below the ground plane
  252. if (!this._betaIsAnimating && this._attachedCamera.beta > limitBeta && timeSinceInteraction >= this._elevationReturnWaitTime) {
  253. this._betaIsAnimating = true;
  254. //Transition to new position
  255. this.stopAllAnimations();
  256. if (!this._betaTransition) {
  257. this._betaTransition = Animation.CreateAnimation("beta", Animation.ANIMATIONTYPE_FLOAT, 60, FramingBehavior.EasingFunction);
  258. }
  259. this._animatables.push(Animation.TransitionTo("beta", defaultBeta, this._attachedCamera, this._attachedCamera.getScene(), 60,
  260. this._betaTransition, this._elevationReturnTime,
  261. () => {
  262. this._clearAnimationLocks();
  263. this.stopAllAnimations();
  264. }));
  265. }
  266. }
  267. /**
  268. * Returns the frustum slope based on the canvas ratio and camera FOV
  269. * @returns The frustum slope represented as a Vector2 with X and Y slopes
  270. */
  271. private _getFrustumSlope(): Vector2 {
  272. // Calculate the viewport ratio
  273. // Aspect Ratio is Height/Width.
  274. let camera = this._attachedCamera;
  275. let engine = camera.getScene().getEngine();
  276. var aspectRatio = engine.getAspectRatio(camera);
  277. // Camera FOV is the vertical field of view (top-bottom) in radians.
  278. // Slope of the frustum top/bottom planes in view space, relative to the forward vector.
  279. var frustumSlopeY = Math.tan(camera.fov / 2);
  280. // Slope of the frustum left/right planes in view space, relative to the forward vector.
  281. // Provides the amount that one side (e.g. left) of the frustum gets wider for every unit
  282. // along the forward vector.
  283. var frustumSlopeX = frustumSlopeY / aspectRatio;
  284. return new Vector2(frustumSlopeX, frustumSlopeY);
  285. }
  286. /**
  287. * Removes all animation locks. Allows new animations to be added to any of the arcCamera properties.
  288. */
  289. private _clearAnimationLocks(): void {
  290. this._betaIsAnimating = false;
  291. }
  292. /**
  293. * Applies any current user interaction to the camera. Takes into account maximum alpha rotation.
  294. */
  295. private _applyUserInteraction(): void {
  296. if (this._userIsMoving()) {
  297. this._lastInteractionTime = Tools.Now;
  298. this.stopAllAnimations();
  299. this._clearAnimationLocks();
  300. }
  301. }
  302. /**
  303. * Stops and removes all animations that have been applied to the camera
  304. */
  305. public stopAllAnimations(): void {
  306. this._attachedCamera.animations = [];
  307. while (this._animatables.length) {
  308. if (this._animatables[0]) {
  309. this._animatables[0].onAnimationEnd = null;
  310. this._animatables[0].stop();
  311. }
  312. this._animatables.shift();
  313. }
  314. }
  315. // Tools
  316. private _userIsMoving(): boolean {
  317. return this._attachedCamera.inertialAlphaOffset !== 0 ||
  318. this._attachedCamera.inertialBetaOffset !== 0 ||
  319. this._attachedCamera.inertialRadiusOffset !== 0 ||
  320. this._attachedCamera.inertialPanningX !== 0 ||
  321. this._attachedCamera.inertialPanningY !== 0 ||
  322. this._isPointerDown;
  323. }
  324. // Statics
  325. /**
  326. * The camera can move all the way towards the mesh.
  327. */
  328. public static IgnoreBoundsSizeMode = 0;
  329. /**
  330. * The camera is not allowed to zoom closer to the mesh than the point at which the adjusted bounding sphere touches the frustum sides
  331. */
  332. public static FitFrustumSidesMode = 1;
  333. }
  334. }