@photo-sphere-viewer/cubemap-adapter
Version: 
Photo Sphere Viewer adapter to display cubemaps.
397 lines (393 loc) • 12.8 kB
JavaScript
/*!
 * Photo Sphere Viewer / Cubemap Adapter 5.14.0
 * @copyright 2015-2025 Damien "Mistic" Sorel
 * @licence MIT (https://opensource.org/licenses/MIT)
 */
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
  CubemapAdapter: () => CubemapAdapter
});
module.exports = __toCommonJS(index_exports);
// src/CubemapAdapter.ts
var import_core2 = require("@photo-sphere-viewer/core");
var import_three = require("three");
// src/utils.ts
var import_core = require("@photo-sphere-viewer/core");
var CUBE_ARRAY = [0, 2, 4, 5, 3, 1];
var CUBE_HASHMAP = ["left", "right", "top", "bottom", "back", "front"];
function isCubemap(cubemap) {
  return cubemap && typeof cubemap === "object" && CUBE_HASHMAP.every((side) => side in cubemap);
}
function cleanCubemapArray(panorama) {
  const cleanPanorama = [];
  if (panorama.length !== 6) {
    throw new import_core.PSVError("A cubemap array must contain exactly 6 images.");
  }
  for (let i = 0; i < 6; i++) {
    cleanPanorama[i] = panorama[CUBE_ARRAY[i]];
  }
  return cleanPanorama;
}
function cleanCubemap(cubemap) {
  const cleanPanorama = [];
  if (!isCubemap(cubemap)) {
    throw new import_core.PSVError("A cubemap object must contain exactly left, front, right, back, top, bottom images.");
  }
  CUBE_HASHMAP.forEach((side, i) => {
    cleanPanorama[i] = cubemap[side];
  });
  return cleanPanorama;
}
// src/CubemapAdapter.ts
var getConfig = import_core2.utils.getConfigParser({
  blur: false
});
var EPS = 1e-6;
var ORIGIN = new import_three.Vector3();
var _CubemapAdapter = class _CubemapAdapter extends import_core2.AbstractAdapter {
  static withConfig(config) {
    return [_CubemapAdapter, config];
  }
  constructor(viewer, config) {
    super(viewer);
    this.config = getConfig(config);
  }
  supportsTransition() {
    return true;
  }
  supportsPreload() {
    return true;
  }
  /**
   * {@link https://github.com/bhautikj/vrProjector/blob/master/vrProjector/CubemapProjection.py#L130}
   */
  textureCoordsToSphericalCoords(point, data) {
    if (import_core2.utils.isNil(point.textureX) || import_core2.utils.isNil(point.textureY) || import_core2.utils.isNil(point.textureFace)) {
      throw new import_core2.PSVError(`Texture position is missing 'textureX', 'textureY' or 'textureFace'`);
    }
    const u = 2 * (point.textureX / data.faceSize - 0.5);
    const v = 2 * (point.textureY / data.faceSize - 0.5);
    function yawPitch(x, y, z) {
      const dv = Math.sqrt(x * x + y * y + z * z);
      return [Math.atan2(y / dv, x / dv), -Math.asin(z / dv)];
    }
    let yaw;
    let pitch;
    switch (point.textureFace) {
      case "front":
        [yaw, pitch] = yawPitch(1, u, v);
        break;
      case "right":
        [yaw, pitch] = yawPitch(-u, 1, v);
        break;
      case "left":
        [yaw, pitch] = yawPitch(u, -1, v);
        break;
      case "back":
        [yaw, pitch] = yawPitch(-1, -u, v);
        break;
      case "bottom":
        [yaw, pitch] = data.flipTopBottom ? yawPitch(-v, u, 1) : yawPitch(v, -u, 1);
        break;
      case "top":
        [yaw, pitch] = data.flipTopBottom ? yawPitch(v, u, -1) : yawPitch(-v, -u, -1);
        break;
    }
    return { yaw, pitch };
  }
  sphericalCoordsToTextureCoords(position, data) {
    const raycaster = this.viewer.renderer.raycaster;
    const mesh = this.viewer.renderer.mesh;
    raycaster.set(ORIGIN, this.viewer.dataHelper.sphericalCoordsToVector3(position));
    const point = raycaster.intersectObject(mesh)[0].point.divideScalar(import_core2.CONSTANTS.SPHERE_RADIUS);
    function mapUV(x, a1, a2) {
      return Math.round(import_three.MathUtils.mapLinear(x, a1, a2, 0, data.faceSize));
    }
    let textureFace;
    let textureX;
    let textureY;
    if (1 - Math.abs(point.z) < EPS) {
      if (point.z > 0) {
        textureFace = "front";
        textureX = mapUV(point.x, 1, -1);
        textureY = mapUV(point.y, 1, -1);
      } else {
        textureFace = "back";
        textureX = mapUV(point.x, -1, 1);
        textureY = mapUV(point.y, 1, -1);
      }
    } else if (1 - Math.abs(point.x) < EPS) {
      if (point.x > 0) {
        textureFace = "left";
        textureX = mapUV(point.z, -1, 1);
        textureY = mapUV(point.y, 1, -1);
      } else {
        textureFace = "right";
        textureX = mapUV(point.z, 1, -1);
        textureY = mapUV(point.y, 1, -1);
      }
    } else {
      if (point.y > 0) {
        textureFace = "top";
        textureX = mapUV(point.x, -1, 1);
        textureY = mapUV(point.z, 1, -1);
      } else {
        textureFace = "bottom";
        textureX = mapUV(point.x, -1, 1);
        textureY = mapUV(point.z, -1, 1);
      }
      if (data.flipTopBottom) {
        textureX = data.faceSize - textureX;
        textureY = data.faceSize - textureY;
      }
    }
    return { textureFace, textureX, textureY };
  }
  async loadTexture(panorama, loader = true) {
    if (this.viewer.config.fisheye) {
      import_core2.utils.logWarn("fisheye effect with cubemap texture can generate distorsion");
    }
    let cleanPanorama;
    if (Array.isArray(panorama) || isCubemap(panorama)) {
      cleanPanorama = {
        type: "separate",
        paths: panorama
      };
    } else {
      cleanPanorama = panorama;
    }
    let result;
    switch (cleanPanorama.type) {
      case "separate":
        result = await this.loadTexturesSeparate(cleanPanorama, loader);
        break;
      case "stripe":
        result = await this.loadTexturesStripe(cleanPanorama, loader);
        break;
      case "net":
        result = await this.loadTexturesNet(cleanPanorama, loader);
        break;
      default:
        throw new import_core2.PSVError("Invalid cubemap panorama, are you using the right adapter?");
    }
    return {
      panorama,
      texture: result.textures,
      cacheKey: result.cacheKey,
      panoData: {
        isCubemap: true,
        flipTopBottom: result.flipTopBottom,
        faceSize: result.textures[0].image.width
      }
    };
  }
  async loadTexturesSeparate(panorama, loader) {
    let paths;
    if (Array.isArray(panorama.paths)) {
      paths = cleanCubemapArray(panorama.paths);
    } else {
      paths = cleanCubemap(panorama.paths);
    }
    const cacheKey = paths[0];
    const promises = [];
    const progress = [0, 0, 0, 0, 0, 0];
    for (let i = 0; i < 6; i++) {
      if (!paths[i]) {
        progress[i] = 100;
        promises.push(Promise.resolve(null));
      } else {
        promises.push(
          this.viewer.textureLoader.loadImage(
            paths[i],
            loader ? (p) => {
              progress[i] = p;
              this.viewer.textureLoader.dispatchProgress(import_core2.utils.sum(progress) / 6);
            } : null,
            cacheKey
          ).then((img) => this.createCubemapTexture(img))
        );
      }
    }
    return {
      textures: await Promise.all(promises),
      cacheKey,
      flipTopBottom: panorama.flipTopBottom ?? false
    };
  }
  createCubemapTexture(img) {
    if (img.width !== img.height) {
      import_core2.utils.logWarn("Invalid cubemap image, the width should equal the height");
    }
    if (this.config.blur || img.width > import_core2.SYSTEM.maxTextureWidth) {
      const ratio = Math.min(1, import_core2.SYSTEM.maxCanvasWidth / img.width);
      const buffer = new OffscreenCanvas(Math.floor(img.width * ratio), Math.floor(img.height * ratio));
      const ctx = buffer.getContext("2d");
      if (this.config.blur) {
        ctx.filter = `blur(${buffer.width / 512}px)`;
      }
      ctx.drawImage(img, 0, 0, buffer.width, buffer.height);
      return import_core2.utils.createTexture(buffer);
    }
    return import_core2.utils.createTexture(img);
  }
  async loadTexturesStripe(panorama, loader) {
    if (!panorama.order) {
      panorama.order = ["left", "front", "right", "back", "top", "bottom"];
    }
    const cacheKey = panorama.path;
    const img = await this.viewer.textureLoader.loadImage(
      panorama.path,
      loader ? (p) => this.viewer.textureLoader.dispatchProgress(p) : null,
      cacheKey
    );
    if (img.width !== img.height * 6) {
      import_core2.utils.logWarn("Invalid cubemap image, the width should be six times the height");
    }
    const ratio = Math.min(1, import_core2.SYSTEM.maxCanvasWidth / img.height);
    const tileWidth = Math.floor(img.height * ratio);
    const textures = {};
    for (let i = 0; i < 6; i++) {
      const buffer = new OffscreenCanvas(tileWidth, tileWidth);
      const ctx = buffer.getContext("2d");
      if (this.config.blur) {
        ctx.filter = `blur(${buffer.width / 512}px)`;
      }
      ctx.drawImage(
        img,
        img.height * i,
        0,
        img.height,
        img.height,
        0,
        0,
        tileWidth,
        tileWidth
      );
      textures[panorama.order[i]] = import_core2.utils.createTexture(buffer);
    }
    return {
      textures: cleanCubemap(textures),
      cacheKey,
      flipTopBottom: panorama.flipTopBottom ?? false
    };
  }
  async loadTexturesNet(panorama, loader) {
    const cacheKey = panorama.path;
    const img = await this.viewer.textureLoader.loadImage(
      panorama.path,
      loader ? (p) => this.viewer.textureLoader.dispatchProgress(p) : null,
      cacheKey
    );
    if (img.width / 4 !== img.height / 3) {
      import_core2.utils.logWarn("Invalid cubemap image, the width should be 4/3rd of the height");
    }
    const ratio = Math.min(1, import_core2.SYSTEM.maxCanvasWidth / (img.width / 4));
    const tileWidth = Math.floor(img.width / 4 * ratio);
    const pts = [
      [0, 1 / 3],
      // left
      [1 / 2, 1 / 3],
      // right
      [1 / 4, 0],
      // top
      [1 / 4, 2 / 3],
      // bottom
      [3 / 4, 1 / 3],
      // back
      [1 / 4, 1 / 3]
      // front
    ];
    const textures = [];
    for (let i = 0; i < 6; i++) {
      const buffer = new OffscreenCanvas(tileWidth, tileWidth);
      const ctx = buffer.getContext("2d");
      if (this.config.blur) {
        ctx.filter = `blur(${buffer.width / 512}px)`;
      }
      ctx.drawImage(
        img,
        img.width * pts[i][0],
        img.height * pts[i][1],
        img.width / 4,
        img.height / 3,
        0,
        0,
        tileWidth,
        tileWidth
      );
      textures[i] = import_core2.utils.createTexture(buffer);
    }
    return {
      textures,
      cacheKey,
      flipTopBottom: true
    };
  }
  createMesh() {
    const cubeSize = import_core2.CONSTANTS.SPHERE_RADIUS * 2;
    const geometry = new import_three.BoxGeometry(cubeSize, cubeSize, cubeSize).scale(1, 1, -1);
    const materials = [];
    for (let i = 0; i < 6; i++) {
      const material = new import_three.MeshBasicMaterial({ depthTest: false, depthWrite: false });
      materials.push(material);
    }
    return new import_three.Mesh(geometry, materials);
  }
  setTexture(mesh, { texture, panoData }) {
    mesh.material.forEach((material, i) => {
      if (texture[i]) {
        if (panoData.flipTopBottom && (i === 2 || i === 3)) {
          texture[i].center = new import_three.Vector2(0.5, 0.5);
          texture[i].rotation = Math.PI;
        }
        material.map = texture[i];
      } else {
        material.opacity = 0;
        material.transparent = true;
      }
    });
  }
  setTextureOpacity(mesh, opacity) {
    mesh.material.forEach((material) => {
      if (material.map) {
        material.opacity = opacity;
        material.transparent = opacity < 1;
      }
    });
  }
  disposeTexture({ texture }) {
    texture.forEach((t) => t.dispose());
  }
  disposeMesh(mesh) {
    mesh.geometry.dispose();
    mesh.material.forEach((m) => m.dispose());
  }
};
_CubemapAdapter.id = "cubemap";
_CubemapAdapter.VERSION = "5.14.0";
_CubemapAdapter.supportsDownload = false;
var CubemapAdapter = _CubemapAdapter;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  CubemapAdapter
});
//# sourceMappingURL=index.cjs.map