@photo-sphere-viewer/cubemap-tiles-adapter
Version:
Photo Sphere Viewer adapter to display tiled cubemaps.
610 lines (604 loc) • 20.3 kB
JavaScript
/*!
* Photo Sphere Viewer / Cubemap Tiles Adapter 5.13.4
* @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, {
CubemapTilesAdapter: () => CubemapTilesAdapter
});
module.exports = __toCommonJS(index_exports);
// src/CubemapTilesAdapter.ts
var import_core3 = require("@photo-sphere-viewer/core");
var import_cubemap_adapter = require("@photo-sphere-viewer/cubemap-adapter");
var import_three3 = require("three");
// ../shared/Queue.ts
var Task = class {
constructor(id, priority, fn) {
this.id = id;
this.priority = priority;
this.fn = fn;
this.status = 1 /* PENDING */;
}
start() {
this.status = 2 /* RUNNING */;
return this.fn(this).then(
() => {
this.status = 4 /* DONE */;
},
() => {
this.status = 5 /* ERROR */;
}
);
}
cancel() {
this.status = 3 /* CANCELLED */;
}
isCancelled() {
return this.status === 3 /* CANCELLED */;
}
};
var Queue = class {
constructor(concurency = 8) {
this.concurency = concurency;
this.runningTasks = {};
this.tasks = {};
}
enqueue(task) {
this.tasks[task.id] = task;
}
clear() {
Object.values(this.tasks).forEach((task) => task.cancel());
this.tasks = {};
this.runningTasks = {};
}
setPriority(taskId, priority) {
const task = this.tasks[taskId];
if (task) {
task.priority = priority;
if (task.status === 0 /* DISABLED */) {
task.status = 1 /* PENDING */;
}
}
}
disableAllTasks() {
Object.values(this.tasks).forEach((task) => {
task.status = 0 /* DISABLED */;
});
}
start() {
if (Object.keys(this.runningTasks).length >= this.concurency) {
return;
}
const nextTask = Object.values(this.tasks).filter((task) => task.status === 1 /* PENDING */).sort((a, b) => b.priority - a.priority).pop();
if (nextTask) {
this.runningTasks[nextTask.id] = true;
nextTask.start().then(() => {
if (!nextTask.isCancelled()) {
delete this.tasks[nextTask.id];
delete this.runningTasks[nextTask.id];
this.start();
}
});
this.start();
}
}
};
// ../shared/tiles-utils.ts
var import_core = require("@photo-sphere-viewer/core");
var import_three = require("three");
function checkTilesLevels(levels) {
let previous = 0;
levels.forEach((level, i) => {
if (!level.zoomRange || level.zoomRange.length !== 2) {
throw new import_core.PSVError(`Tiles level ${i} is missing "zoomRange" property`);
}
if (level.zoomRange[0] >= level.zoomRange[1] || level.zoomRange[0] !== previous || i === 0 && level.zoomRange[0] !== 0 || i === levels.length - 1 && level.zoomRange[1] !== 100) {
throw new import_core.PSVError(`Tiles levels' "zoomRange" are not orderer or are not covering the whole 0-100 range`);
}
previous = level.zoomRange[1];
});
}
function getTileIndexByZoomLevel(levels, zoomLevel) {
return levels.findIndex((level) => {
return zoomLevel >= level.zoomRange[0] && zoomLevel <= level.zoomRange[1];
});
}
function buildErrorMaterial() {
const canvas = new OffscreenCanvas(512, 512);
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#333";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = `${canvas.width / 5}px serif`;
ctx.fillStyle = "#a22";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("\u26A0", canvas.width / 2, canvas.height / 2);
return new import_three.MeshBasicMaterial({ map: import_core.utils.createTexture(canvas) });
}
function createWireFrame(geometry) {
const wireframe = new import_three.WireframeGeometry(geometry);
const line = new import_three.LineSegments(wireframe);
line.material.depthTest = false;
line.material.depthWrite = false;
line.material.opacity = 0.25;
line.material.transparent = true;
return line;
}
var DEBUG_COLORS = ["dodgerblue", "limegreen", "indianred"];
function buildDebugTexture(image, level, id) {
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d");
ctx.fillStyle = DEBUG_COLORS[level % DEBUG_COLORS.length];
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = "multiply";
ctx.drawImage(image, 0, 0);
const fontSize = image.width / 7;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "white";
ctx.font = `${fontSize}px monospace`;
ctx.textAlign = "center";
id.split("\n").forEach((id2, i) => {
ctx.fillText(id2, image.width / 2, image.height / 2 + fontSize * (0.3 + i));
});
return canvas;
}
// src/utils.ts
var import_core2 = require("@photo-sphere-viewer/core");
var import_three2 = require("three");
function isMultiTiles(panorama) {
return !!panorama.levels;
}
function computeTileConfig(tile, level, data) {
return {
...tile,
level,
tileSize: tile.faceSize / tile.nbTiles,
facesByTile: data.CUBE_SEGMENTS / tile.nbTiles
};
}
function getTileConfig(panorama, zoomLevel, data) {
let tile;
let level;
if (!isMultiTiles(panorama)) {
level = 0;
tile = {
...panorama,
zoomRange: [0, 100]
};
} else {
level = getTileIndexByZoomLevel(panorama.levels, zoomLevel);
tile = panorama.levels[level];
}
return computeTileConfig(tile, level, data);
}
function getTileConfigByIndex(panorama, level, data) {
if (!isMultiTiles(panorama) || !panorama.levels[level]) {
return null;
} else {
return computeTileConfig(panorama.levels[level], level, data);
}
}
function checkPanoramaConfig(panorama, data) {
if (typeof panorama !== "object" || !panorama.tileUrl) {
throw new import_core2.PSVError("Invalid panorama configuration, are you using the right adapter?");
}
if (isMultiTiles(panorama)) {
panorama.levels.forEach((level) => checkTile(level, data));
checkTilesLevels(panorama.levels);
} else {
checkTile(panorama, data);
}
}
function checkTile(tile, data) {
if (!tile.faceSize || !tile.nbTiles) {
throw new import_core2.PSVError("Invalid panorama configuration, are you using the right adapter?");
}
if (tile.nbTiles > data.CUBE_SEGMENTS) {
throw new import_core2.PSVError(`Panorama nbTiles must not be greater than ${data.CUBE_SEGMENTS}.`);
}
if (!import_three2.MathUtils.isPowerOfTwo(tile.nbTiles)) {
throw new import_core2.PSVError("Panorama nbTiles must be power of 2.");
}
}
function isTopOrBottom(face) {
return face === 2 || face === 3;
}
function getCacheKey(panorama, firstTile) {
for (let i = 0; i < firstTile.nbTiles; i++) {
const url = panorama.tileUrl("front", i, firstTile.nbTiles / 2, firstTile.level);
if (url) {
return url;
}
}
return panorama.tileUrl.toString();
}
// src/CubemapTilesAdapter.ts
var CUBE_SEGMENTS = 16;
var NB_VERTICES_BY_FACE = 6;
var NB_VERTICES_BY_PLANE = NB_VERTICES_BY_FACE * CUBE_SEGMENTS * CUBE_SEGMENTS;
var NB_VERTICES = 6 * NB_VERTICES_BY_PLANE;
var NB_GROUPS_BY_FACE = CUBE_SEGMENTS * CUBE_SEGMENTS;
var ATTR_UV = "uv";
var ATTR_POSITION = "position";
var CUBE_HASHMAP = ["left", "right", "top", "bottom", "back", "front"];
var ERROR_LEVEL = -1;
function tileId(tile) {
return `${tile.face}:${tile.col}x${tile.row}/${tile.config.level}`;
}
function prettyTileId(tile) {
return `${tileId(tile)}
${CUBE_HASHMAP[tile.face]}`;
}
function meshes(group) {
return group.children;
}
var getConfig = import_core3.utils.getConfigParser({
showErrorTile: true,
baseBlur: true,
antialias: true,
blur: false,
debug: false
});
var vertexPosition = new import_three3.Vector3();
var _CubemapTilesAdapter = class _CubemapTilesAdapter extends import_core3.AbstractAdapter {
constructor(viewer, config) {
super(viewer);
this.state = {
tileConfig: null,
tiles: {},
faces: {},
geom: null,
materials: [],
errorMaterial: null,
inTransition: false
};
this.queue = new Queue();
this.config = getConfig(config);
if (!import_cubemap_adapter.CubemapAdapter) {
throw new import_core3.PSVError("CubemapTilesAdapter requires CubemapAdapter");
}
this.adapter = new import_cubemap_adapter.CubemapAdapter(this.viewer, {
blur: this.config.baseBlur
});
if (this.viewer.config.requestHeaders) {
import_core3.utils.logWarn(
'CubemapTilesAdapter fallbacks to file loader because "requestHeaders" where provided. Consider removing "requestHeaders" if you experience performances issues.'
);
}
}
static withConfig(config) {
return [_CubemapTilesAdapter, config];
}
init() {
super.init();
this.viewer.addEventListener(import_core3.events.TransitionDoneEvent.type, this);
this.viewer.addEventListener(import_core3.events.PositionUpdatedEvent.type, this);
this.viewer.addEventListener(import_core3.events.ZoomUpdatedEvent.type, this);
}
destroy() {
this.viewer.removeEventListener(import_core3.events.TransitionDoneEvent.type, this);
this.viewer.removeEventListener(import_core3.events.PositionUpdatedEvent.type, this);
this.viewer.removeEventListener(import_core3.events.ZoomUpdatedEvent.type, this);
this.__cleanup();
this.state.errorMaterial?.map?.dispose();
this.state.errorMaterial?.dispose();
this.adapter.destroy();
delete this.adapter;
delete this.state.geom;
delete this.state.errorMaterial;
super.destroy();
}
/**
* @internal
*/
handleEvent(e) {
switch (e.type) {
case import_core3.events.PositionUpdatedEvent.type:
case import_core3.events.ZoomUpdatedEvent.type:
this.__refresh();
break;
case import_core3.events.TransitionDoneEvent.type:
this.state.inTransition = false;
if (e.completed) {
this.__switchMesh(this.viewer.renderer.mesh);
}
break;
}
}
supportsTransition(panorama) {
return !!panorama.baseUrl;
}
supportsPreload(panorama) {
return !!panorama.baseUrl;
}
textureCoordsToSphericalCoords(point, data) {
return this.adapter.textureCoordsToSphericalCoords(point, data);
}
sphericalCoordsToTextureCoords(position, data) {
return this.adapter.sphericalCoordsToTextureCoords(position, data);
}
async loadTexture(panorama, loader = true) {
checkPanoramaConfig(panorama, { CUBE_SEGMENTS });
const firstTile = getTileConfig(panorama, 0, { CUBE_SEGMENTS });
const panoData = {
isCubemap: true,
flipTopBottom: panorama.flipTopBottom ?? false,
faceSize: firstTile.faceSize
};
if (panorama.baseUrl) {
const textureData = await this.adapter.loadTexture(panorama.baseUrl, loader);
return {
panorama,
panoData: {
...panoData,
baseData: textureData.panoData
},
cacheKey: textureData.cacheKey,
texture: textureData.texture
};
} else {
return {
panorama,
panoData: {
...panoData,
baseData: null
},
cacheKey: getCacheKey(panorama, firstTile),
texture: null
};
}
}
createMesh() {
const baseMesh = this.adapter.createMesh();
const cubeSize = import_core3.CONSTANTS.SPHERE_RADIUS * 2;
const geometry = new import_three3.BoxGeometry(cubeSize, cubeSize, cubeSize, CUBE_SEGMENTS, CUBE_SEGMENTS, CUBE_SEGMENTS).scale(1, 1, -1).toNonIndexed();
geometry.clearGroups();
for (let i = 0, k = 0; i < NB_VERTICES; i += NB_VERTICES_BY_FACE) {
geometry.addGroup(i, NB_VERTICES_BY_FACE, k++);
}
const materials = [];
const material = new import_three3.MeshBasicMaterial({
opacity: 0,
transparent: true,
depthTest: false,
depthWrite: false
});
for (let i = 0; i < 6; i++) {
for (let j = 0; j < NB_GROUPS_BY_FACE; j++) {
materials.push(material);
}
}
const tilesMesh = new import_three3.Mesh(geometry, materials);
tilesMesh.renderOrder = 1;
const group = new import_three3.Group();
group.add(baseMesh);
group.add(tilesMesh);
return group;
}
/**
* Applies the base texture and starts the loading of tiles
*/
setTexture(group, textureData, transition) {
const [baseMesh] = meshes(group);
if (textureData.texture) {
this.adapter.setTexture(baseMesh, {
panorama: textureData.panorama.baseUrl,
texture: textureData.texture,
panoData: textureData.panoData.baseData
});
} else {
baseMesh.visible = false;
}
if (transition) {
this.state.inTransition = true;
} else {
this.__switchMesh(group);
}
}
setTextureOpacity(group, opacity) {
const [baseMesh] = meshes(group);
this.adapter.setTextureOpacity(baseMesh, opacity);
}
disposeTexture({ texture }) {
texture?.forEach((t) => t.dispose());
}
disposeMesh(group) {
const [baseMesh, tilesMesh] = meshes(group);
baseMesh.geometry.dispose();
baseMesh.material.forEach((m) => m.dispose());
tilesMesh.geometry.dispose();
tilesMesh.material.forEach((m) => {
m.map?.dispose();
m.dispose();
});
}
/**
* Compute visible tiles and load them
*/
__refresh() {
if (!this.state.geom || this.state.inTransition) {
return;
}
const panorama = this.viewer.config.panorama;
const zoomLevel = this.viewer.getZoomLevel();
const tileConfig = getTileConfig(panorama, zoomLevel, { CUBE_SEGMENTS });
const verticesPosition = this.state.geom.getAttribute(ATTR_POSITION);
const tilesToLoad = {};
for (let i = 0; i < NB_VERTICES; i += 1) {
vertexPosition.fromBufferAttribute(verticesPosition, i);
vertexPosition.applyEuler(this.viewer.renderer.sphereCorrection);
if (this.viewer.renderer.isObjectVisible(vertexPosition)) {
const face = Math.floor(i / NB_VERTICES_BY_PLANE);
const segmentIndex = Math.floor((i - face * NB_VERTICES_BY_PLANE) / 6);
const segmentRow = Math.floor(segmentIndex / CUBE_SEGMENTS);
const segmentCol = segmentIndex - segmentRow * CUBE_SEGMENTS;
let config = tileConfig;
while (config) {
let row = Math.floor(segmentRow / config.facesByTile);
let col = Math.floor(segmentCol / config.facesByTile);
const angle = vertexPosition.angleTo(this.viewer.state.direction);
const tile = {
face,
row,
col,
angle,
config,
url: null
};
if (panorama.flipTopBottom && isTopOrBottom(face)) {
col = config.nbTiles - col - 1;
row = config.nbTiles - row - 1;
}
const id = tileId(tile);
if (tilesToLoad[id]) {
tilesToLoad[id].angle = Math.min(tilesToLoad[id].angle, angle);
break;
} else {
tile.url = panorama.tileUrl(CUBE_HASHMAP[face], col, row, config.level);
if (tile.url) {
tilesToLoad[id] = tile;
break;
} else {
config = getTileConfigByIndex(panorama, config.level - 1, { CUBE_SEGMENTS });
}
}
}
}
}
this.state.tileConfig = tileConfig;
this.__loadTiles(Object.values(tilesToLoad));
}
/**
* Loads tiles and change existing tiles priority
*/
__loadTiles(tiles) {
this.queue.disableAllTasks();
tiles.forEach((tile) => {
const id = tileId(tile);
if (this.state.tiles[id]) {
this.queue.setPriority(id, tile.angle);
} else {
this.state.tiles[id] = true;
this.queue.enqueue(new Task(id, tile.angle, (task) => this.__loadTile(tile, task)));
}
});
this.queue.start();
}
/**
* Loads and draw a tile
*/
__loadTile(tile, task) {
return this.viewer.textureLoader.loadImage(tile.url, null, this.viewer.state.textureData.cacheKey).then((image) => {
if (!task.isCancelled()) {
if (this.config.debug) {
image = buildDebugTexture(image, tile.config.level, prettyTileId(tile));
}
const mipmaps = this.config.antialias && tile.config.level > 0;
const material = new import_three3.MeshBasicMaterial({ map: import_core3.utils.createTexture(image, mipmaps) });
this.__swapMaterial(tile, material, false);
this.viewer.needsUpdate();
}
}).catch((err) => {
if (!import_core3.utils.isAbortError(err) && !task.isCancelled() && this.config.showErrorTile) {
if (!this.state.errorMaterial) {
this.state.errorMaterial = buildErrorMaterial();
}
this.__swapMaterial(tile, this.state.errorMaterial, true);
this.viewer.needsUpdate();
}
});
}
/**
* Applies a new texture to the faces
*/
__swapMaterial(tile, material, isError) {
const panoData = this.viewer.state.textureData.panoData;
const uvs = this.state.geom.getAttribute(ATTR_UV);
for (let c = 0; c < tile.config.facesByTile; c++) {
for (let r = 0; r < tile.config.facesByTile; r++) {
const faceCol = tile.col * tile.config.facesByTile + c;
const faceRow = tile.row * tile.config.facesByTile + r;
const firstVertex = tile.face * NB_VERTICES_BY_PLANE + 6 * (CUBE_SEGMENTS * faceRow + faceCol);
if (isError && this.state.faces[firstVertex] > ERROR_LEVEL) {
continue;
}
if (this.state.faces[firstVertex] > tile.config.level) {
continue;
}
this.state.faces[firstVertex] = isError ? ERROR_LEVEL : tile.config.level;
const matIndex = this.state.geom.groups.find((g) => g.start === firstVertex).materialIndex;
this.state.materials[matIndex] = material;
let top = 1 - r / tile.config.facesByTile;
let bottom = 1 - (r + 1) / tile.config.facesByTile;
let left = c / tile.config.facesByTile;
let right = (c + 1) / tile.config.facesByTile;
if (panoData.flipTopBottom && isTopOrBottom(tile.face)) {
top = 1 - top;
bottom = 1 - bottom;
left = 1 - left;
right = 1 - right;
}
uvs.setXY(firstVertex, left, top);
uvs.setXY(firstVertex + 1, left, bottom);
uvs.setXY(firstVertex + 2, right, top);
uvs.setXY(firstVertex + 3, left, bottom);
uvs.setXY(firstVertex + 4, right, bottom);
uvs.setXY(firstVertex + 5, right, top);
}
}
uvs.needsUpdate = true;
}
__switchMesh(group) {
const [, tilesMesh] = meshes(group);
this.__cleanup();
this.state.materials = tilesMesh.material;
this.state.geom = tilesMesh.geometry;
if (this.config.debug) {
const wireframe = createWireFrame(this.state.geom);
this.viewer.renderer.addObject(wireframe);
this.viewer.renderer.setSphereCorrection(this.viewer.config.sphereCorrection, wireframe);
}
setTimeout(() => this.__refresh());
}
/**
* Clears loading queue, dispose all materials
*/
__cleanup() {
this.queue.clear();
this.state.tiles = {};
this.state.faces = {};
this.state.materials = [];
this.state.inTransition = false;
}
};
_CubemapTilesAdapter.id = "cubemap-tiles";
_CubemapTilesAdapter.VERSION = "5.13.4";
_CubemapTilesAdapter.supportsDownload = false;
var CubemapTilesAdapter = _CubemapTilesAdapter;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
CubemapTilesAdapter
});
//# sourceMappingURL=index.cjs.map