import { Vector3, Raycaster, Vector2, Scene, PerspectiveCamera, Object3D, Box3, Sphere, AnimationMixer, Euler, LoopPingPong, LoopRepeat, Matrix3, Triangle } from "../../../../../../three@0.154.0/node_modules/three/build/three.module.js";
import { CSS2DRenderer } from "../../../../../../three@0.154.0/node_modules/three/examples/jsm/renderers/CSS2DRenderer.js";
import { reduceVertices } from "../../../../../../three@0.154.0/node_modules/three/examples/jsm/utils/SceneUtils.js";
import { $currentGLTF, $originalGltfJson, $model } from "../features/scene-graph.js";
import { $nodeFromPoint, $nodeFromIndex } from "../features/scene-graph/model.js";
import { $renderer } from "../model-viewer-base.js";
import { normalizeUnit } from "../styles/conversions.js";
import { parseExpressions } from "../styles/parsers.js";
import { resolveDpr } from "../utilities.js";
import { Damper, SETTLING_TIME } from "./Damper.js";
import { Hotspot } from "./Hotspot.js";
import { Shadow } from "./Shadow.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 MIN_SHADOW_RATIO = 100;
const view = new Vector3();
const target = new Vector3();
const normalWorld = new Vector3();
const raycaster = new Raycaster();
const vector3 = new Vector3();
const ndc = new Vector2();
class ModelScene extends Scene {
  constructor({ canvas, element, width, height }) {
    super();
    this.annotationRenderer = new CSS2DRenderer();
    this.effectRenderer = null;
    this.schemaElement = document.createElement("script");
    this.width = 1;
    this.height = 1;
    this.aspect = 1;
    this.scaleStep = 0;
    this.renderCount = 0;
    this.externalRenderer = null;
    this.camera = new PerspectiveCamera(45, 1, 0.1, 100);
    this.xrCamera = null;
    this.url = null;
    this.target = new Object3D();
    this.animationNames = [];
    this.boundingBox = new Box3();
    this.boundingSphere = new Sphere();
    this.size = new Vector3();
    this.idealAspect = 0;
    this.framedFoVDeg = 0;
    this.shadow = null;
    this.shadowIntensity = 0;
    this.shadowSoftness = 1;
    this.bakedShadows = /* @__PURE__ */ new Set();
    this.exposure = 1;
    this.canScale = true;
    this.isDirty = false;
    this.goalTarget = new Vector3();
    this.targetDamperX = new Damper();
    this.targetDamperY = new Damper();
    this.targetDamperZ = new Damper();
    this._currentGLTF = null;
    this._model = null;
    this.cancelPendingSourceChange = null;
    this.animationsByName = /* @__PURE__ */ new Map();
    this.currentAnimationAction = null;
    this.name = "ModelScene";
    this.element = element;
    this.canvas = canvas;
    this.camera = new PerspectiveCamera(45, 1, 0.1, 100);
    this.camera.name = "MainCamera";
    this.add(this.target);
    this.setSize(width, height);
    this.target.name = "Target";
    this.mixer = new AnimationMixer(this.target);
    const { domElement } = this.annotationRenderer;
    const { style } = domElement;
    style.display = "none";
    style.pointerEvents = "none";
    style.position = "absolute";
    style.top = "0";
    this.element.shadowRoot.querySelector(".default").appendChild(domElement);
    this.schemaElement.setAttribute("type", "application/ld+json");
  }
  /**
   * Function to create the context lazily, as when there is only one
   * <model-viewer> element, the renderer's 3D context can be displayed
   * directly. This extra context is necessary to copy the renderings into when
   * there are more than one.
   */
  get context() {
    return this.canvas.getContext("2d");
  }
  getCamera() {
    return this.xrCamera != null ? this.xrCamera : this.camera;
  }
  queueRender() {
    this.isDirty = true;
  }
  shouldRender() {
    return this.isDirty;
  }
  hasRendered() {
    this.isDirty = false;
  }
  forceRescale() {
    this.scaleStep = -1;
    this.queueRender();
  }
  /**
   * Pass in a THREE.Object3D to be controlled
   * by this model.
   */
  async setObject(model) {
    this.reset();
    this._model = model;
    this.target.add(model);
    await this.setupScene();
  }
  /**
   * Sets the model via URL.
   */
  async setSource(url, progressCallback = () => {
  }) {
    if (!url || url === this.url) {
      progressCallback(1);
      return;
    }
    this.reset();
    this.url = url;
    if (this.externalRenderer != null) {
      const framingInfo = await this.externalRenderer.load(progressCallback);
      this.boundingSphere.radius = framingInfo.framedRadius;
      this.idealAspect = framingInfo.fieldOfViewAspect;
      return;
    }
    if (this.cancelPendingSourceChange != null) {
      this.cancelPendingSourceChange();
      this.cancelPendingSourceChange = null;
    }
    let gltf;
    try {
      gltf = await new Promise(async (resolve, reject) => {
        this.cancelPendingSourceChange = () => reject();
        try {
          const result = await this.element[$renderer].loader.load(url, this.element, progressCallback);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });
    } catch (error) {
      if (error == null) {
        return;
      }
      throw error;
    }
    this.cancelPendingSourceChange = null;
    this.reset();
    this.url = url;
    this._currentGLTF = gltf;
    if (gltf != null) {
      this._model = gltf.scene;
      this.target.add(gltf.scene);
    }
    const { animations } = gltf;
    const animationsByName = /* @__PURE__ */ new Map();
    const animationNames = [];
    for (const animation of animations) {
      animationsByName.set(animation.name, animation);
      animationNames.push(animation.name);
    }
    this.animations = animations;
    this.animationsByName = animationsByName;
    this.animationNames = animationNames;
    await this.setupScene();
  }
  async setupScene() {
    this.applyTransform();
    this.updateBoundingBox();
    await this.updateFraming();
    this.updateShadow();
    this.setShadowIntensity(this.shadowIntensity);
  }
  reset() {
    this.url = null;
    this.renderCount = 0;
    this.queueRender();
    if (this.shadow != null) {
      this.shadow.setIntensity(0);
    }
    this.bakedShadows.clear();
    const { _model } = this;
    if (_model != null) {
      _model.removeFromParent();
      this._model = null;
    }
    const gltf = this._currentGLTF;
    if (gltf != null) {
      gltf.dispose();
      this._currentGLTF = null;
    }
    if (this.currentAnimationAction != null) {
      this.currentAnimationAction.stop();
      this.currentAnimationAction = null;
    }
    this.mixer.stopAllAction();
    this.mixer.uncacheRoot(this);
  }
  dispose() {
    this.reset();
    if (this.shadow != null) {
      this.shadow.dispose();
      this.shadow = null;
    }
    this.element[$currentGLTF] = null;
    this.element[$originalGltfJson] = null;
    this.element[$model] = null;
  }
  get currentGLTF() {
    return this._currentGLTF;
  }
  /**
   * Updates the ModelScene for a new container size in CSS pixels.
   */
  setSize(width, height) {
    if (this.width === width && this.height === height) {
      return;
    }
    this.width = Math.max(width, 1);
    this.height = Math.max(height, 1);
    this.annotationRenderer.setSize(width, height);
    this.aspect = this.width / this.height;
    if (this.externalRenderer != null) {
      const dpr = resolveDpr();
      this.externalRenderer.resize(width * dpr, height * dpr);
    }
    this.queueRender();
  }
  markBakedShadow(mesh) {
    mesh.userData.shadow = true;
    this.bakedShadows.add(mesh);
  }
  unmarkBakedShadow(mesh) {
    mesh.userData.shadow = false;
    mesh.visible = true;
    this.bakedShadows.delete(mesh);
    this.boundingBox.expandByObject(mesh);
  }
  findBakedShadows(group) {
    const boundingBox = new Box3();
    group.traverse((object) => {
      const mesh = object;
      if (!mesh.material) {
        return;
      }
      const material = mesh.material;
      if (!material.transparent) {
        return;
      }
      boundingBox.setFromObject(mesh);
      const size = boundingBox.getSize(vector3);
      const minDim = Math.min(size.x, size.y, size.z);
      const maxDim = Math.max(size.x, size.y, size.z);
      if (maxDim < MIN_SHADOW_RATIO * minDim) {
        return;
      }
      this.markBakedShadow(mesh);
    });
  }
  checkBakedShadows() {
    const { min, max } = this.boundingBox;
    const shadowBox = new Box3();
    this.boundingBox.getSize(this.size);
    for (const mesh of this.bakedShadows) {
      shadowBox.setFromObject(mesh);
      if (shadowBox.min.y < min.y + this.size.y / MIN_SHADOW_RATIO && shadowBox.min.x <= min.x && shadowBox.max.x >= max.x && shadowBox.min.z <= min.z && shadowBox.max.z >= max.z) {
        continue;
      }
      if (shadowBox.min.z < min.z + this.size.z / MIN_SHADOW_RATIO && shadowBox.min.x <= min.x && shadowBox.max.x >= max.x && shadowBox.min.y <= min.y && shadowBox.max.y >= max.y) {
        continue;
      }
      this.unmarkBakedShadow(mesh);
    }
  }
  applyTransform() {
    const { model } = this;
    if (model == null) {
      return;
    }
    const orientation = parseExpressions(this.element.orientation)[0].terms;
    const roll = normalizeUnit(orientation[0]).number;
    const pitch = normalizeUnit(orientation[1]).number;
    const yaw = normalizeUnit(orientation[2]).number;
    model.quaternion.setFromEuler(new Euler(pitch, yaw, roll, "YXZ"));
    const scale = parseExpressions(this.element.scale)[0].terms;
    model.scale.set(scale[0].number, scale[1].number, scale[2].number);
  }
  updateBoundingBox() {
    const { model } = this;
    if (model == null) {
      return;
    }
    this.target.remove(model);
    this.findBakedShadows(model);
    const bound = (box, vertex) => {
      return box.expandByPoint(vertex);
    };
    this.setBakedShadowVisibility(false);
    this.boundingBox = reduceVertices(model, bound, new Box3());
    if (this.boundingBox.isEmpty()) {
      this.setBakedShadowVisibility(true);
      this.bakedShadows.forEach((mesh) => this.unmarkBakedShadow(mesh));
      this.boundingBox = reduceVertices(model, bound, new Box3());
    }
    this.checkBakedShadows();
    this.setBakedShadowVisibility();
    this.boundingBox.getSize(this.size);
    this.target.add(model);
  }
  /**
   * Calculates the boundingSphere and idealAspect that allows the 3D
   * object to be framed tightly in a 2D window of any aspect ratio without
   * clipping at any camera orbit. The camera's center target point can be
   * optionally specified. If no center is specified, it defaults to the center
   * of the bounding box, which means asymmetric models will tend to be tight on
   * one side instead of both. Proper choice of center can correct this.
   */
  async updateFraming() {
    const { model } = this;
    if (model == null) {
      return;
    }
    this.target.remove(model);
    this.setBakedShadowVisibility(false);
    const { center } = this.boundingSphere;
    this.element.requestUpdate("cameraTarget");
    await this.element.updateComplete;
    center.copy(this.getTarget());
    const radiusSquared = (value, vertex) => {
      return Math.max(value, center.distanceToSquared(vertex));
    };
    this.boundingSphere.radius = Math.sqrt(reduceVertices(model, radiusSquared, 0));
    const horizontalTanFov = (value, vertex) => {
      vertex.sub(center);
      const radiusXZ = Math.sqrt(vertex.x * vertex.x + vertex.z * vertex.z);
      return Math.max(value, radiusXZ / (this.idealCameraDistance() - Math.abs(vertex.y)));
    };
    this.idealAspect = reduceVertices(model, horizontalTanFov, 0) / Math.tan(this.framedFoVDeg / 2 * Math.PI / 180);
    this.setBakedShadowVisibility();
    this.target.add(model);
  }
  setBakedShadowVisibility(visible = this.shadowIntensity <= 0) {
    for (const shadow of this.bakedShadows) {
      shadow.visible = visible;
    }
  }
  idealCameraDistance() {
    const halfFovRad = this.framedFoVDeg / 2 * Math.PI / 180;
    return this.boundingSphere.radius / Math.sin(halfFovRad);
  }
  /**
   * Set's the framedFieldOfView based on the aspect ratio of the window in
   * order to keep the model fully visible at any camera orientation.
   */
  adjustedFoV(fovDeg) {
    const vertical = Math.tan(fovDeg / 2 * Math.PI / 180) * Math.max(1, this.idealAspect / this.aspect);
    return 2 * Math.atan(vertical) * 180 / Math.PI;
  }
  getNDC(clientX, clientY) {
    if (this.xrCamera != null) {
      ndc.set(clientX / window.screen.width, clientY / window.screen.height);
    } else {
      const rect = this.element.getBoundingClientRect();
      ndc.set((clientX - rect.x) / this.width, (clientY - rect.y) / this.height);
    }
    ndc.multiplyScalar(2).subScalar(1);
    ndc.y *= -1;
    return ndc;
  }
  /**
   * Returns the size of the corresponding canvas element.
   */
  getSize() {
    return { width: this.width, height: this.height };
  }
  setEnvironmentAndSkybox(environment, skybox) {
    if (this.element[$renderer].arRenderer.presentedScene === this) {
      return;
    }
    this.environment = environment;
    this.background = skybox;
    this.queueRender();
  }
  /**
   * Sets the point in model coordinates the model should orbit/pivot around.
   */
  setTarget(modelX, modelY, modelZ) {
    this.goalTarget.set(-modelX, -modelY, -modelZ);
  }
  /**
   * Set the decay time of, affects the speed of target transitions.
   */
  setTargetDamperDecayTime(decayMilliseconds) {
    this.targetDamperX.setDecayTime(decayMilliseconds);
    this.targetDamperY.setDecayTime(decayMilliseconds);
    this.targetDamperZ.setDecayTime(decayMilliseconds);
  }
  /**
   * Gets the point in model coordinates the model should orbit/pivot around.
   */
  getTarget() {
    return this.goalTarget.clone().multiplyScalar(-1);
  }
  /**
   * Shifts the model to the target point immediately instead of easing in.
   */
  jumpToGoal() {
    this.updateTarget(SETTLING_TIME);
  }
  /**
   * This should be called every frame with the frame delta to cause the target
   * to transition to its set point.
   */
  updateTarget(delta) {
    const goal = this.goalTarget;
    const target2 = this.target.position;
    if (!goal.equals(target2)) {
      const normalization = this.boundingSphere.radius / 10;
      let { x, y, z } = target2;
      x = this.targetDamperX.update(x, goal.x, delta, normalization);
      y = this.targetDamperY.update(y, goal.y, delta, normalization);
      z = this.targetDamperZ.update(z, goal.z, delta, normalization);
      this.target.position.set(x, y, z);
      this.target.updateMatrixWorld();
      this.queueRender();
      return true;
    } else {
      return false;
    }
  }
  /**
   * Yaw the +z (front) of the model toward the indicated world coordinates.
   */
  pointTowards(worldX, worldZ) {
    const { x, z } = this.position;
    this.yaw = Math.atan2(worldX - x, worldZ - z);
  }
  get model() {
    return this._model;
  }
  /**
   * Yaw is the scene's orientation about the y-axis, around the rotation
   * center.
   */
  set yaw(radiansY) {
    this.rotation.y = radiansY;
    this.queueRender();
  }
  get yaw() {
    return this.rotation.y;
  }
  set animationTime(value) {
    this.mixer.setTime(value);
    this.queueShadowRender();
  }
  get animationTime() {
    if (this.currentAnimationAction != null) {
      const loopCount = Math.max(this.currentAnimationAction._loopCount, 0);
      if (this.currentAnimationAction.loop === LoopPingPong && (loopCount & 1) === 1) {
        return this.duration - this.currentAnimationAction.time;
      } else {
        return this.currentAnimationAction.time;
      }
    }
    return 0;
  }
  set animationTimeScale(value) {
    this.mixer.timeScale = value;
  }
  get animationTimeScale() {
    return this.mixer.timeScale;
  }
  get duration() {
    if (this.currentAnimationAction != null && this.currentAnimationAction.getClip()) {
      return this.currentAnimationAction.getClip().duration;
    }
    return 0;
  }
  get hasActiveAnimation() {
    return this.currentAnimationAction != null;
  }
  /**
   * Plays an animation if there are any associated with the current model.
   * Accepts an optional string name of an animation to play. If no name is
   * provided, or if no animation is found by the given name, always falls back
   * to playing the first animation.
   */
  playAnimation(name = null, crossfadeTime = 0, loopMode = LoopRepeat, repetitionCount = Infinity) {
    if (this._currentGLTF == null) {
      return;
    }
    const { animations } = this;
    if (animations == null || animations.length === 0) {
      return;
    }
    let animationClip = null;
    if (name != null) {
      animationClip = this.animationsByName.get(name);
      if (animationClip == null) {
        const parsedAnimationIndex = parseInt(name);
        if (!isNaN(parsedAnimationIndex) && parsedAnimationIndex >= 0 && parsedAnimationIndex < animations.length) {
          animationClip = animations[parsedAnimationIndex];
        }
      }
    }
    if (animationClip == null) {
      animationClip = animations[0];
    }
    try {
      const { currentAnimationAction: lastAnimationAction } = this;
      const action = this.mixer.clipAction(animationClip, this);
      this.currentAnimationAction = action;
      if (this.element.paused) {
        this.mixer.stopAllAction();
      } else {
        action.paused = false;
        if (lastAnimationAction != null && action !== lastAnimationAction) {
          action.crossFadeFrom(lastAnimationAction, crossfadeTime, false);
        } else if (this.animationTimeScale > 0 && this.animationTime == this.duration) {
          this.animationTime = 0;
        }
      }
      action.setLoop(loopMode, repetitionCount);
      action.enabled = true;
      action.clampWhenFinished = true;
      action.play();
    } catch (error) {
      console.error(error);
    }
  }
  stopAnimation() {
    this.currentAnimationAction = null;
    this.mixer.stopAllAction();
  }
  updateAnimation(step) {
    this.mixer.update(step);
    this.queueShadowRender();
  }
  subscribeMixerEvent(event, callback) {
    this.mixer.addEventListener(event, callback);
  }
  /**
   * Call if the object has been changed in such a way that the shadow's shape
   * has changed (not a rotation about the Y axis).
   */
  updateShadow() {
    const shadow = this.shadow;
    if (shadow != null) {
      const side = this.element.arPlacement === "wall" ? "back" : "bottom";
      shadow.setScene(this, this.shadowSoftness, side);
      shadow.needsUpdate = true;
    }
  }
  renderShadow(renderer) {
    const shadow = this.shadow;
    if (shadow != null && shadow.needsUpdate == true) {
      shadow.render(renderer, this);
      shadow.needsUpdate = false;
    }
  }
  queueShadowRender() {
    if (this.shadow != null) {
      this.shadow.needsUpdate = true;
    }
  }
  /**
   * Sets the shadow's intensity, lazily creating the shadow as necessary.
   */
  setShadowIntensity(shadowIntensity) {
    this.shadowIntensity = shadowIntensity;
    if (this._currentGLTF == null) {
      return;
    }
    this.setBakedShadowVisibility();
    if (shadowIntensity <= 0 && this.shadow == null) {
      return;
    }
    if (this.shadow == null) {
      const side = this.element.arPlacement === "wall" ? "back" : "bottom";
      this.shadow = new Shadow(this, this.shadowSoftness, side);
    }
    this.shadow.setIntensity(shadowIntensity);
  }
  /**
   * Sets the shadow's softness by mapping a [0, 1] softness parameter to the
   * shadow's resolution. This involves reallocation, so it should not be
   * changed frequently. Softer shadows are cheaper to render.
   */
  setShadowSoftness(softness) {
    this.shadowSoftness = softness;
    const shadow = this.shadow;
    if (shadow != null) {
      shadow.setSoftness(softness);
    }
  }
  /**
   * Shift the floor vertically from the bottom of the model's bounding box by
   * offset (should generally be negative).
   */
  setShadowOffset(offset) {
    const shadow = this.shadow;
    if (shadow != null) {
      shadow.setOffset(offset);
    }
  }
  hitFromPoint(ndcPosition, object = this) {
    raycaster.setFromCamera(ndcPosition, this.getCamera());
    const hits = raycaster.intersectObject(object, true);
    return hits.find((hit) => hit.object.visible && !hit.object.userData.shadow);
  }
  /**
   * This method returns the world position, model-space normal and texture
   * coordinate of the point on the mesh corresponding to the input pixel
   * coordinates given relative to the model-viewer element. If the mesh
   * is not hit, the result is null.
   */
  positionAndNormalFromPoint(ndcPosition, object = this) {
    var _a;
    const hit = this.hitFromPoint(ndcPosition, object);
    if (hit == null) {
      return null;
    }
    const position = hit.point;
    const normal = hit.face != null ? hit.face.normal.clone().applyNormalMatrix(new Matrix3().getNormalMatrix(hit.object.matrixWorld)) : raycaster.ray.direction.clone().multiplyScalar(-1);
    const uv = (_a = hit.uv) !== null && _a !== void 0 ? _a : null;
    return { position, normal, uv };
  }
  /**
   * This method returns a dynamic hotspot ID string of the point on the mesh
   * corresponding to the input pixel coordinates given relative to the
   * model-viewer element. The ID string can be used in the data-surface
   * attribute of the hotspot to make it follow this point on the surface even
   * as the model animates. If the mesh is not hit, the result is null.
   */
  surfaceFromPoint(ndcPosition, object = this) {
    const model = this.element.model;
    if (model == null) {
      return null;
    }
    const hit = this.hitFromPoint(ndcPosition, object);
    if (hit == null || hit.face == null) {
      return null;
    }
    const node = model[$nodeFromPoint](hit);
    const { meshes, primitives } = node.mesh.userData.associations;
    const va = new Vector3();
    const vb = new Vector3();
    const vc = new Vector3();
    const { a, b, c } = hit.face;
    const mesh = hit.object;
    mesh.getVertexPosition(a, va);
    mesh.getVertexPosition(b, vb);
    mesh.getVertexPosition(c, vc);
    const tri = new Triangle(va, vb, vc);
    const uvw = new Vector3();
    tri.getBarycoord(mesh.worldToLocal(hit.point), uvw);
    return `${meshes} ${primitives} ${a} ${b} ${c} ${uvw.x.toFixed(3)} ${uvw.y.toFixed(3)} ${uvw.z.toFixed(3)}`;
  }
  /**
   * The following methods are for operating on the set of Hotspot objects
   * attached to the scene. These come from DOM elements, provided to slots by
   * the Annotation Mixin.
   */
  addHotspot(hotspot) {
    this.target.add(hotspot);
    this.annotationRenderer.domElement.appendChild(hotspot.element);
  }
  removeHotspot(hotspot) {
    this.target.remove(hotspot);
  }
  /**
   * Helper method to apply a function to all hotspots.
   */
  forHotspots(func) {
    const { children } = this.target;
    for (let i = 0, l = children.length; i < l; i++) {
      const hotspot = children[i];
      if (hotspot instanceof Hotspot) {
        func(hotspot);
      }
    }
  }
  /**
   * Lazy initializer for surface hotspots - will only run once.
   */
  initializeSurface(hotspot) {
    if (hotspot.surface != null && hotspot.mesh == null) {
      const nodes = parseExpressions(hotspot.surface)[0].terms;
      if (nodes.length != 8) {
        console.warn(hotspot.surface + " does not have exactly 8 numbers.");
        return;
      }
      const primitiveNode = this.element.model[$nodeFromIndex](nodes[0].number, nodes[1].number);
      const tri = new Vector3(nodes[2].number, nodes[3].number, nodes[4].number);
      if (primitiveNode == null) {
        console.warn(hotspot.surface + " does not match a node/primitive in this glTF! Skipping this hotspot.");
        return;
      }
      const numVert = primitiveNode.mesh.geometry.attributes.position.count;
      if (tri.x >= numVert || tri.y >= numVert || tri.z >= numVert) {
        console.warn(hotspot.surface + " vertex indices out of range in this glTF! Skipping this hotspot.");
        return;
      }
      const bary = new Vector3(nodes[5].number, nodes[6].number, nodes[7].number);
      hotspot.mesh = primitiveNode.mesh;
      hotspot.tri = tri;
      hotspot.bary = bary;
    }
  }
  /**
   * Update positions of surface hotspots to follow model animation.
   */
  updateSurfaceHotspots() {
    const forceUpdate = !this.element.paused;
    this.forHotspots((hotspot) => {
      this.initializeSurface(hotspot);
      hotspot.updateSurface(forceUpdate);
    });
  }
  /**
   * Update the CSS visibility of the hotspots based on whether their normals
   * point toward the camera.
   */
  updateHotspotsVisibility(viewerPosition) {
    this.forHotspots((hotspot) => {
      view.copy(viewerPosition);
      target.setFromMatrixPosition(hotspot.matrixWorld);
      view.sub(target);
      normalWorld.copy(hotspot.normal).transformDirection(this.target.matrixWorld);
      if (view.dot(normalWorld) < 0) {
        hotspot.hide();
      } else {
        hotspot.show();
      }
    });
  }
  /**
   * Rotate all hotspots to an absolute orientation given by the input number of
   * radians. Zero returns them to upright.
   */
  orientHotspots(radians) {
    this.forHotspots((hotspot) => {
      hotspot.orient(radians);
    });
  }
  /**
   * Set the rendering visibility of all hotspots. This is used to hide them
   * during transitions and such.
   */
  setHotspotsVisibility(visible) {
    this.forHotspots((hotspot) => {
      hotspot.visible = visible;
    });
  }
  updateSchema(src) {
    var _a;
    const { schemaElement, element } = this;
    const { alt, poster, iosSrc } = element;
    if (src != null) {
      const encoding = [{
        "@type": "MediaObject",
        contentUrl: src,
        encodingFormat: ((_a = src.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === "gltf" ? "model/gltf+json" : "model/gltf-binary"
      }];
      if (iosSrc) {
        encoding.push({
          "@type": "MediaObject",
          contentUrl: iosSrc,
          encodingFormat: "model/vnd.usdz+zip"
        });
      }
      const structuredData = {
        "@context": "http://schema.org/",
        "@type": "3DModel",
        image: poster !== null && poster !== void 0 ? poster : void 0,
        name: alt !== null && alt !== void 0 ? alt : void 0,
        encoding
      };
      schemaElement.textContent = JSON.stringify(structuredData);
      document.head.appendChild(schemaElement);
    } else if (schemaElement.parentElement != null) {
      schemaElement.parentElement.removeChild(schemaElement);
    }
  }
}
export {
  ModelScene
};
//# sourceMappingURL=ModelScene.js.map
