UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

206 lines (173 loc) • 8.59 kB
import { Context } from "../engine_setup.js"; import { DeviceUtilities, getParam } from "../engine_utils.js"; const debug = getParam("debugoverlay"); export const arContainerClassName = "ar"; export const quitARClassName = "quit-ar"; // https://developers.google.com/web/fundamentals/web-components/customelements /** @internal */ export class AROverlayHandler { get ARContainer(): HTMLElement | null { return this.arContainer; } private arContainer: HTMLElement | null = null; currentSession: XRSession | null = null; private _createdAROnlyElements: Array<any> = []; private _reparentedObjects: Array<{ el: Element, previousParent: HTMLElement | null }> = []; private contentElement: HTMLElement | null = null; private originalDomOverlayParent: ParentNode | null = null; requestEndAR = () => { this.onRequestedEndAR(); } onBegin(context: Context, overlayContainer: HTMLElement, session: XRSession) { this.currentSession = session; this.arContainer = overlayContainer; if (DeviceUtilities.isMozillaXR() || DeviceUtilities.isNeedleAppClip()) { const arElements = context.domElement!.children; for (let i = 0; i < arElements?.length; i++) { const el = arElements[i]; if (!el) return; if (el === this.arContainer) return; this._reparentedObjects.push({ el: el, previousParent: el.parentElement }); this.arContainer?.appendChild(el); } if (overlayContainer) { this.originalDomOverlayParent = overlayContainer.parentNode; if (this.originalDomOverlayParent) { console.log("Reparent DOM Overlay to body", overlayContainer, overlayContainer.style.display); // mozilla webxr does hide elements on session start // this is only necessary if we generated the overlay element overlayContainer.style.display = ""; overlayContainer.style.visibility = ""; document.body.appendChild(overlayContainer); } } else { console.warn("WebXRViewer: No DOM Overlay found"); } } this.ensureQuitARButton(this.arContainer); } onEnd(_context: Context) { // if (this.arContainer) // this.arContainer.classList.remove("ar-session-active"); for (const created of this._createdAROnlyElements) { if (created.remove) { created.remove(); } } for (const prev of this._reparentedObjects) { const el = prev.el as HTMLElement; prev.previousParent?.appendChild(el); } this._reparentedObjects.length = 0; // mozilla XR exit AR fixes if (DeviceUtilities.isMozillaXR()) { // without the timeout we get errors in mozillas code and can not enter XR again // not sure why we have to wait setTimeout(() => { // Canvas is not in DOM anymore after AR using Mozilla XR const canvas = _context.renderer.domElement; if (canvas) { _context.domElement.shadowRoot?.prepend(canvas); } // Fix visibility const elements = document.querySelectorAll("*"); for (var i = 0; i < elements.length; i++) { const child = elements[i] as any; if (child && child._displayChanged !== undefined && child._displayWas !== undefined) { child.style.display = child._displayWas; } } }, 10); } } createOverlayContainer(needleEngineElement: HTMLElement): HTMLElement { if (this.contentElement) return this.contentElement; if (debug) console.log("Setup overlay container"); const contentElement = needleEngineElement.shadowRoot!.querySelector(".content") as HTMLElement; this.contentElement = contentElement; const overlaySlot = needleEngineElement.shadowRoot!.querySelector(".overlay-content"); if (overlaySlot) contentElement.appendChild(overlaySlot); if (debug && !DeviceUtilities.isMobileDevice()) this.ensureQuitARButton(contentElement); return contentElement; } private onRequestedEndAR() { if (!this.currentSession) return; this.currentSession.end(); this.currentSession = null; } private ensureQuitARButton(element: HTMLElement) { const quitARSlot = document.createElement("slot"); quitARSlot.style.display = "contents"; quitARSlot.style.padding = "10px"; quitARSlot.setAttribute("name", "quit-ar"); this.appendElement(quitARSlot, element); this._createdAROnlyElements.push(quitARSlot); // for mozilla XR reparenting we have to make sure the close button is clickable so we set it on the element directly // it's in general perhaps more safe to set it on the element to ensure it's clickable quitARSlot.style.pointerEvents = "auto"; // No default quit button in the top right corner in app clips // we provide one via the native UI if (DeviceUtilities.isNeedleAppClip()) { // quitARSlot.style.display = "none"; globalThis["NEEDLE_ENGINE_APPCLIP_DISABLE_MENU"] = true; // respect the UI bar at the top of the screen and add some padding to the quit button container // @TODO: this should be done in CSS in one place and not here and in debug overlay const meta = document.querySelector('meta[name="viewport"]'); if (meta && !meta.getAttribute("content")?.includes("viewport-fit=")) { meta.setAttribute("content", meta.getAttribute("content") + ",viewport-fit=cover"); } } // We want to search the document if there's a quit-ar button // In which case we don't want to populate the default button (slot) with any content const quitARElement = document.querySelector(`.${quitARClassName}`); if (quitARElement) { quitARElement.addEventListener('click', this.requestEndAR); if (debug) quitARElement.addEventListener('click', () => console.log("Clicked quit-ar button")); // We found a explicit quit-ar element return; } quitARSlot.addEventListener('click', this.requestEndAR); if (debug) quitARSlot.addEventListener('click', () => console.log("Clicked fallback close button")); // we need another container to make sure the button is always on top const fixedButtonContainer = document.createElement("div"); fixedButtonContainer.style.cssText = ` position: fixed; top: 0; right: 0; z-index: 600; pointer-events: all; padding-top: env(safe-area-inset-top, 0px); padding-right: calc(env(safe-area-inset-right, 0px) + 10px); `; this.appendElement(fixedButtonContainer, quitARSlot); var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.classList.add("quit-ar-button"); svg.setAttribute('width', "40px"); svg.setAttribute('height', "40px"); svg.style.cssText = ` background: rgba(255, 255, 255, .4); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border-radius: 50%; box-shadow: 0 0 5px rgba(0,0,0,.3); outline: 1px solid rgba(255, 255, 255, .6); display: flex; justify-content: center; align-items: center; `; fixedButtonContainer.appendChild(svg); var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M 12,12 L 28,28 M 28,12 12,28'); path.setAttribute('stroke', '#000000'); path.setAttribute('stroke-width', "2px"); path.style.cssText = ` /**filter: drop-shadow(0 0px 1.2px rgba(0,0,0,.7));**/ ` svg.appendChild(path); if (debug) console.log("Created fallback close button", svg, element); } private appendElement(element: Element, parent: HTMLElement) { if (parent.shadowRoot) return parent.shadowRoot.appendChild(element); return parent.appendChild(element); } }