@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.
187 lines (158 loc) • 7.76 kB
text/typescript
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()) {
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.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";
// 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;
`;
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);
}
}