webXREnterExitUI.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import { Observable } from "../Misc/observable.js";
  2. import { WebXRState } from "./webXRTypes.js";
  3. import { Tools } from "../Misc/tools.js";
  4. /**
  5. * Button which can be used to enter a different mode of XR
  6. */
  7. export class WebXREnterExitUIButton {
  8. /**
  9. * Creates a WebXREnterExitUIButton
  10. * @param element button element
  11. * @param sessionMode XR initialization session mode
  12. * @param referenceSpaceType the type of reference space to be used
  13. */
  14. constructor(
  15. /** button element */
  16. element,
  17. /** XR initialization options for the button */
  18. sessionMode,
  19. /** Reference space type */
  20. referenceSpaceType) {
  21. this.element = element;
  22. this.sessionMode = sessionMode;
  23. this.referenceSpaceType = referenceSpaceType;
  24. }
  25. /**
  26. * Extendable function which can be used to update the button's visuals when the state changes
  27. * @param activeButton the current active button in the UI
  28. */
  29. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  30. update(activeButton) { }
  31. }
  32. /**
  33. * Options to create the webXR UI
  34. */
  35. export class WebXREnterExitUIOptions {
  36. }
  37. /**
  38. * UI to allow the user to enter/exit XR mode
  39. */
  40. export class WebXREnterExitUI {
  41. /**
  42. * Construct a new EnterExit UI class
  43. *
  44. * @param _scene babylon scene object to use
  45. * @param options (read-only) version of the options passed to this UI
  46. */
  47. constructor(_scene,
  48. /** version of the options passed to this UI */
  49. options) {
  50. this._scene = _scene;
  51. this.options = options;
  52. this._activeButton = null;
  53. this._buttons = [];
  54. /**
  55. * Fired every time the active button is changed.
  56. *
  57. * When xr is entered via a button that launches xr that button will be the callback parameter
  58. *
  59. * When exiting xr the callback parameter will be null)
  60. */
  61. this.activeButtonChangedObservable = new Observable();
  62. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  63. this._onSessionGranted = (evt) => {
  64. // This section is for future reference.
  65. // As per specs, evt.session.mode should have the supported session mode, but no browser supports it for now.
  66. // // check if the session granted is the same as the one requested
  67. // const grantedMode = (evt.session as any).mode;
  68. // if (grantedMode) {
  69. // this._buttons.some((btn, idx) => {
  70. // if (btn.sessionMode === grantedMode) {
  71. // this._enterXRWithButtonIndex(idx);
  72. // return true;
  73. // }
  74. // return false;
  75. // });
  76. // } else
  77. if (this._helper) {
  78. this._enterXRWithButtonIndex(0);
  79. }
  80. };
  81. this.overlay = document.createElement("div");
  82. this.overlay.classList.add("xr-button-overlay");
  83. // prepare for session granted event
  84. if (!options.ignoreSessionGrantedEvent && navigator.xr) {
  85. navigator.xr.addEventListener("sessiongranted", this._onSessionGranted);
  86. }
  87. // if served over HTTP, warn people.
  88. // Hopefully the browsers will catch up
  89. if (typeof window !== "undefined") {
  90. if (window.location && window.location.protocol === "http:" && window.location.hostname !== "localhost") {
  91. Tools.Warn("WebXR can only be served over HTTPS");
  92. throw new Error("WebXR can only be served over HTTPS");
  93. }
  94. }
  95. if (options.customButtons) {
  96. this._buttons = options.customButtons;
  97. }
  98. else {
  99. this.overlay.style.cssText = "z-index:11;position: absolute; right: 20px;bottom: 50px;";
  100. const sessionMode = options.sessionMode || "immersive-vr";
  101. const referenceSpaceType = options.referenceSpaceType || "local-floor";
  102. const url = typeof SVGSVGElement === "undefined"
  103. ? "https://cdn.babylonjs.com/Assets/vrButton.png"
  104. : "data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%222048%22%20height%3D%221152%22%20viewBox%3D%220%200%202048%201152%22%20version%3D%221.1%22%3E%3Cpath%20transform%3D%22rotate%28180%201024%2C576.0000000000001%29%22%20d%3D%22m1109%2C896q17%2C0%2030%2C-12t13%2C-30t-12.5%2C-30.5t-30.5%2C-12.5l-170%2C0q-18%2C0%20-30.5%2C12.5t-12.5%2C30.5t13%2C30t30%2C12l170%2C0zm-85%2C256q59%2C0%20132.5%2C-1.5t154.5%2C-5.5t164.5%2C-11.5t163%2C-20t150%2C-30t124.5%2C-41.5q23%2C-11%2042%2C-24t38%2C-30q27%2C-25%2041%2C-61.5t14%2C-72.5l0%2C-257q0%2C-123%20-47%2C-232t-128%2C-190t-190%2C-128t-232%2C-47l-81%2C0q-37%2C0%20-68.5%2C14t-60.5%2C34.5t-55.5%2C45t-53%2C45t-53%2C34.5t-55.5%2C14t-55.5%2C-14t-53%2C-34.5t-53%2C-45t-55.5%2C-45t-60.5%2C-34.5t-68.5%2C-14l-81%2C0q-123%2C0%20-232%2C47t-190%2C128t-128%2C190t-47%2C232l0%2C257q0%2C68%2038%2C115t97%2C73q54%2C24%20124.5%2C41.5t150%2C30t163%2C20t164.5%2C11.5t154.5%2C5.5t132.5%2C1.5zm939%2C-298q0%2C39%20-24.5%2C67t-58.5%2C42q-54%2C23%20-122%2C39.5t-143.5%2C28t-155.5%2C19t-157%2C11t-148.5%2C5t-129.5%2C1.5q-59%2C0%20-130%2C-1.5t-148%2C-5t-157%2C-11t-155.5%2C-19t-143.5%2C-28t-122%2C-39.5q-34%2C-14%20-58.5%2C-42t-24.5%2C-67l0%2C-257q0%2C-106%2040.5%2C-199t110%2C-162.5t162.5%2C-109.5t199%2C-40l81%2C0q27%2C0%2052%2C14t50%2C34.5t51%2C44.5t55.5%2C44.5t63.5%2C34.5t74%2C14t74%2C-14t63.5%2C-34.5t55.5%2C-44.5t51%2C-44.5t50%2C-34.5t52%2C-14l14%2C0q37%2C0%2070%2C0.5t64.5%2C4.5t63.5%2C12t68%2C23q71%2C30%20128.5%2C78.5t98.5%2C110t63.5%2C133.5t22.5%2C149l0%2C257z%22%20fill%3D%22white%22%20/%3E%3C/svg%3E%0A";
  105. let css = ".babylonVRicon { color: #868686; border-color: #868686; border-style: solid; margin-left: 10px; height: 50px; width: 80px; background-color: rgba(51,51,51,0.7); background-image: url(" +
  106. url +
  107. "); background-size: 80%; background-repeat:no-repeat; background-position: center; border: none; outline: none; transition: transform 0.125s ease-out } .babylonVRicon:hover { transform: scale(1.05) } .babylonVRicon:active {background-color: rgba(51,51,51,1) } .babylonVRicon:focus {background-color: rgba(51,51,51,1) }";
  108. css += '.babylonVRicon.vrdisplaypresenting { background-image: none;} .vrdisplaypresenting::after { content: "EXIT"} .xr-error::after { content: "ERROR"}';
  109. const style = document.createElement("style");
  110. style.appendChild(document.createTextNode(css));
  111. document.getElementsByTagName("head")[0].appendChild(style);
  112. const hmdBtn = document.createElement("button");
  113. hmdBtn.className = "babylonVRicon";
  114. hmdBtn.title = `${sessionMode} - ${referenceSpaceType}`;
  115. this._buttons.push(new WebXREnterExitUIButton(hmdBtn, sessionMode, referenceSpaceType));
  116. this._buttons[this._buttons.length - 1].update = function (activeButton) {
  117. this.element.style.display = activeButton === null || activeButton === this ? "" : "none";
  118. hmdBtn.className = "babylonVRicon" + (activeButton === this ? " vrdisplaypresenting" : "");
  119. };
  120. this._updateButtons(null);
  121. }
  122. const renderCanvas = _scene.getEngine().getInputElement();
  123. if (renderCanvas && renderCanvas.parentNode) {
  124. renderCanvas.parentNode.appendChild(this.overlay);
  125. _scene.onDisposeObservable.addOnce(() => {
  126. this.dispose();
  127. });
  128. }
  129. }
  130. /**
  131. * Set the helper to be used with this UI component.
  132. * The UI is bound to an experience helper. If not provided the UI can still be used but the events should be registered by the developer.
  133. *
  134. * @param helper the experience helper to attach
  135. * @param renderTarget an optional render target (in case it is created outside of the helper scope)
  136. * @returns a promise that resolves when the ui is ready
  137. */
  138. async setHelperAsync(helper, renderTarget) {
  139. this._helper = helper;
  140. this._renderTarget = renderTarget;
  141. const supportedPromises = this._buttons.map((btn) => {
  142. return helper.sessionManager.isSessionSupportedAsync(btn.sessionMode);
  143. });
  144. helper.onStateChangedObservable.add((state) => {
  145. if (state == WebXRState.NOT_IN_XR) {
  146. this._updateButtons(null);
  147. }
  148. });
  149. const results = await Promise.all(supportedPromises);
  150. results.forEach((supported, i) => {
  151. if (supported) {
  152. this.overlay.appendChild(this._buttons[i].element);
  153. this._buttons[i].element.onclick = this._enterXRWithButtonIndex.bind(this, i);
  154. }
  155. else {
  156. Tools.Warn(`Session mode "${this._buttons[i].sessionMode}" not supported in browser`);
  157. }
  158. });
  159. }
  160. /**
  161. * Creates UI to allow the user to enter/exit XR mode
  162. * @param scene the scene to add the ui to
  163. * @param helper the xr experience helper to enter/exit xr with
  164. * @param options options to configure the UI
  165. * @returns the created ui
  166. */
  167. static async CreateAsync(scene, helper, options) {
  168. const ui = new WebXREnterExitUI(scene, options);
  169. await ui.setHelperAsync(helper, options.renderTarget || undefined);
  170. return ui;
  171. }
  172. async _enterXRWithButtonIndex(idx = 0) {
  173. if (this._helper.state == WebXRState.IN_XR) {
  174. await this._helper.exitXRAsync();
  175. this._updateButtons(null);
  176. }
  177. else if (this._helper.state == WebXRState.NOT_IN_XR) {
  178. try {
  179. await this._helper.enterXRAsync(this._buttons[idx].sessionMode, this._buttons[idx].referenceSpaceType, this._renderTarget, {
  180. optionalFeatures: this.options.optionalFeatures,
  181. requiredFeatures: this.options.requiredFeatures,
  182. });
  183. this._updateButtons(this._buttons[idx]);
  184. }
  185. catch (e) {
  186. // make sure button is visible
  187. this._updateButtons(null);
  188. const element = this._buttons[idx].element;
  189. const prevTitle = element.title;
  190. element.title = "Error entering XR session : " + prevTitle;
  191. element.classList.add("xr-error");
  192. if (this.options.onError) {
  193. this.options.onError(e);
  194. }
  195. }
  196. }
  197. }
  198. /**
  199. * Disposes of the XR UI component
  200. */
  201. dispose() {
  202. const renderCanvas = this._scene.getEngine().getInputElement();
  203. if (renderCanvas && renderCanvas.parentNode && renderCanvas.parentNode.contains(this.overlay)) {
  204. renderCanvas.parentNode.removeChild(this.overlay);
  205. }
  206. this.activeButtonChangedObservable.clear();
  207. navigator.xr.removeEventListener("sessiongranted", this._onSessionGranted);
  208. }
  209. _updateButtons(activeButton) {
  210. this._activeButton = activeButton;
  211. this._buttons.forEach((b) => {
  212. b.update(this._activeButton);
  213. });
  214. this.activeButtonChangedObservable.notifyObservers(this._activeButton);
  215. }
  216. }
  217. //# sourceMappingURL=webXREnterExitUI.js.map