"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const three_module = require("../../../../../../three@0.154.0/node_modules/three/build/three.module.js");
const XREstimatedLight = require("../../../../../../three@0.154.0/node_modules/three/examples/jsm/webxr/XREstimatedLight.js");
const environment = require("../features/environment.js");
const modelViewerBase = require("../model-viewer-base.js");
const utilities = require("../utilities.js");
const Damper = require("./Damper.js");
const PlacementBox = require("./PlacementBox.js");
const SmoothControls = require("./SmoothControls.js");
/* @license
 * Copyright 2019 Google LLC. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
const INIT_FRAMES = 30;
const AR_SHADOW_INTENSITY = 0.8;
const ROTATION_RATE = 1.5;
const HIT_ANGLE_DEG = 20;
const SCALE_SNAP_HIGH = 1.3;
const SCALE_SNAP_LOW = 1 / SCALE_SNAP_HIGH;
const MIN_VIEWPORT_SCALE = 0.25;
const MAX_DISTANCE = 10;
const ARStatus = {
  NOT_PRESENTING: "not-presenting",
  SESSION_STARTED: "session-started",
  OBJECT_PLACED: "object-placed",
  FAILED: "failed"
};
const ARTracking = {
  TRACKING: "tracking",
  NOT_TRACKING: "not-tracking"
};
const vector3 = new three_module.Vector3();
const matrix4 = new three_module.Matrix4();
const hitPosition = new three_module.Vector3();
const camera = new three_module.PerspectiveCamera(45, 1, 0.1, 100);
class ARRenderer extends three_module.EventDispatcher {
  constructor(renderer) {
    super();
    this.renderer = renderer;
    this.currentSession = null;
    this.placeOnWall = false;
    this.placementBox = null;
    this.lastTick = null;
    this.turntableRotation = null;
    this.oldShadowIntensity = null;
    this.frame = null;
    this.initialHitSource = null;
    this.transientHitTestSource = null;
    this.inputSource = null;
    this._presentedScene = null;
    this.resolveCleanup = null;
    this.exitWebXRButtonContainer = null;
    this.overlay = null;
    this.xrLight = null;
    this.tracking = true;
    this.frames = 0;
    this.initialized = false;
    this.oldTarget = new three_module.Vector3();
    this.placementComplete = false;
    this.isTranslating = false;
    this.isRotating = false;
    this.isTwoFingering = false;
    this.lastDragPosition = new three_module.Vector3();
    this.firstRatio = 0;
    this.lastAngle = 0;
    this.goalPosition = new three_module.Vector3();
    this.goalYaw = 0;
    this.goalScale = 1;
    this.xDamper = new Damper.Damper();
    this.yDamper = new Damper.Damper();
    this.zDamper = new Damper.Damper();
    this.yawDamper = new Damper.Damper();
    this.scaleDamper = new Damper.Damper();
    this.onExitWebXRButtonContainerClick = () => this.stopPresenting();
    this.onUpdateScene = () => {
      if (this.placementBox != null && this.isPresenting) {
        this.placementBox.dispose();
        this.placementBox = new PlacementBox.PlacementBox(this.presentedScene, this.placeOnWall ? "back" : "bottom");
      }
    };
    this.onSelectStart = (event) => {
      const hitSource = this.transientHitTestSource;
      if (hitSource == null) {
        return;
      }
      const fingers = this.frame.getHitTestResultsForTransientInput(hitSource);
      const scene = this.presentedScene;
      const box = this.placementBox;
      if (fingers.length === 1) {
        this.inputSource = event.inputSource;
        const { axes } = this.inputSource.gamepad;
        const hitPosition2 = box.getHit(this.presentedScene, axes[0], axes[1]);
        box.show = true;
        if (hitPosition2 != null) {
          this.isTranslating = true;
          this.lastDragPosition.copy(hitPosition2);
        } else if (this.placeOnWall === false) {
          this.isRotating = true;
          this.lastAngle = axes[0] * ROTATION_RATE;
        }
      } else if (fingers.length === 2) {
        box.show = true;
        this.isTwoFingering = true;
        const { separation } = this.fingerPolar(fingers);
        this.firstRatio = separation / scene.scale.x;
      }
    };
    this.onSelectEnd = () => {
      this.isTranslating = false;
      this.isRotating = false;
      this.isTwoFingering = false;
      this.inputSource = null;
      this.goalPosition.y += this.placementBox.offsetHeight * this.presentedScene.scale.x;
      this.placementBox.show = false;
    };
    this.threeRenderer = renderer.threeRenderer;
    this.threeRenderer.xr.enabled = true;
  }
  async resolveARSession() {
    utilities.assertIsArCandidate();
    const session = await navigator.xr.requestSession("immersive-ar", {
      requiredFeatures: ["hit-test"],
      optionalFeatures: ["dom-overlay", "light-estimation"],
      domOverlay: this.overlay ? { root: this.overlay } : void 0
    });
    this.threeRenderer.xr.setReferenceSpaceType("local");
    await this.threeRenderer.xr.setSession(session);
    this.threeRenderer.xr.cameraAutoUpdate = false;
    return session;
  }
  /**
   * The currently presented scene, if any
   */
  get presentedScene() {
    return this._presentedScene;
  }
  /**
   * Resolves to true if the renderer has detected all the necessary qualities
   * to support presentation in AR.
   */
  async supportsPresentation() {
    try {
      utilities.assertIsArCandidate();
      return await navigator.xr.isSessionSupported("immersive-ar");
    } catch (error) {
      console.warn("Request to present in WebXR denied:");
      console.warn(error);
      console.warn("Falling back to next ar-mode");
      return false;
    }
  }
  /**
   * Present a scene in AR
   */
  async present(scene, environmentEstimation = false) {
    if (this.isPresenting) {
      console.warn("Cannot present while a model is already presenting");
    }
    let waitForAnimationFrame = new Promise((resolve, _reject) => {
      requestAnimationFrame(() => resolve());
    });
    scene.setHotspotsVisibility(false);
    scene.queueRender();
    await waitForAnimationFrame;
    this._presentedScene = scene;
    this.overlay = scene.element.shadowRoot.querySelector("div.default");
    if (environmentEstimation === true) {
      this.xrLight = new XREstimatedLight.XREstimatedLight(this.threeRenderer);
      this.xrLight.addEventListener("estimationstart", () => {
        if (!this.isPresenting || this.xrLight == null) {
          return;
        }
        const scene2 = this.presentedScene;
        scene2.add(this.xrLight);
        scene2.environment = this.xrLight.environment;
      });
    }
    const currentSession = await this.resolveARSession();
    currentSession.addEventListener("end", () => {
      this.postSessionCleanup();
    }, { once: true });
    const exitButton = scene.element.shadowRoot.querySelector(".slot.exit-webxr-ar-button");
    exitButton.classList.add("enabled");
    exitButton.addEventListener("click", this.onExitWebXRButtonContainerClick);
    this.exitWebXRButtonContainer = exitButton;
    const viewerRefSpace = await currentSession.requestReferenceSpace("viewer");
    this.tracking = true;
    this.frames = 0;
    this.initialized = false;
    this.turntableRotation = scene.yaw;
    this.goalYaw = scene.yaw;
    this.goalScale = 1;
    scene.background = null;
    this.oldShadowIntensity = scene.shadowIntensity;
    scene.setShadowIntensity(0.01);
    this.oldTarget.copy(scene.getTarget());
    scene.element.addEventListener("load", this.onUpdateScene);
    const radians = HIT_ANGLE_DEG * Math.PI / 180;
    const ray = this.placeOnWall === true ? void 0 : new XRRay(new DOMPoint(0, 0, 0), { x: 0, y: -Math.sin(radians), z: -Math.cos(radians) });
    currentSession.requestHitTestSource({ space: viewerRefSpace, offsetRay: ray }).then((hitTestSource) => {
      this.initialHitSource = hitTestSource;
    });
    this.currentSession = currentSession;
    this.placementBox = new PlacementBox.PlacementBox(scene, this.placeOnWall ? "back" : "bottom");
    this.placementComplete = false;
    this.lastTick = performance.now();
    this.dispatchEvent({ type: "status", status: ARStatus.SESSION_STARTED });
  }
  /**
   * If currently presenting a scene in AR, stops presentation and exits AR.
   */
  async stopPresenting() {
    if (!this.isPresenting) {
      return;
    }
    const cleanupPromise = new Promise((resolve) => {
      this.resolveCleanup = resolve;
    });
    try {
      await this.currentSession.end();
      await cleanupPromise;
    } catch (error) {
      console.warn("Error while trying to end WebXR AR session");
      console.warn(error);
      this.postSessionCleanup();
    }
  }
  /**
   * True if a scene is currently in the process of being presented in AR
   */
  get isPresenting() {
    return this.presentedScene != null;
  }
  get target() {
    return this.oldTarget;
  }
  updateTarget() {
    const scene = this.presentedScene;
    if (scene != null) {
      const target = scene.getTarget();
      this.oldTarget.copy(target);
      if (this.placeOnWall) {
        target.z = scene.boundingBox.min.z;
      } else {
        target.y = scene.boundingBox.min.y;
      }
      scene.setTarget(target.x, target.y, target.z);
    }
  }
  postSessionCleanup() {
    const session = this.currentSession;
    if (session != null) {
      session.removeEventListener("selectstart", this.onSelectStart);
      session.removeEventListener("selectend", this.onSelectEnd);
      this.currentSession = null;
    }
    const scene = this.presentedScene;
    this._presentedScene = null;
    if (scene != null) {
      const { element } = scene;
      if (this.xrLight != null) {
        scene.remove(this.xrLight);
        this.xrLight.dispose();
        this.xrLight = null;
      }
      scene.position.set(0, 0, 0);
      scene.scale.set(1, 1, 1);
      scene.setShadowOffset(0);
      const yaw = this.turntableRotation;
      if (yaw != null) {
        scene.yaw = yaw;
      }
      const intensity = this.oldShadowIntensity;
      if (intensity != null) {
        scene.setShadowIntensity(intensity);
      }
      scene.setEnvironmentAndSkybox(element[environment.$currentEnvironmentMap], element[environment.$currentBackground]);
      const point = this.oldTarget;
      scene.setTarget(point.x, point.y, point.z);
      scene.xrCamera = null;
      scene.element.removeEventListener("load", this.onUpdateScene);
      scene.orientHotspots(0);
      element.requestUpdate("cameraTarget");
      element.requestUpdate("maxCameraOrbit");
      element[modelViewerBase.$onResize](element.getBoundingClientRect());
      requestAnimationFrame(() => {
        scene.element.dispatchEvent(new CustomEvent("camera-change", { detail: { source: SmoothControls.ChangeSource.NONE } }));
      });
    }
    this.renderer.height = 0;
    const exitButton = this.exitWebXRButtonContainer;
    if (exitButton != null) {
      exitButton.classList.remove("enabled");
      exitButton.removeEventListener("click", this.onExitWebXRButtonContainerClick);
      this.exitWebXRButtonContainer = null;
    }
    const hitSource = this.transientHitTestSource;
    if (hitSource != null) {
      hitSource.cancel();
      this.transientHitTestSource = null;
    }
    const hitSourceInitial = this.initialHitSource;
    if (hitSourceInitial != null) {
      hitSourceInitial.cancel();
      this.initialHitSource = null;
    }
    if (this.placementBox != null) {
      this.placementBox.dispose();
      this.placementBox = null;
    }
    this.lastTick = null;
    this.turntableRotation = null;
    this.oldShadowIntensity = null;
    this.frame = null;
    this.inputSource = null;
    this.overlay = null;
    if (this.resolveCleanup != null) {
      this.resolveCleanup();
    }
    this.dispatchEvent({ type: "status", status: ARStatus.NOT_PRESENTING });
  }
  updateView(view) {
    const scene = this.presentedScene;
    const xr = this.threeRenderer.xr;
    xr.updateCamera(camera);
    scene.xrCamera = xr.getCamera();
    const { elements } = scene.getCamera().matrixWorld;
    scene.orientHotspots(Math.atan2(elements[1], elements[5]));
    if (!this.initialized) {
      this.placeInitially();
      this.initialized = true;
    }
    if (view.requestViewportScale && view.recommendedViewportScale) {
      const scale = view.recommendedViewportScale;
      view.requestViewportScale(Math.max(scale, MIN_VIEWPORT_SCALE));
    }
    const layer = xr.getBaseLayer();
    if (layer != null) {
      const viewport = layer instanceof XRWebGLLayer ? layer.getViewport(view) : xr.getBinding().getViewSubImage(layer, view).viewport;
      this.threeRenderer.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
    }
  }
  placeInitially() {
    const scene = this.presentedScene;
    const { position, element } = scene;
    const xrCamera = scene.getCamera();
    const { width, height } = this.overlay.getBoundingClientRect();
    scene.setSize(width, height);
    xrCamera.projectionMatrixInverse.copy(xrCamera.projectionMatrix).invert();
    const { theta, radius } = element.getCameraOrbit();
    const cameraDirection = xrCamera.getWorldDirection(vector3);
    scene.yaw = Math.atan2(-cameraDirection.x, -cameraDirection.z) - theta;
    this.goalYaw = scene.yaw;
    position.copy(xrCamera.position).add(cameraDirection.multiplyScalar(radius));
    this.updateTarget();
    const target = scene.getTarget();
    position.add(target).sub(this.oldTarget);
    this.goalPosition.copy(position);
    scene.setHotspotsVisibility(true);
    const { session } = this.frame;
    session.addEventListener("selectstart", this.onSelectStart);
    session.addEventListener("selectend", this.onSelectEnd);
    session.requestHitTestSourceForTransientInput({ profile: "generic-touchscreen" }).then((hitTestSource) => {
      this.transientHitTestSource = hitTestSource;
    });
  }
  getTouchLocation() {
    const { axes } = this.inputSource.gamepad;
    let location = this.placementBox.getExpandedHit(this.presentedScene, axes[0], axes[1]);
    if (location != null) {
      vector3.copy(location).sub(this.presentedScene.getCamera().position);
      if (vector3.length() > MAX_DISTANCE)
        return null;
    }
    return location;
  }
  getHitPoint(hitResult) {
    const refSpace = this.threeRenderer.xr.getReferenceSpace();
    const pose = hitResult.getPose(refSpace);
    if (pose == null) {
      return null;
    }
    const hitMatrix = matrix4.fromArray(pose.transform.matrix);
    if (this.placeOnWall === true) {
      this.goalYaw = Math.atan2(hitMatrix.elements[4], hitMatrix.elements[6]);
    }
    return hitMatrix.elements[5] > 0.75 !== this.placeOnWall ? hitPosition.setFromMatrixPosition(hitMatrix) : null;
  }
  moveToFloor(frame) {
    const hitSource = this.initialHitSource;
    if (hitSource == null) {
      return;
    }
    const hitTestResults = frame.getHitTestResults(hitSource);
    if (hitTestResults.length == 0) {
      return;
    }
    const hit = hitTestResults[0];
    const hitPoint = this.getHitPoint(hit);
    if (hitPoint == null) {
      return;
    }
    this.placementBox.show = true;
    if (!this.isTranslating) {
      if (this.placeOnWall) {
        this.goalPosition.copy(hitPoint);
      } else {
        this.goalPosition.y = hitPoint.y;
      }
    }
    hitSource.cancel();
    this.initialHitSource = null;
    this.dispatchEvent({ type: "status", status: ARStatus.OBJECT_PLACED });
  }
  fingerPolar(fingers) {
    const fingerOne = fingers[0].inputSource.gamepad.axes;
    const fingerTwo = fingers[1].inputSource.gamepad.axes;
    const deltaX = fingerTwo[0] - fingerOne[0];
    const deltaY = fingerTwo[1] - fingerOne[1];
    const angle = Math.atan2(deltaY, deltaX);
    let deltaYaw = this.lastAngle - angle;
    if (deltaYaw > Math.PI) {
      deltaYaw -= 2 * Math.PI;
    } else if (deltaYaw < -Math.PI) {
      deltaYaw += 2 * Math.PI;
    }
    this.lastAngle = angle;
    return {
      separation: Math.sqrt(deltaX * deltaX + deltaY * deltaY),
      deltaYaw
    };
  }
  processInput(frame) {
    const hitSource = this.transientHitTestSource;
    if (hitSource == null) {
      return;
    }
    if (!this.isTranslating && !this.isTwoFingering && !this.isRotating) {
      return;
    }
    const fingers = frame.getHitTestResultsForTransientInput(hitSource);
    const scene = this.presentedScene;
    const scale = scene.scale.x;
    if (this.isTwoFingering) {
      if (fingers.length < 2) {
        this.isTwoFingering = false;
      } else {
        const { separation, deltaYaw } = this.fingerPolar(fingers);
        if (this.placeOnWall === false) {
          this.goalYaw += deltaYaw;
        }
        if (scene.canScale) {
          const scale2 = separation / this.firstRatio;
          this.goalScale = scale2 < SCALE_SNAP_HIGH && scale2 > SCALE_SNAP_LOW ? 1 : scale2;
        }
      }
      return;
    } else if (fingers.length === 2) {
      this.isTranslating = false;
      this.isRotating = false;
      this.isTwoFingering = true;
      const { separation } = this.fingerPolar(fingers);
      this.firstRatio = separation / scale;
      return;
    }
    if (this.isRotating) {
      const angle = this.inputSource.gamepad.axes[0] * ROTATION_RATE;
      this.goalYaw += angle - this.lastAngle;
      this.lastAngle = angle;
    } else if (this.isTranslating) {
      fingers.forEach((finger) => {
        if (finger.inputSource !== this.inputSource) {
          return;
        }
        let hit = null;
        if (finger.results.length > 0) {
          hit = this.getHitPoint(finger.results[0]);
        }
        if (hit == null) {
          hit = this.getTouchLocation();
        }
        if (hit == null) {
          return;
        }
        this.goalPosition.sub(this.lastDragPosition);
        if (this.placeOnWall === false) {
          const offset = hit.y - this.lastDragPosition.y;
          if (offset < 0) {
            this.placementBox.offsetHeight = offset / scale;
            this.presentedScene.setShadowOffset(offset);
            const cameraPosition = vector3.copy(scene.getCamera().position);
            const alpha = -offset / (cameraPosition.y - hit.y);
            cameraPosition.multiplyScalar(alpha);
            hit.multiplyScalar(1 - alpha).add(cameraPosition);
          }
        }
        this.goalPosition.add(hit);
        this.lastDragPosition.copy(hit);
      });
    }
  }
  moveScene(delta) {
    const scene = this.presentedScene;
    const { position, yaw } = scene;
    const boundingRadius = scene.boundingSphere.radius;
    const goal = this.goalPosition;
    const oldScale = scene.scale.x;
    const box = this.placementBox;
    let source = SmoothControls.ChangeSource.NONE;
    if (!goal.equals(position) || this.goalScale !== oldScale) {
      source = SmoothControls.ChangeSource.USER_INTERACTION;
      let { x, y, z } = position;
      x = this.xDamper.update(x, goal.x, delta, boundingRadius);
      y = this.yDamper.update(y, goal.y, delta, boundingRadius);
      z = this.zDamper.update(z, goal.z, delta, boundingRadius);
      position.set(x, y, z);
      const newScale = this.scaleDamper.update(oldScale, this.goalScale, delta, 1);
      scene.scale.set(newScale, newScale, newScale);
      if (!this.isTranslating) {
        const offset = goal.y - y;
        if (this.placementComplete && this.placeOnWall === false) {
          box.offsetHeight = offset / newScale;
          scene.setShadowOffset(offset);
        } else if (offset === 0) {
          this.placementComplete = true;
          box.show = false;
          scene.setShadowIntensity(AR_SHADOW_INTENSITY);
        }
      }
    }
    box.updateOpacity(delta);
    scene.updateTarget(delta);
    scene.yaw = this.yawDamper.update(yaw, this.goalYaw, delta, Math.PI);
    scene.element.dispatchEvent(new CustomEvent("camera-change", { detail: { source } }));
  }
  /**
   * Only public to make it testable.
   */
  onWebXRFrame(time, frame) {
    this.frame = frame;
    ++this.frames;
    const refSpace = this.threeRenderer.xr.getReferenceSpace();
    const pose = frame.getViewerPose(refSpace);
    if (pose == null && this.tracking === true && this.frames > INIT_FRAMES) {
      this.tracking = false;
      this.dispatchEvent({ type: "tracking", status: ARTracking.NOT_TRACKING });
    }
    const scene = this.presentedScene;
    if (pose == null || scene == null || !scene.element.loaded) {
      this.threeRenderer.clear();
      return;
    }
    if (this.tracking === false) {
      this.tracking = true;
      this.dispatchEvent({ type: "tracking", status: ARTracking.TRACKING });
    }
    let isFirstView = true;
    for (const view of pose.views) {
      this.updateView(view);
      if (isFirstView) {
        this.moveToFloor(frame);
        this.processInput(frame);
        const delta = time - this.lastTick;
        this.moveScene(delta);
        this.renderer.preRender(scene, time, delta);
        this.lastTick = time;
        scene.renderShadow(this.threeRenderer);
      }
      this.threeRenderer.render(scene, scene.getCamera());
      isFirstView = false;
    }
  }
}
exports.ARRenderer = ARRenderer;
exports.ARStatus = ARStatus;
exports.ARTracking = ARTracking;
//# sourceMappingURL=ARRenderer.js.map
