6567 lines
208 KiB
JavaScript
6567 lines
208 KiB
JavaScript
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three')) :
|
|
typeof define === 'function' && define.amd ? define(['exports', 'three'], factory) :
|
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.PhotoSphereViewer = {}, global.THREE));
|
|
})(this, (function (exports, THREE) {
|
|
|
|
console.warn('PhotoSphereViewer "index.js" scripts are deprecated and will be removed in a future version. Please use ES Modules: https://photo-sphere-viewer.js.org/guide/#your-first-viewer');
|
|
|
|
/*!
|
|
* PhotoSphereViewer 5.7.3
|
|
* @copyright 2014-2015 Jérémy Heleine
|
|
* @copyright 2024 Damien "Mistic" Sorel
|
|
* @licence MIT (https://opensource.org/licenses/MIT)
|
|
*/
|
|
"use strict";
|
|
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;
|
|
};
|
|
|
|
// three
|
|
var require_three = () => THREE;
|
|
|
|
// src/index.ts
|
|
var src_exports = {};
|
|
__export(src_exports, {
|
|
AbstractAdapter: () => AbstractAdapter,
|
|
AbstractButton: () => AbstractButton,
|
|
AbstractComponent: () => AbstractComponent,
|
|
AbstractConfigurablePlugin: () => AbstractConfigurablePlugin,
|
|
AbstractPlugin: () => AbstractPlugin,
|
|
CONSTANTS: () => constants_exports,
|
|
Cache: () => Cache,
|
|
DEFAULTS: () => DEFAULTS,
|
|
EquirectangularAdapter: () => EquirectangularAdapter,
|
|
PSVError: () => PSVError,
|
|
SYSTEM: () => SYSTEM,
|
|
TypedEvent: () => TypedEvent,
|
|
VERSION: () => VERSION,
|
|
Viewer: () => Viewer,
|
|
events: () => events_exports,
|
|
registerButton: () => registerButton,
|
|
utils: () => utils_exports
|
|
});
|
|
|
|
// src/data/constants.ts
|
|
var constants_exports = {};
|
|
__export(constants_exports, {
|
|
ACTIONS: () => ACTIONS,
|
|
ANIMATION_MIN_DURATION: () => ANIMATION_MIN_DURATION,
|
|
CAPTURE_EVENTS_CLASS: () => CAPTURE_EVENTS_CLASS,
|
|
CTRLZOOM_TIMEOUT: () => CTRLZOOM_TIMEOUT,
|
|
DBLCLICK_DELAY: () => DBLCLICK_DELAY,
|
|
DEFAULT_TRANSITION: () => DEFAULT_TRANSITION,
|
|
EASINGS: () => EASINGS,
|
|
ICONS: () => ICONS,
|
|
IDS: () => IDS,
|
|
INERTIA_WINDOW: () => INERTIA_WINDOW,
|
|
KEY_CODES: () => KEY_CODES,
|
|
LONGTOUCH_DELAY: () => LONGTOUCH_DELAY,
|
|
MOVE_THRESHOLD: () => MOVE_THRESHOLD,
|
|
SPHERE_RADIUS: () => SPHERE_RADIUS,
|
|
TWOFINGERSOVERLAY_DELAY: () => TWOFINGERSOVERLAY_DELAY,
|
|
VIEWER_DATA: () => VIEWER_DATA
|
|
});
|
|
|
|
// src/icons/arrow.svg
|
|
var arrow_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="40 40 432 432"><g transform="rotate(0, 256, 256)"><path fill="currentColor" d="M425.23 210.55H227.39a5 5 0 01-3.53-8.53l56.56-56.57a45.5 45.5 0 000-64.28 45.15 45.15 0 00-32.13-13.3 45.15 45.15 0 00-32.14 13.3L41.32 256l174.83 174.83a45.15 45.15 0 0032.14 13.3 45.15 45.15 0 0032.13-13.3 45.5 45.5 0 000-64.28l-56.57-56.57a5 5 0 013.54-8.53h197.84c25.06 0 45.45-20.39 45.45-45.45s-20.4-45.45-45.45-45.45z"/></g><!-- Created by Flatart from the Noun Project --></svg>\n';
|
|
|
|
// src/icons/close.svg
|
|
var close_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g fill="currentColor" transform=" translate(50, 50) rotate(45)"><rect x="-5" y="-65" width="10" height="130"/><rect x="-65" y="-5" width="130" height="10"/></g></svg>';
|
|
|
|
// src/icons/download.svg
|
|
var download_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="currentColor" d="M83.3 35.6h-17V3H32.2v32.6H16.6l33.6 32.7 33-32.7z"/><path fill="currentColor" d="M83.3 64.2v16.3H16.6V64.2H-.1v32.6H100V64.2H83.3z"/><!--Created by Michael Zenaty from the Noun Project--></svg>\n';
|
|
|
|
// src/icons/fullscreen-in.svg
|
|
var fullscreen_in_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="currentColor" d="M100 40H87.1V18.8h-21V6H100zM100 93.2H66V80.3h21.1v-21H100zM34 93.2H0v-34h12.9v21.1h21zM12.9 40H0V6h34v12.9H12.8z"/><!--Created by Garrett Knoll from the Noun Project--></svg>\n';
|
|
|
|
// src/icons/fullscreen-out.svg
|
|
var fullscreen_out_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="currentColor" d="M66 7h13v21h21v13H66zM66 60.3h34v12.9H79v21H66zM0 60.3h34v34H21V73.1H0zM21 7h13v34H0V28h21z"/><!--Created by Garrett Knoll from the Noun Project--></svg>\n';
|
|
|
|
// src/icons/info.svg
|
|
var info_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="currentColor" d="M28.3 26.1c-1 2.6-1.9 4.8-2.6 7-2.5 7.4-5 14.7-7.2 22-1.3 4.4.5 7.2 4.3 7.8 1.3.2 2.8.2 4.2-.1 8.2-2 11.9-8.6 15.7-15.2l-2.2 2a18.8 18.8 0 0 1-7.4 5.2 2 2 0 0 1-1.6-.2c-.2-.1 0-1 0-1.4l.8-1.8L41.9 28c.5-1.4.9-3 .7-4.4-.2-2.6-3-4.4-6.3-4.4-8.8.2-15 4.5-19.5 11.8-.2.3-.2.6-.3 1.3 3.7-2.8 6.8-6.1 11.8-6.2z"/><circle fill="currentColor" cx="39.3" cy="9.2" r="8.2"/><!--Created by Arafat Uddin from the Noun Project--></svg>\n';
|
|
|
|
// src/icons/menu.svg
|
|
var menu_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80"><g fill="currentColor"><circle r="10" cx="20" cy="20"/><circle r="10" cx="50" cy="20"/><circle r="10" cx="80" cy="20"/><circle r="10" cx="20" cy="50"/><circle r="10" cx="50" cy="50"/><circle r="10" cx="80" cy="50"/><circle r="10" cx="20" cy="80"/><circle r="10" cx="50" cy="80"/><circle r="10" cx="80" cy="80"/></g><!-- Created by Richard Kun\xE1k from the Noun Project--></svg>\n';
|
|
|
|
// src/icons/zoom-in.svg
|
|
var zoom_in_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="currentColor" d="M14.043 12.22a7.738 7.738 0 1 0-1.823 1.822l4.985 4.985c.503.504 1.32.504 1.822 0a1.285 1.285 0 0 0 0-1.822l-4.984-4.985zm-6.305 1.043a5.527 5.527 0 1 1 0-11.053 5.527 5.527 0 0 1 0 11.053z"/><path fill="currentColor" d="M8.728 4.009H6.744v2.737H4.006V8.73h2.738v2.736h1.984V8.73h2.737V6.746H8.728z"/><!--Created by Ryan Canning from the Noun Project--></svg>\n';
|
|
|
|
// src/icons/zoom-out.svg
|
|
var zoom_out_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="currentColor" d="M14.043 12.22a7.738 7.738 0 1 0-1.823 1.822l4.985 4.985c.503.504 1.32.504 1.822 0a1.285 1.285 0 0 0 0-1.822l-4.984-4.985zm-6.305 1.043a5.527 5.527 0 1 1 0-11.053 5.527 5.527 0 0 1 0 11.053z"/><path fill="currentColor" d="M4.006 6.746h7.459V8.73H4.006z"/><!--Created by Ryan Canning from the Noun Project--></svg>\n';
|
|
|
|
// src/data/constants.ts
|
|
var DEFAULT_TRANSITION = 1500;
|
|
var ANIMATION_MIN_DURATION = 500;
|
|
var MOVE_THRESHOLD = 4;
|
|
var DBLCLICK_DELAY = 300;
|
|
var LONGTOUCH_DELAY = 500;
|
|
var TWOFINGERSOVERLAY_DELAY = 100;
|
|
var CTRLZOOM_TIMEOUT = 2e3;
|
|
var INERTIA_WINDOW = 300;
|
|
var SPHERE_RADIUS = 10;
|
|
var VIEWER_DATA = "photoSphereViewer";
|
|
var CAPTURE_EVENTS_CLASS = "psv--capture-event";
|
|
var ACTIONS = /* @__PURE__ */ ((ACTIONS2) => {
|
|
ACTIONS2["ROTATE_UP"] = "ROTATE_UP";
|
|
ACTIONS2["ROTATE_DOWN"] = "ROTATE_DOWN";
|
|
ACTIONS2["ROTATE_RIGHT"] = "ROTATE_RIGHT";
|
|
ACTIONS2["ROTATE_LEFT"] = "ROTATE_LEFT";
|
|
ACTIONS2["ZOOM_IN"] = "ZOOM_IN";
|
|
ACTIONS2["ZOOM_OUT"] = "ZOOM_OUT";
|
|
return ACTIONS2;
|
|
})(ACTIONS || {});
|
|
var IDS = {
|
|
MENU: "menu",
|
|
TWO_FINGERS: "twoFingers",
|
|
CTRL_ZOOM: "ctrlZoom",
|
|
ERROR: "error",
|
|
DESCRIPTION: "description"
|
|
};
|
|
var KEY_CODES = {
|
|
Enter: "Enter",
|
|
Control: "Control",
|
|
Escape: "Escape",
|
|
Space: " ",
|
|
PageUp: "PageUp",
|
|
PageDown: "PageDown",
|
|
ArrowLeft: "ArrowLeft",
|
|
ArrowUp: "ArrowUp",
|
|
ArrowRight: "ArrowRight",
|
|
ArrowDown: "ArrowDown",
|
|
Delete: "Delete",
|
|
Plus: "+",
|
|
Minus: "-"
|
|
};
|
|
var ICONS = {
|
|
arrow: arrow_default,
|
|
close: close_default,
|
|
download: download_default,
|
|
fullscreenIn: fullscreen_in_default,
|
|
fullscreenOut: fullscreen_out_default,
|
|
info: info_default,
|
|
menu: menu_default,
|
|
zoomIn: zoom_in_default,
|
|
zoomOut: zoom_out_default
|
|
};
|
|
var EASINGS = {
|
|
linear: (t) => t,
|
|
inQuad: (t) => t * t,
|
|
outQuad: (t) => t * (2 - t),
|
|
inOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
|
|
inCubic: (t) => t * t * t,
|
|
outCubic: (t) => --t * t * t + 1,
|
|
inOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
|
|
inQuart: (t) => t * t * t * t,
|
|
outQuart: (t) => 1 - --t * t * t * t,
|
|
inOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t,
|
|
inQuint: (t) => t * t * t * t * t,
|
|
outQuint: (t) => 1 + --t * t * t * t * t,
|
|
inOutQuint: (t) => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t,
|
|
inSine: (t) => 1 - Math.cos(t * (Math.PI / 2)),
|
|
outSine: (t) => Math.sin(t * (Math.PI / 2)),
|
|
inOutSine: (t) => 0.5 - 0.5 * Math.cos(Math.PI * t),
|
|
inExpo: (t) => Math.pow(2, 10 * (t - 1)),
|
|
outExpo: (t) => 1 - Math.pow(2, -10 * t),
|
|
inOutExpo: (t) => (t = t * 2 - 1) < 0 ? 0.5 * Math.pow(2, 10 * t) : 1 - 0.5 * Math.pow(2, -10 * t),
|
|
inCirc: (t) => 1 - Math.sqrt(1 - t * t),
|
|
outCirc: (t) => Math.sqrt(1 - (t - 1) * (t - 1)),
|
|
inOutCirc: (t) => (t *= 2) < 1 ? 0.5 - 0.5 * Math.sqrt(1 - t * t) : 0.5 + 0.5 * Math.sqrt(1 - (t -= 2) * t)
|
|
};
|
|
|
|
// src/utils/index.ts
|
|
var utils_exports = {};
|
|
__export(utils_exports, {
|
|
Animation: () => Animation,
|
|
Dynamic: () => Dynamic,
|
|
MultiDynamic: () => MultiDynamic,
|
|
PressHandler: () => PressHandler,
|
|
Slider: () => Slider,
|
|
SliderDirection: () => SliderDirection,
|
|
addClasses: () => addClasses,
|
|
angle: () => angle,
|
|
applyEulerInverse: () => applyEulerInverse,
|
|
checkStylesheet: () => checkStylesheet,
|
|
checkVersion: () => checkVersion,
|
|
cleanCssPosition: () => cleanCssPosition,
|
|
clone: () => clone,
|
|
createTexture: () => createTexture,
|
|
cssPositionIsOrdered: () => cssPositionIsOrdered,
|
|
dasherize: () => dasherize,
|
|
deepEqual: () => deepEqual,
|
|
deepmerge: () => deepmerge,
|
|
distance: () => distance,
|
|
exitFullscreen: () => exitFullscreen,
|
|
firstNonNull: () => firstNonNull,
|
|
getAbortError: () => getAbortError,
|
|
getAngle: () => getAngle,
|
|
getClosest: () => getClosest,
|
|
getConfigParser: () => getConfigParser,
|
|
getElement: () => getElement,
|
|
getPosition: () => getPosition,
|
|
getShortestArc: () => getShortestArc,
|
|
getStyleProperty: () => getStyleProperty,
|
|
getTouchData: () => getTouchData,
|
|
getXMPValue: () => getXMPValue,
|
|
greatArcDistance: () => greatArcDistance,
|
|
hasParent: () => hasParent,
|
|
invertResolvableBoolean: () => invertResolvableBoolean,
|
|
isAbortError: () => isAbortError,
|
|
isEmpty: () => isEmpty,
|
|
isExtendedPosition: () => isExtendedPosition,
|
|
isFullscreenEnabled: () => isFullscreenEnabled,
|
|
isNil: () => isNil,
|
|
isPlainObject: () => isPlainObject,
|
|
logWarn: () => logWarn,
|
|
parseAngle: () => parseAngle,
|
|
parsePoint: () => parsePoint,
|
|
parseSpeed: () => parseSpeed,
|
|
removeClasses: () => removeClasses,
|
|
requestFullscreen: () => requestFullscreen,
|
|
resolveBoolean: () => resolveBoolean,
|
|
speedToDuration: () => speedToDuration,
|
|
sum: () => sum,
|
|
throttle: () => throttle,
|
|
toggleClass: () => toggleClass,
|
|
wrap: () => wrap
|
|
});
|
|
|
|
// src/utils/math.ts
|
|
function wrap(value, max) {
|
|
let result = value % max;
|
|
if (result < 0) {
|
|
result += max;
|
|
}
|
|
return result;
|
|
}
|
|
function sum(array) {
|
|
return array.reduce((a, b) => a + b, 0);
|
|
}
|
|
function distance(p1, p2) {
|
|
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
|
}
|
|
function angle(p1, p2) {
|
|
return Math.atan2(p2.y - p1.y, p2.x - p1.x);
|
|
}
|
|
function getShortestArc(from, to) {
|
|
const candidates = [
|
|
0,
|
|
// direct
|
|
Math.PI * 2,
|
|
// clock-wise cross zero
|
|
-Math.PI * 2
|
|
// counter-clock-wise cross zero
|
|
];
|
|
return candidates.reduce((value, candidate) => {
|
|
const newCandidate = to - from + candidate;
|
|
return Math.abs(newCandidate) < Math.abs(value) ? newCandidate : value;
|
|
}, Infinity);
|
|
}
|
|
function getAngle(position1, position2) {
|
|
return Math.acos(
|
|
Math.cos(position1.pitch) * Math.cos(position2.pitch) * Math.cos(position1.yaw - position2.yaw) + Math.sin(position1.pitch) * Math.sin(position2.pitch)
|
|
);
|
|
}
|
|
function greatArcDistance([yaw1, pitch1], [yaw2, pitch2]) {
|
|
if (yaw1 - yaw2 > Math.PI) {
|
|
yaw1 -= 2 * Math.PI;
|
|
} else if (yaw1 - yaw2 < -Math.PI) {
|
|
yaw1 += 2 * Math.PI;
|
|
}
|
|
const x = (yaw2 - yaw1) * Math.cos((pitch1 + pitch2) / 2);
|
|
const y = pitch2 - pitch1;
|
|
return Math.sqrt(x * x + y * y);
|
|
}
|
|
|
|
// src/utils/browser.ts
|
|
function getElement(selector) {
|
|
if (typeof selector === "string") {
|
|
return selector.match(/^[a-z]/i) ? document.getElementById(selector) : document.querySelector(selector);
|
|
} else {
|
|
return selector;
|
|
}
|
|
}
|
|
function toggleClass(element, className, active) {
|
|
if (active === void 0) {
|
|
element.classList.toggle(className);
|
|
} else if (active) {
|
|
element.classList.add(className);
|
|
} else if (!active) {
|
|
element.classList.remove(className);
|
|
}
|
|
}
|
|
function addClasses(element, className) {
|
|
element.classList.add(...className.split(" "));
|
|
}
|
|
function removeClasses(element, className) {
|
|
element.classList.remove(...className.split(" "));
|
|
}
|
|
function hasParent(el, parent) {
|
|
let test = el;
|
|
do {
|
|
if (test === parent) {
|
|
return true;
|
|
}
|
|
test = test.parentElement;
|
|
} while (test);
|
|
return false;
|
|
}
|
|
function getClosest(el, selector) {
|
|
if (!el?.matches) {
|
|
return null;
|
|
}
|
|
let test = el;
|
|
do {
|
|
if (test.matches(selector)) {
|
|
return test;
|
|
}
|
|
test = test.parentElement;
|
|
} while (test);
|
|
return null;
|
|
}
|
|
function getPosition(el) {
|
|
let x = 0;
|
|
let y = 0;
|
|
let test = el;
|
|
while (test) {
|
|
x += test.offsetLeft - test.scrollLeft + test.clientLeft;
|
|
y += test.offsetTop - test.scrollTop + test.clientTop;
|
|
test = test.offsetParent;
|
|
}
|
|
return { x, y };
|
|
}
|
|
function getStyleProperty(elt, varname) {
|
|
return window.getComputedStyle(elt).getPropertyValue(varname);
|
|
}
|
|
function getTouchData(e) {
|
|
if (e.touches.length < 2) {
|
|
return null;
|
|
}
|
|
const p1 = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
const p2 = { x: e.touches[1].clientX, y: e.touches[1].clientY };
|
|
return {
|
|
distance: distance(p1, p2),
|
|
angle: angle(p1, p2),
|
|
center: { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }
|
|
};
|
|
}
|
|
function isFullscreenEnabled(elt) {
|
|
return (document.fullscreenElement || document.webkitFullscreenElement) === elt;
|
|
}
|
|
function requestFullscreen(elt) {
|
|
(elt.requestFullscreen || elt.webkitRequestFullscreen).call(elt);
|
|
}
|
|
function exitFullscreen() {
|
|
(document.exitFullscreen || document.webkitExitFullscreen).call(document);
|
|
}
|
|
|
|
// src/utils/misc.ts
|
|
function dasherize(str) {
|
|
return str.replace(/[A-Z](?:(?=[^A-Z])|[A-Z]*(?=[A-Z][^A-Z]|$))/g, (s, i) => {
|
|
return (i > 0 ? "-" : "") + s.toLowerCase();
|
|
});
|
|
}
|
|
function throttle(callback, wait) {
|
|
let paused = false;
|
|
return function(...args) {
|
|
if (!paused) {
|
|
paused = true;
|
|
setTimeout(() => {
|
|
callback.apply(this, args);
|
|
paused = false;
|
|
}, wait);
|
|
}
|
|
};
|
|
}
|
|
function isPlainObject(value) {
|
|
if (typeof value !== "object" || value === null || Object.prototype.toString.call(value) !== "[object Object]") {
|
|
return false;
|
|
}
|
|
if (Object.getPrototypeOf(value) === null) {
|
|
return true;
|
|
}
|
|
let proto = value;
|
|
while (Object.getPrototypeOf(proto) !== null) {
|
|
proto = Object.getPrototypeOf(proto);
|
|
}
|
|
return Object.getPrototypeOf(value) === proto;
|
|
}
|
|
function deepmerge(target, src) {
|
|
const first = src;
|
|
return function merge(target2, src2) {
|
|
if (Array.isArray(src2)) {
|
|
if (!target2 || !Array.isArray(target2)) {
|
|
target2 = [];
|
|
} else {
|
|
target2.length = 0;
|
|
}
|
|
src2.forEach((e, i) => {
|
|
target2[i] = merge(null, e);
|
|
});
|
|
} else if (typeof src2 === "object") {
|
|
if (!target2 || Array.isArray(target2)) {
|
|
target2 = {};
|
|
}
|
|
Object.keys(src2).forEach((key) => {
|
|
if (typeof src2[key] !== "object" || !src2[key] || !isPlainObject(src2[key])) {
|
|
target2[key] = src2[key];
|
|
} else if (src2[key] !== first) {
|
|
if (!target2[key]) {
|
|
target2[key] = merge(null, src2[key]);
|
|
} else {
|
|
merge(target2[key], src2[key]);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
target2 = src2;
|
|
}
|
|
return target2;
|
|
}(target, src);
|
|
}
|
|
function clone(src) {
|
|
return deepmerge(null, src);
|
|
}
|
|
function isEmpty(obj) {
|
|
return !obj || Object.keys(obj).length === 0 && obj.constructor === Object;
|
|
}
|
|
function isNil(val) {
|
|
return val === null || val === void 0;
|
|
}
|
|
function firstNonNull(...values) {
|
|
for (const val of values) {
|
|
if (!isNil(val)) {
|
|
return val;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function deepEqual(obj1, obj2) {
|
|
if (obj1 === obj2) {
|
|
return true;
|
|
} else if (isObject(obj1) && isObject(obj2)) {
|
|
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
|
|
return false;
|
|
}
|
|
for (const prop of Object.keys(obj1)) {
|
|
if (!deepEqual(obj1[prop], obj2[prop])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
function isObject(obj) {
|
|
return typeof obj === "object" && obj !== null;
|
|
}
|
|
|
|
// src/utils/psv.ts
|
|
var import_three = require_three();
|
|
|
|
// src/PSVError.ts
|
|
var PSVError = class _PSVError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "PSVError";
|
|
Error.captureStackTrace?.(this, _PSVError);
|
|
}
|
|
};
|
|
|
|
// src/utils/psv.ts
|
|
function resolveBoolean(value, cb) {
|
|
if (isPlainObject(value)) {
|
|
cb(value.initial, true);
|
|
value.promise.then((res) => cb(res, false));
|
|
} else {
|
|
cb(value, true);
|
|
}
|
|
}
|
|
function invertResolvableBoolean(value) {
|
|
return {
|
|
initial: !value.initial,
|
|
promise: value.promise.then((res) => !res)
|
|
};
|
|
}
|
|
function getAbortError() {
|
|
const error = new Error("Loading was aborted.");
|
|
error.name = "AbortError";
|
|
return error;
|
|
}
|
|
function isAbortError(err) {
|
|
return err?.name === "AbortError";
|
|
}
|
|
function logWarn(message) {
|
|
console.warn(`PhotoSphereViewer: ${message}`);
|
|
}
|
|
function isExtendedPosition(object) {
|
|
if (!object) {
|
|
return false;
|
|
}
|
|
return [
|
|
["textureX", "textureY"],
|
|
["yaw", "pitch"]
|
|
].some(([key1, key2]) => {
|
|
return object[key1] !== void 0 && object[key2] !== void 0;
|
|
});
|
|
}
|
|
function getXMPValue(data, attr) {
|
|
let result = data.match("<GPano:" + attr + ">(.*)</GPano:" + attr + ">");
|
|
if (result !== null) {
|
|
const val = parseInt(result[1], 10);
|
|
return isNaN(val) ? null : val;
|
|
}
|
|
result = data.match("GPano:" + attr + '="(.*?)"');
|
|
if (result !== null) {
|
|
const val = parseInt(result[1], 10);
|
|
return isNaN(val) ? null : val;
|
|
}
|
|
return null;
|
|
}
|
|
var CSS_POSITIONS = {
|
|
top: "0%",
|
|
bottom: "100%",
|
|
left: "0%",
|
|
right: "100%",
|
|
center: "50%"
|
|
};
|
|
var X_VALUES = ["left", "center", "right"];
|
|
var Y_VALUES = ["top", "center", "bottom"];
|
|
var POS_VALUES = [...X_VALUES, ...Y_VALUES];
|
|
var CENTER = "center";
|
|
function parsePoint(value) {
|
|
if (!value) {
|
|
return { x: 0.5, y: 0.5 };
|
|
}
|
|
if (typeof value === "object") {
|
|
return value;
|
|
}
|
|
let tokens = value.toLocaleLowerCase().split(" ").slice(0, 2);
|
|
if (tokens.length === 1) {
|
|
if (CSS_POSITIONS[tokens[0]]) {
|
|
tokens = [tokens[0], CENTER];
|
|
} else {
|
|
tokens = [tokens[0], tokens[0]];
|
|
}
|
|
}
|
|
const xFirst = tokens[1] !== "left" && tokens[1] !== "right" && tokens[0] !== "top" && tokens[0] !== "bottom";
|
|
tokens = tokens.map((token) => CSS_POSITIONS[token] || token);
|
|
if (!xFirst) {
|
|
tokens.reverse();
|
|
}
|
|
const parsed = tokens.join(" ").match(/^([0-9.]+)% ([0-9.]+)%$/);
|
|
if (parsed) {
|
|
return {
|
|
x: parseFloat(parsed[1]) / 100,
|
|
y: parseFloat(parsed[2]) / 100
|
|
};
|
|
} else {
|
|
return { x: 0.5, y: 0.5 };
|
|
}
|
|
}
|
|
function cleanCssPosition(value, { allowCenter, cssOrder } = {
|
|
allowCenter: true,
|
|
cssOrder: true
|
|
}) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
if (typeof value === "string") {
|
|
value = value.split(" ");
|
|
}
|
|
if (value.length === 1) {
|
|
if (value[0] === CENTER) {
|
|
value = [CENTER, CENTER];
|
|
} else if (X_VALUES.indexOf(value[0]) !== -1) {
|
|
value = [CENTER, value[0]];
|
|
} else if (Y_VALUES.indexOf(value[0]) !== -1) {
|
|
value = [value[0], CENTER];
|
|
}
|
|
}
|
|
if (value.length !== 2 || POS_VALUES.indexOf(value[0]) === -1 || POS_VALUES.indexOf(value[1]) === -1) {
|
|
logWarn(`Unparsable position ${value}`);
|
|
return null;
|
|
}
|
|
if (!allowCenter && value[0] === CENTER && value[1] === CENTER) {
|
|
logWarn(`Invalid position center center`);
|
|
return null;
|
|
}
|
|
if (cssOrder && !cssPositionIsOrdered(value)) {
|
|
value = [value[1], value[0]];
|
|
}
|
|
if (value[1] === CENTER && X_VALUES.indexOf(value[0]) !== -1) {
|
|
value = [CENTER, value[0]];
|
|
}
|
|
if (value[0] === CENTER && Y_VALUES.indexOf(value[1]) !== -1) {
|
|
value = [value[1], CENTER];
|
|
}
|
|
return value;
|
|
}
|
|
function cssPositionIsOrdered(value) {
|
|
return Y_VALUES.indexOf(value[0]) !== -1 && X_VALUES.indexOf(value[1]) !== -1;
|
|
}
|
|
function parseSpeed(speed) {
|
|
let parsed;
|
|
if (typeof speed === "string") {
|
|
const speedStr = speed.toString().trim();
|
|
let speedValue = parseFloat(speedStr.replace(/^(-?[0-9]+(?:\.[0-9]*)?).*$/, "$1"));
|
|
const speedUnit = speedStr.replace(/^-?[0-9]+(?:\.[0-9]*)?(.*)$/, "$1").trim();
|
|
if (speedUnit.match(/(pm|per minute)$/)) {
|
|
speedValue /= 60;
|
|
}
|
|
switch (speedUnit) {
|
|
case "dpm":
|
|
case "degrees per minute":
|
|
case "dps":
|
|
case "degrees per second":
|
|
parsed = import_three.MathUtils.degToRad(speedValue);
|
|
break;
|
|
case "rdpm":
|
|
case "radians per minute":
|
|
case "rdps":
|
|
case "radians per second":
|
|
parsed = speedValue;
|
|
break;
|
|
case "rpm":
|
|
case "revolutions per minute":
|
|
case "rps":
|
|
case "revolutions per second":
|
|
parsed = speedValue * Math.PI * 2;
|
|
break;
|
|
default:
|
|
throw new PSVError(`Unknown speed unit "${speedUnit}"`);
|
|
}
|
|
} else {
|
|
parsed = speed;
|
|
}
|
|
return parsed;
|
|
}
|
|
function speedToDuration(value, angle2) {
|
|
if (typeof value !== "number") {
|
|
const speed = parseSpeed(value);
|
|
return angle2 / Math.abs(speed) * 1e3;
|
|
} else {
|
|
return Math.abs(value);
|
|
}
|
|
}
|
|
function parseAngle(angle2, zeroCenter = false, halfCircle = zeroCenter) {
|
|
let parsed;
|
|
if (typeof angle2 === "string") {
|
|
const match = angle2.toLowerCase().trim().match(/^(-?[0-9]+(?:\.[0-9]*)?)(.*)$/);
|
|
if (!match) {
|
|
throw new PSVError(`Unknown angle "${angle2}"`);
|
|
}
|
|
const value = parseFloat(match[1]);
|
|
const unit = match[2];
|
|
if (unit) {
|
|
switch (unit) {
|
|
case "deg":
|
|
case "degs":
|
|
parsed = import_three.MathUtils.degToRad(value);
|
|
break;
|
|
case "rad":
|
|
case "rads":
|
|
parsed = value;
|
|
break;
|
|
default:
|
|
throw new PSVError(`Unknown angle unit "${unit}"`);
|
|
}
|
|
} else {
|
|
parsed = value;
|
|
}
|
|
} else if (typeof angle2 === "number" && !isNaN(angle2)) {
|
|
parsed = angle2;
|
|
} else {
|
|
throw new PSVError(`Unknown angle "${angle2}"`);
|
|
}
|
|
parsed = wrap(zeroCenter ? parsed + Math.PI : parsed, Math.PI * 2);
|
|
return zeroCenter ? import_three.MathUtils.clamp(parsed - Math.PI, -Math.PI / (halfCircle ? 2 : 1), Math.PI / (halfCircle ? 2 : 1)) : parsed;
|
|
}
|
|
function createTexture(img, mimaps = false) {
|
|
const texture = new import_three.Texture(img);
|
|
texture.needsUpdate = true;
|
|
texture.minFilter = mimaps ? import_three.LinearMipmapLinearFilter : import_three.LinearFilter;
|
|
texture.generateMipmaps = mimaps;
|
|
texture.anisotropy = mimaps ? 2 : 1;
|
|
return texture;
|
|
}
|
|
var quaternion = new import_three.Quaternion();
|
|
function applyEulerInverse(vector, euler) {
|
|
quaternion.setFromEuler(euler).invert();
|
|
vector.applyQuaternion(quaternion);
|
|
}
|
|
function getConfigParser(defaults, parsers) {
|
|
const parser = function(userConfig) {
|
|
if (!userConfig) {
|
|
return clone(defaults);
|
|
}
|
|
const rawConfig = clone({
|
|
...defaults,
|
|
...userConfig
|
|
});
|
|
const config = {};
|
|
for (let [key, value] of Object.entries(rawConfig)) {
|
|
if (parsers && key in parsers) {
|
|
value = parsers[key](value, {
|
|
rawConfig,
|
|
defValue: defaults[key]
|
|
});
|
|
} else if (!(key in defaults)) {
|
|
logWarn(`Unknown option ${key}`);
|
|
continue;
|
|
}
|
|
config[key] = value;
|
|
}
|
|
return config;
|
|
};
|
|
parser.defaults = defaults;
|
|
parser.parsers = parsers || {};
|
|
return parser;
|
|
}
|
|
function checkStylesheet(element, name) {
|
|
if (getStyleProperty(element, `--psv-${name}-loaded`) !== "true") {
|
|
console.error(`PhotoSphereViewer: stylesheet "@photo-sphere-viewer/${name}/index.css" is not loaded`);
|
|
}
|
|
}
|
|
function checkVersion(name, version, coreVersion) {
|
|
if (version && version !== coreVersion) {
|
|
console.error(`PhotoSphereViewer: @photo-sphere-viewer/${name} is in version ${version} but @photo-sphere-viewer/core is in version ${coreVersion}`);
|
|
}
|
|
}
|
|
|
|
// src/utils/Animation.ts
|
|
var Animation = class {
|
|
constructor(options) {
|
|
this.easing = EASINGS["linear"];
|
|
this.callbacks = [];
|
|
this.resolved = false;
|
|
this.cancelled = false;
|
|
this.options = options;
|
|
if (options) {
|
|
if (options.easing) {
|
|
this.easing = typeof options.easing === "function" ? options.easing : EASINGS[options.easing] || EASINGS["linear"];
|
|
}
|
|
this.delayTimeout = setTimeout(() => {
|
|
this.delayTimeout = void 0;
|
|
this.animationFrame = window.requestAnimationFrame((t) => this.__run(t));
|
|
}, options.delay || 0);
|
|
} else {
|
|
this.resolved = true;
|
|
}
|
|
}
|
|
__run(timestamp) {
|
|
if (this.cancelled) {
|
|
return;
|
|
}
|
|
if (!this.start) {
|
|
this.start = timestamp;
|
|
}
|
|
const progress = (timestamp - this.start) / this.options.duration;
|
|
const current = {};
|
|
if (progress < 1) {
|
|
for (const [name, prop] of Object.entries(this.options.properties)) {
|
|
if (prop) {
|
|
const value = prop.start + (prop.end - prop.start) * this.easing(progress);
|
|
current[name] = value;
|
|
}
|
|
}
|
|
this.options.onTick(current, progress);
|
|
this.animationFrame = window.requestAnimationFrame((t) => this.__run(t));
|
|
} else {
|
|
for (const [name, prop] of Object.entries(this.options.properties)) {
|
|
if (prop) {
|
|
current[name] = prop.end;
|
|
}
|
|
}
|
|
this.options.onTick(current, 1);
|
|
this.__resolve(true);
|
|
this.animationFrame = void 0;
|
|
}
|
|
}
|
|
__resolve(value) {
|
|
if (value) {
|
|
this.resolved = true;
|
|
} else {
|
|
this.cancelled = true;
|
|
}
|
|
this.callbacks.forEach((cb) => cb(value));
|
|
this.callbacks.length = 0;
|
|
}
|
|
/**
|
|
* Promise chaining
|
|
* @param [onFulfilled] - Called when the animation is complete (true) or cancelled (false)
|
|
*/
|
|
then(onFulfilled) {
|
|
if (this.resolved || this.cancelled) {
|
|
return Promise.resolve(this.resolved).then(onFulfilled);
|
|
}
|
|
return new Promise((resolve) => {
|
|
this.callbacks.push(resolve);
|
|
}).then(onFulfilled);
|
|
}
|
|
/**
|
|
* Cancels the animation
|
|
*/
|
|
cancel() {
|
|
if (!this.cancelled && !this.resolved) {
|
|
this.__resolve(false);
|
|
if (this.delayTimeout) {
|
|
window.clearTimeout(this.delayTimeout);
|
|
this.delayTimeout = void 0;
|
|
}
|
|
if (this.animationFrame) {
|
|
window.cancelAnimationFrame(this.animationFrame);
|
|
this.animationFrame = void 0;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/utils/Dynamic.ts
|
|
var import_three2 = require_three();
|
|
var Dynamic = class {
|
|
constructor(fn, config) {
|
|
this.fn = fn;
|
|
this.mode = 0 /* STOP */;
|
|
this.speed = 0;
|
|
this.speedMult = 0;
|
|
this.currentSpeed = 0;
|
|
this.target = 0;
|
|
this.__current = 0;
|
|
this.min = config.min;
|
|
this.max = config.max;
|
|
this.wrap = config.wrap;
|
|
this.current = config.defaultValue;
|
|
if (this.wrap && this.min !== 0) {
|
|
throw new PSVError("invalid config");
|
|
}
|
|
if (this.fn) {
|
|
this.fn(this.current);
|
|
}
|
|
}
|
|
get current() {
|
|
return this.__current;
|
|
}
|
|
set current(current) {
|
|
this.__current = current;
|
|
}
|
|
/**
|
|
* Changes base speed
|
|
*/
|
|
setSpeed(speed) {
|
|
this.speed = speed;
|
|
}
|
|
/**
|
|
* Defines the target position
|
|
*/
|
|
goto(position, speedMult = 1) {
|
|
this.mode = 2 /* POSITION */;
|
|
this.target = this.wrap ? wrap(position, this.max) : import_three2.MathUtils.clamp(position, this.min, this.max);
|
|
this.speedMult = speedMult;
|
|
}
|
|
/**
|
|
* Increases/decreases the target position
|
|
*/
|
|
step(step, speedMult = 1) {
|
|
if (speedMult === 0) {
|
|
this.setValue(this.current + step);
|
|
} else {
|
|
if (this.mode !== 2 /* POSITION */) {
|
|
this.target = this.current;
|
|
}
|
|
this.goto(this.target + step, speedMult);
|
|
}
|
|
}
|
|
/**
|
|
* Starts infinite movement
|
|
*/
|
|
roll(invert = false, speedMult = 1) {
|
|
this.mode = 1 /* INFINITE */;
|
|
this.target = invert ? -Infinity : Infinity;
|
|
this.speedMult = speedMult;
|
|
}
|
|
/**
|
|
* Stops movement
|
|
*/
|
|
stop() {
|
|
this.mode = 0 /* STOP */;
|
|
}
|
|
/**
|
|
* Defines the current position and immediately stops movement
|
|
* @param {number} value
|
|
*/
|
|
setValue(value) {
|
|
this.target = this.wrap ? wrap(value, this.max) : import_three2.MathUtils.clamp(value, this.min, this.max);
|
|
this.mode = 0 /* STOP */;
|
|
this.currentSpeed = 0;
|
|
if (this.target !== this.current) {
|
|
this.current = this.target;
|
|
if (this.fn) {
|
|
this.fn(this.current);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
update(elapsed) {
|
|
if (this.mode === 2 /* POSITION */) {
|
|
if (this.wrap && Math.abs(this.target - this.current) > this.max / 2) {
|
|
this.current = this.current < this.target ? this.current + this.max : this.current - this.max;
|
|
}
|
|
const dstStop = this.currentSpeed * this.currentSpeed / (this.speed * this.speedMult * 4);
|
|
if (Math.abs(this.target - this.current) <= dstStop) {
|
|
this.mode = 0 /* STOP */;
|
|
}
|
|
}
|
|
let targetSpeed = this.mode === 0 /* STOP */ ? 0 : this.speed * this.speedMult;
|
|
if (this.target < this.current) {
|
|
targetSpeed = -targetSpeed;
|
|
}
|
|
if (this.currentSpeed < targetSpeed) {
|
|
this.currentSpeed = Math.min(
|
|
targetSpeed,
|
|
this.currentSpeed + elapsed / 1e3 * this.speed * this.speedMult * 2
|
|
);
|
|
} else if (this.currentSpeed > targetSpeed) {
|
|
this.currentSpeed = Math.max(
|
|
targetSpeed,
|
|
this.currentSpeed - elapsed / 1e3 * this.speed * this.speedMult * 2
|
|
);
|
|
}
|
|
let next = null;
|
|
if (this.current > this.target && this.currentSpeed) {
|
|
next = Math.max(this.target, this.current + this.currentSpeed * elapsed / 1e3);
|
|
} else if (this.current < this.target && this.currentSpeed) {
|
|
next = Math.min(this.target, this.current + this.currentSpeed * elapsed / 1e3);
|
|
}
|
|
if (next !== null) {
|
|
next = this.wrap ? wrap(next, this.max) : import_three2.MathUtils.clamp(next, this.min, this.max);
|
|
if (next !== this.current) {
|
|
this.current = next;
|
|
if (this.fn) {
|
|
this.fn(this.current);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// src/utils/MultiDynamic.ts
|
|
var MultiDynamic = class {
|
|
constructor(fn, dynamics) {
|
|
this.fn = fn;
|
|
this.dynamics = dynamics;
|
|
if (this.fn) {
|
|
this.fn(this.current);
|
|
}
|
|
}
|
|
get current() {
|
|
return Object.entries(this.dynamics).reduce(
|
|
(values, [name, dynamic]) => {
|
|
values[name] = dynamic.current;
|
|
return values;
|
|
},
|
|
{}
|
|
);
|
|
}
|
|
/**
|
|
* Changes base speed
|
|
*/
|
|
setSpeed(speed) {
|
|
for (const d of Object.values(this.dynamics)) {
|
|
d.setSpeed(speed);
|
|
}
|
|
}
|
|
/**
|
|
* Defines the target positions
|
|
*/
|
|
goto(positions, speedMult = 1) {
|
|
for (const [name, position] of Object.entries(positions)) {
|
|
this.dynamics[name].goto(position, speedMult);
|
|
}
|
|
}
|
|
/**
|
|
* Increase/decrease the target positions
|
|
*/
|
|
step(steps, speedMult = 1) {
|
|
if (speedMult === 0) {
|
|
this.setValue(
|
|
Object.keys(steps).reduce(
|
|
(values, name) => {
|
|
values[name] = steps[name] + this.dynamics[name].current;
|
|
return values;
|
|
},
|
|
{}
|
|
)
|
|
);
|
|
} else {
|
|
for (const [name, step] of Object.entries(steps)) {
|
|
this.dynamics[name].step(step, speedMult);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Starts infinite movements
|
|
*/
|
|
roll(rolls, speedMult = 1) {
|
|
for (const [name, roll] of Object.entries(rolls)) {
|
|
this.dynamics[name].roll(roll, speedMult);
|
|
}
|
|
}
|
|
/**
|
|
* Stops movements
|
|
*/
|
|
stop() {
|
|
for (const d of Object.values(this.dynamics)) {
|
|
d.stop();
|
|
}
|
|
}
|
|
/**
|
|
* Defines the current positions and immediately stops movements
|
|
*/
|
|
setValue(values) {
|
|
let hasUpdates = false;
|
|
for (const [name, value] of Object.entries(values)) {
|
|
hasUpdates = this.dynamics[name].setValue(value) || hasUpdates;
|
|
}
|
|
if (hasUpdates && this.fn) {
|
|
this.fn(this.current);
|
|
}
|
|
return hasUpdates;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
update(elapsed) {
|
|
let hasUpdates = false;
|
|
for (const d of Object.values(this.dynamics)) {
|
|
hasUpdates = d.update(elapsed) || hasUpdates;
|
|
}
|
|
if (hasUpdates && this.fn) {
|
|
this.fn(this.current);
|
|
}
|
|
return hasUpdates;
|
|
}
|
|
};
|
|
|
|
// src/utils/PressHandler.ts
|
|
var PressHandler = class {
|
|
constructor(delay = 200) {
|
|
this.delay = delay;
|
|
this.time = 0;
|
|
this.delay = delay;
|
|
}
|
|
get pending() {
|
|
return this.time !== 0;
|
|
}
|
|
down() {
|
|
if (this.timeout) {
|
|
clearTimeout(this.timeout);
|
|
this.timeout = void 0;
|
|
}
|
|
this.time = (/* @__PURE__ */ new Date()).getTime();
|
|
}
|
|
up(cb) {
|
|
if (!this.time) {
|
|
return;
|
|
}
|
|
const elapsed = Date.now() - this.time;
|
|
if (elapsed < this.delay) {
|
|
this.timeout = setTimeout(() => {
|
|
cb();
|
|
this.timeout = void 0;
|
|
this.time = 0;
|
|
}, this.delay);
|
|
} else {
|
|
cb();
|
|
this.time = 0;
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/utils/Slider.ts
|
|
var SliderDirection = /* @__PURE__ */ ((SliderDirection2) => {
|
|
SliderDirection2["VERTICAL"] = "VERTICAL";
|
|
SliderDirection2["HORIZONTAL"] = "HORIZONTAL";
|
|
return SliderDirection2;
|
|
})(SliderDirection || {});
|
|
var Slider = class {
|
|
constructor(container, direction, listener) {
|
|
this.container = container;
|
|
this.direction = direction;
|
|
this.listener = listener;
|
|
this.mousedown = false;
|
|
this.mouseover = false;
|
|
this.container.addEventListener("click", this);
|
|
this.container.addEventListener("mousedown", this);
|
|
this.container.addEventListener("mouseenter", this);
|
|
this.container.addEventListener("mouseleave", this);
|
|
this.container.addEventListener("touchstart", this);
|
|
this.container.addEventListener("mousemove", this, true);
|
|
this.container.addEventListener("touchmove", this, true);
|
|
window.addEventListener("mouseup", this);
|
|
window.addEventListener("touchend", this);
|
|
}
|
|
get isVertical() {
|
|
return this.direction === "VERTICAL" /* VERTICAL */;
|
|
}
|
|
get isHorizontal() {
|
|
return this.direction === "HORIZONTAL" /* HORIZONTAL */;
|
|
}
|
|
destroy() {
|
|
window.removeEventListener("mouseup", this);
|
|
window.removeEventListener("touchend", this);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
handleEvent(e) {
|
|
switch (e.type) {
|
|
case "click":
|
|
e.stopPropagation();
|
|
break;
|
|
case "mousedown":
|
|
this.__onMouseDown(e);
|
|
break;
|
|
case "mouseenter":
|
|
this.__onMouseEnter(e);
|
|
break;
|
|
case "mouseleave":
|
|
this.__onMouseLeave(e);
|
|
break;
|
|
case "touchstart":
|
|
this.__onTouchStart(e);
|
|
break;
|
|
case "mousemove":
|
|
this.__onMouseMove(e);
|
|
break;
|
|
case "touchmove":
|
|
this.__onTouchMove(e);
|
|
break;
|
|
case "mouseup":
|
|
this.__onMouseUp(e);
|
|
break;
|
|
case "touchend":
|
|
this.__onTouchEnd(e);
|
|
break;
|
|
}
|
|
}
|
|
__onMouseDown(evt) {
|
|
this.mousedown = true;
|
|
this.__update(evt.clientX, evt.clientY, true);
|
|
}
|
|
__onMouseEnter(evt) {
|
|
this.mouseover = true;
|
|
this.__update(evt.clientX, evt.clientY, true);
|
|
}
|
|
__onTouchStart(evt) {
|
|
this.mouseover = true;
|
|
this.mousedown = true;
|
|
const touch = evt.changedTouches[0];
|
|
this.__update(touch.clientX, touch.clientY, true);
|
|
}
|
|
__onMouseMove(evt) {
|
|
if (this.mousedown || this.mouseover) {
|
|
evt.stopPropagation();
|
|
this.__update(evt.clientX, evt.clientY, true);
|
|
}
|
|
}
|
|
__onTouchMove(evt) {
|
|
if (this.mousedown || this.mouseover) {
|
|
evt.stopPropagation();
|
|
const touch = evt.changedTouches[0];
|
|
this.__update(touch.clientX, touch.clientY, true);
|
|
}
|
|
}
|
|
__onMouseUp(evt) {
|
|
if (this.mousedown) {
|
|
this.mousedown = false;
|
|
this.__update(evt.clientX, evt.clientY, false);
|
|
}
|
|
}
|
|
__onMouseLeave(evt) {
|
|
if (this.mouseover) {
|
|
this.mouseover = false;
|
|
this.__update(evt.clientX, evt.clientY, true);
|
|
}
|
|
}
|
|
__onTouchEnd(evt) {
|
|
if (this.mousedown) {
|
|
this.mouseover = false;
|
|
this.mousedown = false;
|
|
const touch = evt.changedTouches[0];
|
|
this.__update(touch.clientX, touch.clientY, false);
|
|
}
|
|
}
|
|
__update(clientX, clientY, moving) {
|
|
const boundingClientRect = this.container.getBoundingClientRect();
|
|
const cursor = this.isVertical ? clientY : clientX;
|
|
const pos = boundingClientRect[this.isVertical ? "bottom" : "left"];
|
|
const size = boundingClientRect[this.isVertical ? "height" : "width"];
|
|
const val = Math.abs((pos - cursor) / size);
|
|
this.listener({
|
|
value: val,
|
|
click: !moving,
|
|
mousedown: this.mousedown,
|
|
mouseover: this.mouseover,
|
|
cursor: { clientX, clientY }
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/events.ts
|
|
var events_exports = {};
|
|
__export(events_exports, {
|
|
BeforeAnimateEvent: () => BeforeAnimateEvent,
|
|
BeforeRenderEvent: () => BeforeRenderEvent,
|
|
BeforeRotateEvent: () => BeforeRotateEvent,
|
|
ClickEvent: () => ClickEvent,
|
|
ConfigChangedEvent: () => ConfigChangedEvent,
|
|
DoubleClickEvent: () => DoubleClickEvent,
|
|
FullscreenEvent: () => FullscreenEvent,
|
|
HideNotificationEvent: () => HideNotificationEvent,
|
|
HideOverlayEvent: () => HideOverlayEvent,
|
|
HidePanelEvent: () => HidePanelEvent,
|
|
HideTooltipEvent: () => HideTooltipEvent,
|
|
KeypressEvent: () => KeypressEvent,
|
|
LoadProgressEvent: () => LoadProgressEvent,
|
|
ObjectEnterEvent: () => ObjectEnterEvent,
|
|
ObjectEvent: () => ObjectEvent,
|
|
ObjectHoverEvent: () => ObjectHoverEvent,
|
|
ObjectLeaveEvent: () => ObjectLeaveEvent,
|
|
PanoramaErrorEvent: () => PanoramaErrorEvent,
|
|
PanoramaLoadEvent: () => PanoramaLoadEvent,
|
|
PanoramaLoadedEvent: () => PanoramaLoadedEvent,
|
|
PositionUpdatedEvent: () => PositionUpdatedEvent,
|
|
ReadyEvent: () => ReadyEvent,
|
|
RenderEvent: () => RenderEvent,
|
|
RollUpdatedEvent: () => RollUpdatedEvent,
|
|
ShowNotificationEvent: () => ShowNotificationEvent,
|
|
ShowOverlayEvent: () => ShowOverlayEvent,
|
|
ShowPanelEvent: () => ShowPanelEvent,
|
|
ShowTooltipEvent: () => ShowTooltipEvent,
|
|
SizeUpdatedEvent: () => SizeUpdatedEvent,
|
|
StopAllEvent: () => StopAllEvent,
|
|
ViewerEvent: () => ViewerEvent,
|
|
ZoomUpdatedEvent: () => ZoomUpdatedEvent
|
|
});
|
|
|
|
// src/lib/TypedEventTarget.ts
|
|
var TypedEvent = class extends Event {
|
|
constructor(type, cancelable = false) {
|
|
super(type, { cancelable });
|
|
}
|
|
};
|
|
var TypedEventTarget = class extends EventTarget {
|
|
dispatchEvent(e) {
|
|
return super.dispatchEvent(e);
|
|
}
|
|
/**
|
|
* @template T the name of event
|
|
* @template E the class of the event
|
|
*/
|
|
addEventListener(type, callback, options) {
|
|
super.addEventListener(type, callback, options);
|
|
}
|
|
/**
|
|
* @template T the name of event
|
|
* @template E the class of the event
|
|
*/
|
|
removeEventListener(type, callback, options) {
|
|
super.removeEventListener(type, callback, options);
|
|
}
|
|
};
|
|
|
|
// src/events.ts
|
|
var ViewerEvent = class extends TypedEvent {
|
|
};
|
|
var _BeforeAnimateEvent = class _BeforeAnimateEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(position, zoomLevel) {
|
|
super(_BeforeAnimateEvent.type, true);
|
|
this.position = position;
|
|
this.zoomLevel = zoomLevel;
|
|
}
|
|
};
|
|
_BeforeAnimateEvent.type = "before-animate";
|
|
var BeforeAnimateEvent = _BeforeAnimateEvent;
|
|
var _BeforeRenderEvent = class _BeforeRenderEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(timestamp, elapsed) {
|
|
super(_BeforeRenderEvent.type);
|
|
this.timestamp = timestamp;
|
|
this.elapsed = elapsed;
|
|
}
|
|
};
|
|
_BeforeRenderEvent.type = "before-render";
|
|
var BeforeRenderEvent = _BeforeRenderEvent;
|
|
var _BeforeRotateEvent = class _BeforeRotateEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(position) {
|
|
super(_BeforeRotateEvent.type, true);
|
|
this.position = position;
|
|
}
|
|
};
|
|
_BeforeRotateEvent.type = "before-rotate";
|
|
var BeforeRotateEvent = _BeforeRotateEvent;
|
|
var _ClickEvent = class _ClickEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(data) {
|
|
super(_ClickEvent.type);
|
|
this.data = data;
|
|
}
|
|
};
|
|
_ClickEvent.type = "click";
|
|
var ClickEvent = _ClickEvent;
|
|
var _ConfigChangedEvent = class _ConfigChangedEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(options) {
|
|
super(_ConfigChangedEvent.type);
|
|
this.options = options;
|
|
}
|
|
/**
|
|
* Checks if at least one of the `options` has been modified
|
|
*/
|
|
containsOptions(...options) {
|
|
return options.some((option) => this.options.includes(option));
|
|
}
|
|
};
|
|
_ConfigChangedEvent.type = "config-changed";
|
|
var ConfigChangedEvent = _ConfigChangedEvent;
|
|
var _DoubleClickEvent = class _DoubleClickEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(data) {
|
|
super(_DoubleClickEvent.type);
|
|
this.data = data;
|
|
}
|
|
};
|
|
_DoubleClickEvent.type = "dblclick";
|
|
var DoubleClickEvent = _DoubleClickEvent;
|
|
var _FullscreenEvent = class _FullscreenEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(fullscreenEnabled) {
|
|
super(_FullscreenEvent.type);
|
|
this.fullscreenEnabled = fullscreenEnabled;
|
|
}
|
|
};
|
|
_FullscreenEvent.type = "fullscreen";
|
|
var FullscreenEvent = _FullscreenEvent;
|
|
var _HideNotificationEvent = class _HideNotificationEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(notificationId) {
|
|
super(_HideNotificationEvent.type);
|
|
this.notificationId = notificationId;
|
|
}
|
|
};
|
|
_HideNotificationEvent.type = "hide-notification";
|
|
var HideNotificationEvent = _HideNotificationEvent;
|
|
var _HideOverlayEvent = class _HideOverlayEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(overlayId) {
|
|
super(_HideOverlayEvent.type);
|
|
this.overlayId = overlayId;
|
|
}
|
|
};
|
|
_HideOverlayEvent.type = "hide-overlay";
|
|
var HideOverlayEvent = _HideOverlayEvent;
|
|
var _HidePanelEvent = class _HidePanelEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(panelId) {
|
|
super(_HidePanelEvent.type);
|
|
this.panelId = panelId;
|
|
}
|
|
};
|
|
_HidePanelEvent.type = "hide-panel";
|
|
var HidePanelEvent = _HidePanelEvent;
|
|
var _HideTooltipEvent = class _HideTooltipEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(tooltipData) {
|
|
super(_HideTooltipEvent.type);
|
|
this.tooltipData = tooltipData;
|
|
}
|
|
};
|
|
_HideTooltipEvent.type = "hide-tooltip";
|
|
var HideTooltipEvent = _HideTooltipEvent;
|
|
var _KeypressEvent = class _KeypressEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(key) {
|
|
super(_KeypressEvent.type, true);
|
|
this.key = key;
|
|
}
|
|
};
|
|
_KeypressEvent.type = "key-press";
|
|
var KeypressEvent = _KeypressEvent;
|
|
var _LoadProgressEvent = class _LoadProgressEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(progress) {
|
|
super(_LoadProgressEvent.type);
|
|
this.progress = progress;
|
|
}
|
|
};
|
|
_LoadProgressEvent.type = "load-progress";
|
|
var LoadProgressEvent = _LoadProgressEvent;
|
|
var _PanoramaLoadEvent = class _PanoramaLoadEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(panorama) {
|
|
super(_PanoramaLoadEvent.type);
|
|
this.panorama = panorama;
|
|
}
|
|
};
|
|
_PanoramaLoadEvent.type = "panorama-load";
|
|
var PanoramaLoadEvent = _PanoramaLoadEvent;
|
|
var _PanoramaLoadedEvent = class _PanoramaLoadedEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(data) {
|
|
super(_PanoramaLoadedEvent.type);
|
|
this.data = data;
|
|
}
|
|
};
|
|
_PanoramaLoadedEvent.type = "panorama-loaded";
|
|
var PanoramaLoadedEvent = _PanoramaLoadedEvent;
|
|
var _PanoramaErrorEvent = class _PanoramaErrorEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(panorama, error) {
|
|
super(_PanoramaErrorEvent.type);
|
|
this.panorama = panorama;
|
|
this.error = error;
|
|
}
|
|
};
|
|
_PanoramaErrorEvent.type = "panorama-error";
|
|
var PanoramaErrorEvent = _PanoramaErrorEvent;
|
|
var _PositionUpdatedEvent = class _PositionUpdatedEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(position) {
|
|
super(_PositionUpdatedEvent.type);
|
|
this.position = position;
|
|
}
|
|
};
|
|
_PositionUpdatedEvent.type = "position-updated";
|
|
var PositionUpdatedEvent = _PositionUpdatedEvent;
|
|
var _RollUpdatedEvent = class _RollUpdatedEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(roll) {
|
|
super(_RollUpdatedEvent.type);
|
|
this.roll = roll;
|
|
}
|
|
};
|
|
_RollUpdatedEvent.type = "roll-updated";
|
|
var RollUpdatedEvent = _RollUpdatedEvent;
|
|
var _ReadyEvent = class _ReadyEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor() {
|
|
super(_ReadyEvent.type);
|
|
}
|
|
};
|
|
_ReadyEvent.type = "ready";
|
|
var ReadyEvent = _ReadyEvent;
|
|
var _RenderEvent = class _RenderEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor() {
|
|
super(_RenderEvent.type);
|
|
}
|
|
};
|
|
_RenderEvent.type = "render";
|
|
var RenderEvent = _RenderEvent;
|
|
var _ShowNotificationEvent = class _ShowNotificationEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(notificationId) {
|
|
super(_ShowNotificationEvent.type);
|
|
this.notificationId = notificationId;
|
|
}
|
|
};
|
|
_ShowNotificationEvent.type = "show-notification";
|
|
var ShowNotificationEvent = _ShowNotificationEvent;
|
|
var _ShowOverlayEvent = class _ShowOverlayEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(overlayId) {
|
|
super(_ShowOverlayEvent.type);
|
|
this.overlayId = overlayId;
|
|
}
|
|
};
|
|
_ShowOverlayEvent.type = "show-overlay";
|
|
var ShowOverlayEvent = _ShowOverlayEvent;
|
|
var _ShowPanelEvent = class _ShowPanelEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(panelId) {
|
|
super(_ShowPanelEvent.type);
|
|
this.panelId = panelId;
|
|
}
|
|
};
|
|
_ShowPanelEvent.type = "show-panel";
|
|
var ShowPanelEvent = _ShowPanelEvent;
|
|
var _ShowTooltipEvent = class _ShowTooltipEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(tooltip, tooltipData) {
|
|
super(_ShowTooltipEvent.type);
|
|
this.tooltip = tooltip;
|
|
this.tooltipData = tooltipData;
|
|
}
|
|
};
|
|
_ShowTooltipEvent.type = "show-tooltip";
|
|
var ShowTooltipEvent = _ShowTooltipEvent;
|
|
var _SizeUpdatedEvent = class _SizeUpdatedEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(size) {
|
|
super(_SizeUpdatedEvent.type);
|
|
this.size = size;
|
|
}
|
|
};
|
|
_SizeUpdatedEvent.type = "size-updated";
|
|
var SizeUpdatedEvent = _SizeUpdatedEvent;
|
|
var _StopAllEvent = class _StopAllEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor() {
|
|
super(_StopAllEvent.type);
|
|
}
|
|
};
|
|
_StopAllEvent.type = "stop-all";
|
|
var StopAllEvent = _StopAllEvent;
|
|
var _ZoomUpdatedEvent = class _ZoomUpdatedEvent extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(zoomLevel) {
|
|
super(_ZoomUpdatedEvent.type);
|
|
this.zoomLevel = zoomLevel;
|
|
}
|
|
};
|
|
_ZoomUpdatedEvent.type = "zoom-updated";
|
|
var ZoomUpdatedEvent = _ZoomUpdatedEvent;
|
|
var ObjectEvent = class extends ViewerEvent {
|
|
/** @internal */
|
|
constructor(type, originalEvent, object, viewerPoint, userDataKey) {
|
|
super(type);
|
|
this.originalEvent = originalEvent;
|
|
this.object = object;
|
|
this.viewerPoint = viewerPoint;
|
|
this.userDataKey = userDataKey;
|
|
}
|
|
};
|
|
var _ObjectEnterEvent = class _ObjectEnterEvent extends ObjectEvent {
|
|
/** @internal */
|
|
constructor(originalEvent, object, viewerPoint, userDataKey) {
|
|
super(_ObjectEnterEvent.type, originalEvent, object, viewerPoint, userDataKey);
|
|
}
|
|
};
|
|
_ObjectEnterEvent.type = "enter-object";
|
|
var ObjectEnterEvent = _ObjectEnterEvent;
|
|
var _ObjectLeaveEvent = class _ObjectLeaveEvent extends ObjectEvent {
|
|
/** @internal */
|
|
constructor(originalEvent, object, viewerPoint, userDataKey) {
|
|
super(_ObjectLeaveEvent.type, originalEvent, object, viewerPoint, userDataKey);
|
|
}
|
|
};
|
|
_ObjectLeaveEvent.type = "leave-object";
|
|
var ObjectLeaveEvent = _ObjectLeaveEvent;
|
|
var _ObjectHoverEvent = class _ObjectHoverEvent extends ObjectEvent {
|
|
/** @internal */
|
|
constructor(originalEvent, object, viewerPoint, userDataKey) {
|
|
super(_ObjectHoverEvent.type, originalEvent, object, viewerPoint, userDataKey);
|
|
}
|
|
};
|
|
_ObjectHoverEvent.type = "hover-object";
|
|
var ObjectHoverEvent = _ObjectHoverEvent;
|
|
|
|
// src/adapters/AbstractAdapter.ts
|
|
var AbstractAdapter = class {
|
|
constructor(viewer) {
|
|
this.viewer = viewer;
|
|
}
|
|
/**
|
|
* Initializes the adapter
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
init() {
|
|
}
|
|
/**
|
|
* Destroys the adapter
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
destroy() {
|
|
}
|
|
/**
|
|
* Indicates if the adapter supports transitions between panoramas
|
|
*/
|
|
// @ts-ignore unused parameter
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
supportsTransition(panorama) {
|
|
return false;
|
|
}
|
|
/**
|
|
* Indicates if the adapter supports preload of a panorama
|
|
*/
|
|
// @ts-ignore unused parameter
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
supportsPreload(panorama) {
|
|
return false;
|
|
}
|
|
/**
|
|
* Converts pixel texture coordinates to spherical radians coordinates
|
|
* @throws {@link PSVError} when the current adapter does not support texture coordinates
|
|
*/
|
|
// @ts-ignore unused parameter
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
textureCoordsToSphericalCoords(point, data) {
|
|
throw new PSVError("Current adapter does not support texture coordinates.");
|
|
}
|
|
/**
|
|
* Converts spherical radians coordinates to pixel texture coordinates
|
|
* @throws {@link PSVError} when the current adapter does not support texture coordinates
|
|
*/
|
|
// @ts-ignore unused parameter
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
sphericalCoordsToTextureCoords(position, data) {
|
|
throw new PSVError("Current adapter does not support texture coordinates.");
|
|
}
|
|
};
|
|
/**
|
|
* Indicates if the adapter supports panorama download natively
|
|
*/
|
|
AbstractAdapter.supportsDownload = false;
|
|
function adapterInterop(adapter) {
|
|
if (adapter) {
|
|
for (const [, p] of [["_", adapter], ...Object.entries(adapter)]) {
|
|
if (p.prototype instanceof AbstractAdapter) {
|
|
checkVersion(p.id, p.VERSION, "5.7.3");
|
|
return p;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// src/adapters/EquirectangularAdapter.ts
|
|
var import_three3 = require_three();
|
|
|
|
// src/data/system.ts
|
|
var LOCALSTORAGE_TOUCH_SUPPORT = `${VIEWER_DATA}_touchSupport`;
|
|
var SYSTEM = {
|
|
/**
|
|
* Indicates if the system data has been loaded
|
|
*/
|
|
loaded: false,
|
|
/**
|
|
* Device screen pixel ratio
|
|
*/
|
|
pixelRatio: 1,
|
|
/**
|
|
* Device supports WebGL
|
|
*/
|
|
isWebGLSupported: false,
|
|
/**
|
|
* Maximum WebGL texture width
|
|
*/
|
|
maxTextureWidth: 0,
|
|
/**
|
|
* Device supports touch events
|
|
*/
|
|
isTouchEnabled: null,
|
|
/**
|
|
* Name of the fullscreen event
|
|
*/
|
|
fullscreenEvent: null,
|
|
/**
|
|
* @internal
|
|
*/
|
|
__maxCanvasWidth: null,
|
|
/**
|
|
* Maximum canvas width
|
|
*/
|
|
get maxCanvasWidth() {
|
|
if (this.__maxCanvasWidth === null) {
|
|
this.__maxCanvasWidth = getMaxCanvasWidth(this.maxTextureWidth);
|
|
}
|
|
return this.__maxCanvasWidth;
|
|
},
|
|
/**
|
|
* Loads the system if not already loaded
|
|
* @internal
|
|
*/
|
|
load() {
|
|
if (!this.loaded) {
|
|
const ctx = getWebGLCtx();
|
|
this.pixelRatio = window.devicePixelRatio || 1;
|
|
this.isWebGLSupported = ctx !== null;
|
|
this.maxTextureWidth = ctx ? ctx.getParameter(ctx.MAX_TEXTURE_SIZE) : 0;
|
|
this.isTouchEnabled = isTouchEnabled();
|
|
this.fullscreenEvent = getFullscreenEvent();
|
|
this.loaded = true;
|
|
}
|
|
if (!SYSTEM.isWebGLSupported) {
|
|
throw new PSVError("WebGL is not supported.");
|
|
}
|
|
if (SYSTEM.maxTextureWidth === 0) {
|
|
throw new PSVError("Unable to detect system capabilities");
|
|
}
|
|
}
|
|
};
|
|
function getWebGLCtx() {
|
|
const canvas = document.createElement("canvas");
|
|
const names = ["webgl2", "webgl", "experimental-webgl"];
|
|
let context = null;
|
|
if (!canvas.getContext) {
|
|
return null;
|
|
}
|
|
if (names.some((name) => {
|
|
try {
|
|
context = canvas.getContext(name);
|
|
return context !== null;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
})) {
|
|
return context;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
function isTouchEnabled() {
|
|
let initial = "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
if (LOCALSTORAGE_TOUCH_SUPPORT in localStorage) {
|
|
initial = localStorage[LOCALSTORAGE_TOUCH_SUPPORT] === "true";
|
|
}
|
|
const promise = new Promise((resolve) => {
|
|
const clear = () => {
|
|
window.removeEventListener("mousedown", listenerMouse);
|
|
window.removeEventListener("touchstart", listenerTouch);
|
|
clearTimeout(listenerTimeoutId);
|
|
};
|
|
const listenerMouse = () => {
|
|
clear();
|
|
localStorage[LOCALSTORAGE_TOUCH_SUPPORT] = false;
|
|
resolve(false);
|
|
};
|
|
const listenerTouch = () => {
|
|
clear();
|
|
localStorage[LOCALSTORAGE_TOUCH_SUPPORT] = true;
|
|
resolve(true);
|
|
};
|
|
const listenerTimeout = () => {
|
|
clear();
|
|
localStorage[LOCALSTORAGE_TOUCH_SUPPORT] = initial;
|
|
resolve(initial);
|
|
};
|
|
window.addEventListener("mousedown", listenerMouse, false);
|
|
window.addEventListener("touchstart", listenerTouch, false);
|
|
const listenerTimeoutId = setTimeout(listenerTimeout, 1e4);
|
|
});
|
|
return { initial, promise };
|
|
}
|
|
function getMaxCanvasWidth(maxWidth) {
|
|
const canvas = document.createElement("canvas");
|
|
const ctx = canvas.getContext("2d");
|
|
canvas.width = maxWidth;
|
|
canvas.height = maxWidth / 2;
|
|
while (canvas.width > 1024) {
|
|
ctx.fillStyle = "white";
|
|
ctx.fillRect(0, 0, 1, 1);
|
|
try {
|
|
if (ctx.getImageData(0, 0, 1, 1).data[0] > 0) {
|
|
return canvas.width;
|
|
}
|
|
} catch (e) {
|
|
}
|
|
canvas.width /= 2;
|
|
canvas.height /= 2;
|
|
}
|
|
throw new PSVError("Unable to detect system capabilities");
|
|
}
|
|
function getFullscreenEvent() {
|
|
if ("exitFullscreen" in document) {
|
|
return "fullscreenchange";
|
|
}
|
|
if ("webkitExitFullscreen" in document) {
|
|
return "webkitfullscreenchange";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// src/adapters/interpolationWorker.ts
|
|
function interpolationWorker() {
|
|
self.onmessage = (e) => {
|
|
const panoData = e.data.panoData;
|
|
const buffer = new OffscreenCanvas(panoData.fullWidth, panoData.fullHeight);
|
|
const ctx = buffer.getContext("2d");
|
|
const img = new OffscreenCanvas(panoData.croppedWidth, panoData.croppedHeight);
|
|
const ctxImg = img.getContext("2d");
|
|
ctxImg.putImageData(e.data.image, 0, 0);
|
|
autoBackground(buffer, img, panoData);
|
|
postMessage(ctx.getImageData(0, 0, buffer.width, buffer.height));
|
|
};
|
|
function autoBackground(buffer, img, panoData) {
|
|
const croppedY2 = panoData.fullHeight - panoData.croppedHeight - panoData.croppedY;
|
|
const croppedX2 = panoData.fullWidth - panoData.croppedWidth - panoData.croppedX;
|
|
const middleY = panoData.croppedY + panoData.croppedHeight / 2;
|
|
const blurSize = buffer.width / 32;
|
|
const padding = blurSize;
|
|
const edge = 10;
|
|
const filter = `blur(${blurSize}px)`;
|
|
const ctx = buffer.getContext("2d");
|
|
ctx.drawImage(
|
|
img,
|
|
panoData.croppedX,
|
|
panoData.croppedY,
|
|
panoData.croppedWidth,
|
|
panoData.croppedHeight
|
|
);
|
|
if (panoData.croppedY > 0) {
|
|
if (panoData.croppedX > 0 || croppedX2 > 0) {
|
|
ctx.filter = "none";
|
|
const colorLeft = getAverageColor(ctx, panoData.croppedX, panoData.croppedY, edge, edge, 2);
|
|
const colorRight = getAverageColor(ctx, buffer.width - croppedX2 - 11, panoData.croppedY, edge, edge, 2);
|
|
const colorCenter = averageRgb(colorLeft, colorRight);
|
|
if (panoData.croppedX > 0) {
|
|
ctx.fillStyle = createHorizontalGradient(ctx, 0, panoData.croppedX, colorCenter, colorLeft);
|
|
ctx.fillRect(-padding, -padding, panoData.croppedX + padding * 2, middleY + padding);
|
|
}
|
|
if (croppedX2 > 0) {
|
|
ctx.fillStyle = createHorizontalGradient(ctx, buffer.width - croppedX2, buffer.width, colorRight, colorCenter);
|
|
ctx.fillRect(buffer.width - croppedX2 - padding, -padding, croppedX2 + padding * 2, middleY + padding);
|
|
}
|
|
}
|
|
ctx.filter = filter;
|
|
ctx.drawImage(
|
|
img,
|
|
0,
|
|
0,
|
|
img.width,
|
|
edge,
|
|
panoData.croppedX,
|
|
-padding,
|
|
panoData.croppedWidth,
|
|
panoData.croppedY + padding * 2
|
|
);
|
|
ctx.fillStyle = rgbCss(getAverageColor(ctx, 0, 0, buffer.width, edge, edge));
|
|
ctx.fillRect(-padding, -padding, buffer.width + padding * 2, padding * 2);
|
|
}
|
|
if (croppedY2 > 0) {
|
|
if (panoData.croppedX > 0 || croppedX2 > 0) {
|
|
ctx.filter = "none";
|
|
const colorLeft = getAverageColor(ctx, panoData.croppedX, buffer.height - croppedY2 - 1 - edge, edge, edge, 2);
|
|
const colorRight = getAverageColor(ctx, buffer.width - croppedX2 - 1 - edge, buffer.height - croppedY2 - 1 - edge, edge, edge, 2);
|
|
const colorCenter = averageRgb(colorLeft, colorRight);
|
|
if (panoData.croppedX > 0) {
|
|
ctx.fillStyle = createHorizontalGradient(ctx, 0, panoData.croppedX, colorCenter, colorLeft);
|
|
ctx.fillRect(-padding, middleY, panoData.croppedX + padding * 2, buffer.height - middleY + padding);
|
|
}
|
|
if (croppedX2 > 0) {
|
|
ctx.fillStyle = createHorizontalGradient(ctx, buffer.width - croppedX2, buffer.width, colorRight, colorCenter);
|
|
ctx.fillRect(buffer.width - croppedX2 - padding, middleY, croppedX2 + padding * 2, buffer.height - middleY + padding);
|
|
}
|
|
}
|
|
ctx.filter = filter;
|
|
ctx.drawImage(
|
|
img,
|
|
0,
|
|
img.height - edge,
|
|
img.width,
|
|
edge,
|
|
panoData.croppedX,
|
|
buffer.height - croppedY2 - padding,
|
|
panoData.croppedWidth,
|
|
croppedY2 + padding * 2
|
|
);
|
|
ctx.fillStyle = rgbCss(getAverageColor(ctx, 0, buffer.height - 1 - edge, buffer.width, edge, edge));
|
|
ctx.fillRect(-padding, buffer.height - padding, buffer.width + padding * 2, padding * 2);
|
|
}
|
|
if (panoData.croppedX > 0) {
|
|
ctx.filter = filter;
|
|
ctx.drawImage(
|
|
img,
|
|
img.width - edge,
|
|
0,
|
|
edge,
|
|
img.height,
|
|
-padding,
|
|
panoData.croppedY,
|
|
padding * 2,
|
|
panoData.croppedHeight
|
|
);
|
|
ctx.drawImage(
|
|
img,
|
|
0,
|
|
0,
|
|
edge,
|
|
img.height,
|
|
0,
|
|
panoData.croppedY,
|
|
panoData.croppedX + padding,
|
|
panoData.croppedHeight
|
|
);
|
|
}
|
|
if (croppedX2 > 0) {
|
|
ctx.filter = filter;
|
|
ctx.drawImage(
|
|
img,
|
|
0,
|
|
0,
|
|
edge,
|
|
img.height,
|
|
buffer.width - padding,
|
|
panoData.croppedY,
|
|
padding * 2,
|
|
panoData.croppedHeight
|
|
);
|
|
ctx.drawImage(
|
|
img,
|
|
img.width - edge,
|
|
0,
|
|
edge,
|
|
img.height,
|
|
buffer.width - croppedX2 - padding,
|
|
panoData.croppedY,
|
|
croppedX2 + padding,
|
|
panoData.croppedHeight
|
|
);
|
|
}
|
|
ctx.filter = "none";
|
|
ctx.drawImage(
|
|
img,
|
|
panoData.croppedX,
|
|
panoData.croppedY,
|
|
panoData.croppedWidth,
|
|
panoData.croppedHeight
|
|
);
|
|
}
|
|
function rgbCss(color) {
|
|
return `rgb(${color.r}, ${color.g}, ${color.b})`;
|
|
}
|
|
function averageRgb(c1, c2) {
|
|
return {
|
|
r: Math.round(c1.r / 2 + c2.r / 2),
|
|
g: Math.round(c1.g / 2 + c2.g / 2),
|
|
b: Math.round(c1.b / 2 + c2.b / 2)
|
|
};
|
|
}
|
|
function createHorizontalGradient(ctx, x1, x2, c1, c2) {
|
|
const grad = ctx.createLinearGradient(x1, 0, x2, 0);
|
|
grad.addColorStop(0, rgbCss(c1));
|
|
grad.addColorStop(1, rgbCss(c2));
|
|
return grad;
|
|
}
|
|
function getAverageColor(ctx, x, y, w, h, every) {
|
|
every = Math.round(every);
|
|
let r = 0;
|
|
let g = 0;
|
|
let b = 0;
|
|
let count = 0;
|
|
const data = ctx.getImageData(x, y, w, h);
|
|
for (let row = 0; row < h; row += every) {
|
|
for (let col = 0; col < w; col += every) {
|
|
const i = 4 * (row * w + col);
|
|
r += data.data[i];
|
|
g += data.data[i + 1];
|
|
b += data.data[i + 2];
|
|
count++;
|
|
}
|
|
}
|
|
r = Math.round(r / count);
|
|
g = Math.round(g / count);
|
|
b = Math.round(b / count);
|
|
return { r, g, b };
|
|
}
|
|
}
|
|
var interpolationWorkerSrc = URL.createObjectURL(
|
|
new Blob(
|
|
["(", interpolationWorker.toString(), ")()"],
|
|
{ type: "application/javascript" }
|
|
)
|
|
);
|
|
|
|
// src/adapters/EquirectangularAdapter.ts
|
|
var getConfig = getConfigParser(
|
|
{
|
|
backgroundColor: "#000",
|
|
interpolateBackground: false,
|
|
resolution: 64,
|
|
useXmpData: true,
|
|
blur: false
|
|
},
|
|
{
|
|
resolution: (resolution) => {
|
|
if (!resolution || !import_three3.MathUtils.isPowerOfTwo(resolution)) {
|
|
throw new PSVError("EquirectangularAdapter resolution must be power of two");
|
|
}
|
|
return resolution;
|
|
}
|
|
}
|
|
);
|
|
var EquirectangularAdapter = class extends AbstractAdapter {
|
|
constructor(viewer, config) {
|
|
super(viewer);
|
|
this.config = getConfig(config);
|
|
if (this.config.interpolateBackground) {
|
|
if (!window.Worker) {
|
|
logWarn("Web Worker API not available");
|
|
this.config.interpolateBackground = false;
|
|
} else {
|
|
this.interpolationWorker = new Worker(interpolationWorkerSrc);
|
|
}
|
|
}
|
|
this.SPHERE_SEGMENTS = this.config.resolution;
|
|
this.SPHERE_HORIZONTAL_SEGMENTS = this.SPHERE_SEGMENTS / 2;
|
|
}
|
|
supportsTransition() {
|
|
return true;
|
|
}
|
|
supportsPreload() {
|
|
return true;
|
|
}
|
|
destroy() {
|
|
this.interpolationWorker?.terminate();
|
|
super.destroy();
|
|
}
|
|
textureCoordsToSphericalCoords(point, data) {
|
|
if (isNil(point.textureX) || isNil(point.textureY)) {
|
|
throw new PSVError(`Texture position is missing 'textureX' or 'textureY'`);
|
|
}
|
|
const relativeX = (point.textureX + data.croppedX) / data.fullWidth * Math.PI * 2;
|
|
const relativeY = (point.textureY + data.croppedY) / data.fullHeight * Math.PI;
|
|
return {
|
|
yaw: relativeX >= Math.PI ? relativeX - Math.PI : relativeX + Math.PI,
|
|
pitch: Math.PI / 2 - relativeY
|
|
};
|
|
}
|
|
sphericalCoordsToTextureCoords(position, data) {
|
|
const relativeLong = position.yaw / Math.PI / 2 * data.fullWidth;
|
|
const relativeLat = position.pitch / Math.PI * data.fullHeight;
|
|
return {
|
|
textureX: Math.round(
|
|
position.yaw < Math.PI ? relativeLong + data.fullWidth / 2 : relativeLong - data.fullWidth / 2
|
|
) - data.croppedX,
|
|
textureY: Math.round(data.fullHeight / 2 - relativeLat) - data.croppedY
|
|
};
|
|
}
|
|
async loadTexture(panorama, loader = true, newPanoData, useXmpPanoData = this.config.useXmpData) {
|
|
if (typeof panorama !== "string") {
|
|
return Promise.reject(new PSVError("Invalid panorama url, are you using the right adapter?"));
|
|
}
|
|
const blob = await this.viewer.textureLoader.loadFile(
|
|
panorama,
|
|
loader ? (p) => this.viewer.loader.setProgress(p) : null,
|
|
panorama
|
|
);
|
|
const xmpPanoData = useXmpPanoData ? await this.loadXMP(blob) : null;
|
|
const img = await this.viewer.textureLoader.blobToImage(blob);
|
|
if (typeof newPanoData === "function") {
|
|
newPanoData = newPanoData(img, xmpPanoData);
|
|
}
|
|
if (!newPanoData && !xmpPanoData) {
|
|
newPanoData = this.__defaultPanoData(img);
|
|
}
|
|
const panoData = {
|
|
isEquirectangular: true,
|
|
fullWidth: firstNonNull(newPanoData?.fullWidth, xmpPanoData?.fullWidth, img.width),
|
|
fullHeight: firstNonNull(newPanoData?.fullHeight, xmpPanoData?.fullHeight, img.height),
|
|
croppedWidth: firstNonNull(newPanoData?.croppedWidth, xmpPanoData?.croppedWidth, img.width),
|
|
croppedHeight: firstNonNull(newPanoData?.croppedHeight, xmpPanoData?.croppedHeight, img.height),
|
|
croppedX: firstNonNull(newPanoData?.croppedX, xmpPanoData?.croppedX, 0),
|
|
croppedY: firstNonNull(newPanoData?.croppedY, xmpPanoData?.croppedY, 0),
|
|
poseHeading: firstNonNull(newPanoData?.poseHeading, xmpPanoData?.poseHeading, 0),
|
|
posePitch: firstNonNull(newPanoData?.posePitch, xmpPanoData?.posePitch, 0),
|
|
poseRoll: firstNonNull(newPanoData?.poseRoll, xmpPanoData?.poseRoll, 0)
|
|
};
|
|
if (panoData.croppedWidth !== img.width || panoData.croppedHeight !== img.height) {
|
|
logWarn(`Invalid panoData, croppedWidth/croppedHeight is not coherent with the loaded image.
|
|
panoData: ${panoData.croppedWidth}x${panoData.croppedHeight}, image: ${img.width}x${img.height}`);
|
|
}
|
|
if (Math.abs(panoData.fullWidth - panoData.fullHeight * 2) > 1) {
|
|
logWarn("Invalid panoData, fullWidth should be twice fullHeight");
|
|
panoData.fullWidth = panoData.fullHeight * 2;
|
|
}
|
|
if (panoData.croppedX + panoData.croppedWidth > panoData.fullWidth) {
|
|
logWarn("Invalid panoData, croppedX + croppedWidth > fullWidth");
|
|
panoData.croppedX = panoData.fullWidth - panoData.croppedWidth;
|
|
}
|
|
if (panoData.croppedY + panoData.croppedHeight > panoData.fullHeight) {
|
|
logWarn("Invalid panoData, croppedY + croppedHeight > fullHeight");
|
|
panoData.croppedY = panoData.fullHeight - panoData.croppedHeight;
|
|
}
|
|
const texture = this.createEquirectangularTexture(img, panoData);
|
|
return {
|
|
panorama,
|
|
texture,
|
|
panoData,
|
|
cacheKey: panorama
|
|
};
|
|
}
|
|
/**
|
|
* Loads the XMP data of an image
|
|
*/
|
|
async loadXMP(blob) {
|
|
const binary = await this.loadBlobAsString(blob);
|
|
const a = binary.indexOf("<x:xmpmeta");
|
|
const b = binary.indexOf("</x:xmpmeta>");
|
|
const data = binary.substring(a, b);
|
|
if (a !== -1 && b !== -1 && data.includes("GPano:")) {
|
|
return {
|
|
isEquirectangular: true,
|
|
fullWidth: getXMPValue(data, "FullPanoWidthPixels"),
|
|
fullHeight: getXMPValue(data, "FullPanoHeightPixels"),
|
|
croppedWidth: getXMPValue(data, "CroppedAreaImageWidthPixels"),
|
|
croppedHeight: getXMPValue(data, "CroppedAreaImageHeightPixels"),
|
|
croppedX: getXMPValue(data, "CroppedAreaLeftPixels"),
|
|
croppedY: getXMPValue(data, "CroppedAreaTopPixels"),
|
|
poseHeading: getXMPValue(data, "PoseHeadingDegrees"),
|
|
posePitch: getXMPValue(data, "PosePitchDegrees"),
|
|
poseRoll: getXMPValue(data, "PoseRollDegrees")
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Reads a Blob as a string
|
|
*/
|
|
loadBlobAsString(blob) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result);
|
|
reader.onerror = reject;
|
|
reader.readAsText(blob);
|
|
});
|
|
}
|
|
/**
|
|
* Creates the final texture from image and panorama data
|
|
*/
|
|
createEquirectangularTexture(img, panoData) {
|
|
if (this.config.blur || panoData.fullWidth > SYSTEM.maxTextureWidth || panoData.croppedWidth !== panoData.fullWidth || panoData.croppedHeight !== panoData.fullHeight) {
|
|
const ratio = Math.min(1, SYSTEM.maxCanvasWidth / panoData.fullWidth);
|
|
const resizedPanoData = {
|
|
fullWidth: panoData.fullWidth * ratio,
|
|
fullHeight: panoData.fullHeight * ratio,
|
|
croppedWidth: panoData.croppedWidth * ratio,
|
|
croppedHeight: panoData.croppedHeight * ratio,
|
|
croppedX: panoData.croppedX * ratio,
|
|
croppedY: panoData.croppedY * ratio
|
|
};
|
|
const buffer = document.createElement("canvas");
|
|
buffer.width = resizedPanoData.fullWidth;
|
|
buffer.height = resizedPanoData.fullHeight;
|
|
const ctx = buffer.getContext("2d");
|
|
if (this.config.backgroundColor) {
|
|
ctx.fillStyle = this.config.backgroundColor;
|
|
ctx.fillRect(0, 0, buffer.width, buffer.height);
|
|
}
|
|
if (this.config.blur) {
|
|
const blurSize = buffer.width / 2048;
|
|
const margin = Math.ceil(blurSize * 2);
|
|
if (resizedPanoData.croppedWidth === buffer.width) {
|
|
ctx.drawImage(
|
|
img,
|
|
0,
|
|
0,
|
|
margin / ratio,
|
|
img.height,
|
|
0,
|
|
resizedPanoData.croppedY,
|
|
margin,
|
|
resizedPanoData.croppedHeight
|
|
);
|
|
ctx.drawImage(
|
|
img,
|
|
img.width - margin / ratio,
|
|
0,
|
|
margin / ratio,
|
|
img.height,
|
|
buffer.width - margin,
|
|
resizedPanoData.croppedY,
|
|
margin,
|
|
resizedPanoData.croppedHeight
|
|
);
|
|
}
|
|
if (resizedPanoData.croppedHeight === buffer.height) {
|
|
ctx.drawImage(
|
|
img,
|
|
0,
|
|
0,
|
|
1,
|
|
1,
|
|
resizedPanoData.croppedX,
|
|
0,
|
|
resizedPanoData.croppedWidth,
|
|
margin
|
|
);
|
|
ctx.drawImage(
|
|
img,
|
|
0,
|
|
img.height - 1,
|
|
1,
|
|
1,
|
|
resizedPanoData.croppedX,
|
|
buffer.height - margin,
|
|
resizedPanoData.croppedWidth,
|
|
margin
|
|
);
|
|
}
|
|
ctx.filter = `blur(${blurSize}px)`;
|
|
}
|
|
ctx.drawImage(
|
|
img,
|
|
resizedPanoData.croppedX,
|
|
resizedPanoData.croppedY,
|
|
resizedPanoData.croppedWidth,
|
|
resizedPanoData.croppedHeight
|
|
);
|
|
const t = createTexture(buffer);
|
|
if (this.config.interpolateBackground && (panoData.croppedWidth !== panoData.fullWidth || panoData.croppedHeight !== panoData.fullHeight)) {
|
|
this.interpolationWorker.postMessage({
|
|
image: ctx.getImageData(
|
|
resizedPanoData.croppedX,
|
|
resizedPanoData.croppedY,
|
|
resizedPanoData.croppedWidth,
|
|
resizedPanoData.croppedHeight
|
|
),
|
|
panoData: resizedPanoData
|
|
});
|
|
this.interpolationWorker.onmessage = (e) => {
|
|
ctx.putImageData(e.data, 0, 0);
|
|
t.needsUpdate = true;
|
|
this.viewer.needsUpdate();
|
|
};
|
|
}
|
|
return t;
|
|
}
|
|
return createTexture(img);
|
|
}
|
|
createMesh(scale = 1) {
|
|
const geometry = new import_three3.SphereGeometry(
|
|
SPHERE_RADIUS * scale,
|
|
this.SPHERE_SEGMENTS,
|
|
this.SPHERE_HORIZONTAL_SEGMENTS,
|
|
-Math.PI / 2
|
|
).scale(-1, 1, 1);
|
|
return new import_three3.Mesh(geometry, new import_three3.MeshBasicMaterial());
|
|
}
|
|
setTexture(mesh, textureData) {
|
|
mesh.material.map = textureData.texture;
|
|
}
|
|
setTextureOpacity(mesh, opacity) {
|
|
mesh.material.opacity = opacity;
|
|
mesh.material.transparent = opacity < 1;
|
|
}
|
|
disposeTexture(textureData) {
|
|
textureData.texture?.dispose();
|
|
}
|
|
__defaultPanoData(img) {
|
|
const fullWidth = Math.max(img.width, img.height * 2);
|
|
const fullHeight = Math.round(fullWidth / 2);
|
|
const croppedX = Math.round((fullWidth - img.width) / 2);
|
|
const croppedY = Math.round((fullHeight - img.height) / 2);
|
|
return {
|
|
isEquirectangular: true,
|
|
fullWidth,
|
|
fullHeight,
|
|
croppedWidth: img.width,
|
|
croppedHeight: img.height,
|
|
croppedX,
|
|
croppedY
|
|
};
|
|
}
|
|
};
|
|
EquirectangularAdapter.id = "equirectangular";
|
|
EquirectangularAdapter.VERSION = "5.7.3";
|
|
EquirectangularAdapter.supportsDownload = true;
|
|
|
|
// src/components/AbstractComponent.ts
|
|
var AbstractComponent = class _AbstractComponent {
|
|
constructor(parent, config) {
|
|
this.parent = parent;
|
|
/**
|
|
* All child components
|
|
* @internal
|
|
*/
|
|
this.children = [];
|
|
/**
|
|
* Container element
|
|
*/
|
|
this.container = document.createElement("div");
|
|
/**
|
|
* Internal properties
|
|
* @internal
|
|
*/
|
|
this.state = {
|
|
visible: true
|
|
};
|
|
this.viewer = parent instanceof _AbstractComponent ? parent.viewer : parent;
|
|
this.container.className = config.className || "";
|
|
this.parent.children.push(this);
|
|
this.parent.container.appendChild(this.container);
|
|
}
|
|
/**
|
|
* Destroys the component
|
|
*/
|
|
destroy() {
|
|
this.parent.container.removeChild(this.container);
|
|
const childIdx = this.parent.children.indexOf(this);
|
|
if (childIdx !== -1) {
|
|
this.parent.children.splice(childIdx, 1);
|
|
}
|
|
this.children.slice().forEach((child) => child.destroy());
|
|
this.children.length = 0;
|
|
}
|
|
/**
|
|
* Displays or hides the component
|
|
*/
|
|
toggle(visible = !this.isVisible()) {
|
|
if (!visible) {
|
|
this.hide();
|
|
} else {
|
|
this.show();
|
|
}
|
|
}
|
|
/**
|
|
* Hides the component
|
|
*/
|
|
// @ts-ignore unused parameter
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
hide(options) {
|
|
this.container.style.display = "none";
|
|
this.state.visible = false;
|
|
}
|
|
/**
|
|
* Displays the component
|
|
*/
|
|
// @ts-ignore unused parameter
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
show(options) {
|
|
this.container.style.display = "";
|
|
this.state.visible = true;
|
|
}
|
|
/**
|
|
* Checks if the component is visible
|
|
*/
|
|
isVisible() {
|
|
return this.state.visible;
|
|
}
|
|
};
|
|
|
|
// src/buttons/AbstractButton.ts
|
|
var getConfig2 = getConfigParser({
|
|
id: null,
|
|
className: null,
|
|
title: null,
|
|
hoverScale: false,
|
|
collapsable: false,
|
|
tabbable: true,
|
|
icon: null,
|
|
iconActive: null
|
|
});
|
|
var AbstractButton = class extends AbstractComponent {
|
|
constructor(navbar, config) {
|
|
super(navbar, {
|
|
className: `psv-button ${config.hoverScale ? "psv-button--hover-scale" : ""} ${config.className || ""}`
|
|
});
|
|
/**
|
|
* Internal properties
|
|
*/
|
|
this.state = {
|
|
visible: true,
|
|
enabled: true,
|
|
supported: true,
|
|
collapsed: false,
|
|
active: false,
|
|
width: 0
|
|
};
|
|
this.config = getConfig2(config);
|
|
this.config.id = this.constructor.id;
|
|
if (config.icon) {
|
|
this.__setIcon(config.icon);
|
|
}
|
|
this.state.width = this.container.offsetWidth;
|
|
if (this.config.title) {
|
|
this.container.title = this.config.title;
|
|
} else if (this.id && this.id in this.viewer.config.lang) {
|
|
this.container.title = this.viewer.config.lang[this.id];
|
|
}
|
|
if (config.tabbable) {
|
|
this.container.tabIndex = 0;
|
|
}
|
|
this.container.addEventListener("click", (e) => {
|
|
if (this.state.enabled) {
|
|
this.onClick();
|
|
}
|
|
e.stopPropagation();
|
|
});
|
|
this.container.addEventListener("keydown", (e) => {
|
|
if (e.key === KEY_CODES.Enter && this.state.enabled) {
|
|
this.onClick();
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
}
|
|
get id() {
|
|
return this.config.id;
|
|
}
|
|
get title() {
|
|
return this.container.title;
|
|
}
|
|
get content() {
|
|
return this.container.innerHTML;
|
|
}
|
|
get width() {
|
|
return this.state.width;
|
|
}
|
|
get collapsable() {
|
|
return this.config.collapsable;
|
|
}
|
|
show(refresh = true) {
|
|
if (!this.isVisible()) {
|
|
this.state.visible = true;
|
|
if (!this.state.collapsed) {
|
|
this.container.style.display = "";
|
|
}
|
|
if (refresh) {
|
|
this.viewer.navbar.autoSize();
|
|
}
|
|
}
|
|
}
|
|
hide(refresh = true) {
|
|
if (this.isVisible()) {
|
|
this.state.visible = false;
|
|
this.container.style.display = "none";
|
|
if (refresh) {
|
|
this.viewer.navbar.autoSize();
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Hides/shows the button depending of the result of {@link isSupported}
|
|
* @internal
|
|
*/
|
|
checkSupported() {
|
|
resolveBoolean(this.isSupported(), (supported, init) => {
|
|
if (!this.state) {
|
|
return;
|
|
}
|
|
this.state.supported = supported;
|
|
if (!init) {
|
|
this.toggle(supported);
|
|
} else if (!supported) {
|
|
this.hide();
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Perform action when the navbar size/content changes
|
|
* @internal
|
|
*/
|
|
autoSize() {
|
|
}
|
|
/**
|
|
* Checks if the button can be displayed
|
|
*/
|
|
isSupported() {
|
|
return true;
|
|
}
|
|
/**
|
|
* Changes the active state of the button
|
|
*/
|
|
toggleActive(active = !this.state.active) {
|
|
if (active !== this.state.active) {
|
|
this.state.active = active;
|
|
toggleClass(this.container, "psv-button--active", this.state.active);
|
|
if (this.config.iconActive) {
|
|
this.__setIcon(this.state.active ? this.config.iconActive : this.config.icon);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Disables the button
|
|
*/
|
|
disable() {
|
|
this.container.classList.add("psv-button--disabled");
|
|
this.state.enabled = false;
|
|
}
|
|
/**
|
|
* Enables the button
|
|
*/
|
|
enable() {
|
|
this.container.classList.remove("psv-button--disabled");
|
|
this.state.enabled = true;
|
|
}
|
|
/**
|
|
* Collapses the button in the navbar menu
|
|
*/
|
|
collapse() {
|
|
this.state.collapsed = true;
|
|
this.container.style.display = "none";
|
|
}
|
|
/**
|
|
* Uncollapses the button from the navbar menu
|
|
*/
|
|
uncollapse() {
|
|
this.state.collapsed = false;
|
|
if (this.state.visible) {
|
|
this.container.style.display = "";
|
|
}
|
|
}
|
|
__setIcon(icon) {
|
|
this.container.innerHTML = icon;
|
|
addClasses(this.container.querySelector("svg"), "psv-button-svg");
|
|
}
|
|
};
|
|
|
|
// src/buttons/CustomButton.ts
|
|
var CustomButton = class extends AbstractButton {
|
|
constructor(navbar, config) {
|
|
super(navbar, {
|
|
className: `psv-custom-button ${config.className || ""}`,
|
|
hoverScale: false,
|
|
collapsable: config.collapsable !== false,
|
|
tabbable: config.tabbable !== false,
|
|
title: config.title
|
|
});
|
|
this.customOnClick = config.onClick;
|
|
if (config.id) {
|
|
this.config.id = config.id;
|
|
} else {
|
|
this.config.id = "psvButton-" + Math.random().toString(36).substring(2);
|
|
}
|
|
if (config.content) {
|
|
if (typeof config.content === "string") {
|
|
this.container.innerHTML = config.content;
|
|
} else {
|
|
this.container.classList.add("psv-custom-button--no-padding");
|
|
config.content.style.height = "100%";
|
|
config.content.attachViewer?.(this.viewer);
|
|
this.container.appendChild(config.content);
|
|
}
|
|
}
|
|
this.state.width = this.container.offsetWidth;
|
|
if (config.disabled) {
|
|
this.disable();
|
|
}
|
|
if (config.visible === false) {
|
|
this.hide();
|
|
}
|
|
}
|
|
onClick() {
|
|
this.customOnClick?.(this.viewer);
|
|
}
|
|
};
|
|
|
|
// src/buttons/DescriptionButton.ts
|
|
var DescriptionButton = class extends AbstractButton {
|
|
constructor(navbar) {
|
|
super(navbar, {
|
|
className: "psv-description-button",
|
|
hoverScale: true,
|
|
collapsable: false,
|
|
tabbable: true,
|
|
icon: ICONS.info
|
|
});
|
|
this.mode = 0 /* NONE */;
|
|
this.viewer.addEventListener(HideNotificationEvent.type, this);
|
|
this.viewer.addEventListener(ShowNotificationEvent.type, this);
|
|
this.viewer.addEventListener(HidePanelEvent.type, this);
|
|
this.viewer.addEventListener(ShowPanelEvent.type, this);
|
|
this.viewer.addEventListener(ConfigChangedEvent.type, this);
|
|
}
|
|
destroy() {
|
|
this.viewer.removeEventListener(HideNotificationEvent.type, this);
|
|
this.viewer.removeEventListener(ShowNotificationEvent.type, this);
|
|
this.viewer.removeEventListener(HidePanelEvent.type, this);
|
|
this.viewer.removeEventListener(ShowPanelEvent.type, this);
|
|
this.viewer.removeEventListener(ConfigChangedEvent.type, this);
|
|
super.destroy();
|
|
}
|
|
handleEvent(e) {
|
|
if (e instanceof ConfigChangedEvent) {
|
|
e.containsOptions("description") && this.autoSize(true);
|
|
return;
|
|
}
|
|
if (!this.mode) {
|
|
return;
|
|
}
|
|
let closed = false;
|
|
if (e instanceof HideNotificationEvent) {
|
|
closed = this.mode === 1 /* NOTIF */;
|
|
} else if (e instanceof ShowNotificationEvent) {
|
|
closed = this.mode === 1 /* NOTIF */ && e.notificationId !== IDS.DESCRIPTION;
|
|
} else if (e instanceof HidePanelEvent) {
|
|
closed = this.mode === 2 /* PANEL */;
|
|
} else if (e instanceof ShowPanelEvent) {
|
|
closed = this.mode === 2 /* PANEL */ && e.panelId !== IDS.DESCRIPTION;
|
|
}
|
|
if (closed) {
|
|
this.toggleActive(false);
|
|
this.mode = 0 /* NONE */;
|
|
}
|
|
}
|
|
onClick() {
|
|
if (this.mode) {
|
|
this.__close();
|
|
} else {
|
|
this.__open();
|
|
}
|
|
}
|
|
hide(refresh) {
|
|
super.hide(refresh);
|
|
if (this.mode) {
|
|
this.__close();
|
|
}
|
|
}
|
|
/**
|
|
* This button can only be refreshed from NavbarCaption
|
|
* @internal
|
|
*/
|
|
autoSize(refresh = false) {
|
|
if (refresh) {
|
|
const caption = this.viewer.navbar.getButton("caption", false);
|
|
const captionHidden = caption && !caption.isVisible();
|
|
const hasDescription = !!this.viewer.config.description;
|
|
if (captionHidden || hasDescription) {
|
|
this.show(false);
|
|
} else {
|
|
this.hide(false);
|
|
}
|
|
}
|
|
}
|
|
__close() {
|
|
switch (this.mode) {
|
|
case 1 /* NOTIF */:
|
|
this.viewer.notification.hide(IDS.DESCRIPTION);
|
|
break;
|
|
case 2 /* PANEL */:
|
|
this.viewer.panel.hide(IDS.DESCRIPTION);
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
__open() {
|
|
this.toggleActive(true);
|
|
if (this.viewer.config.description) {
|
|
this.mode = 2 /* PANEL */;
|
|
this.viewer.panel.show({
|
|
id: IDS.DESCRIPTION,
|
|
content: (this.viewer.config.caption ? `<p>${this.viewer.config.caption}</p>` : "") + this.viewer.config.description
|
|
});
|
|
} else {
|
|
this.mode = 1 /* NOTIF */;
|
|
this.viewer.notification.show({
|
|
id: IDS.DESCRIPTION,
|
|
content: this.viewer.config.caption
|
|
});
|
|
}
|
|
}
|
|
};
|
|
DescriptionButton.id = "description";
|
|
|
|
// src/buttons/DownloadButton.ts
|
|
var DownloadButton = class extends AbstractButton {
|
|
constructor(navbar) {
|
|
super(navbar, {
|
|
className: "psv-download-button",
|
|
hoverScale: true,
|
|
collapsable: true,
|
|
tabbable: true,
|
|
icon: ICONS.download
|
|
});
|
|
this.viewer.addEventListener(ConfigChangedEvent.type, this);
|
|
}
|
|
destroy() {
|
|
this.viewer.removeEventListener(ConfigChangedEvent.type, this);
|
|
super.destroy();
|
|
}
|
|
handleEvent(e) {
|
|
if (e instanceof ConfigChangedEvent) {
|
|
e.containsOptions("downloadUrl") && this.checkSupported();
|
|
}
|
|
}
|
|
onClick() {
|
|
const link = document.createElement("a");
|
|
link.href = this.viewer.config.downloadUrl || this.viewer.config.panorama;
|
|
if (link.href.startsWith("data:") && !this.viewer.config.downloadName) {
|
|
link.download = "panorama." + link.href.substring(0, link.href.indexOf(";")).split("/").pop();
|
|
} else {
|
|
link.download = this.viewer.config.downloadName || link.href.split("/").pop();
|
|
}
|
|
link.target = "_blank";
|
|
this.viewer.container.appendChild(link);
|
|
link.click();
|
|
setTimeout(() => {
|
|
this.viewer.container.removeChild(link);
|
|
}, 100);
|
|
}
|
|
checkSupported() {
|
|
const supported = this.viewer.adapter.constructor.supportsDownload || this.viewer.config.downloadUrl;
|
|
if (supported) {
|
|
this.show();
|
|
} else {
|
|
this.hide();
|
|
}
|
|
}
|
|
};
|
|
DownloadButton.id = "download";
|
|
|
|
// src/buttons/FullscreenButton.ts
|
|
var FullscreenButton = class extends AbstractButton {
|
|
constructor(navbar) {
|
|
super(navbar, {
|
|
className: "psv-fullscreen-button",
|
|
hoverScale: true,
|
|
collapsable: false,
|
|
tabbable: true,
|
|
icon: ICONS.fullscreenIn,
|
|
iconActive: ICONS.fullscreenOut
|
|
});
|
|
this.viewer.addEventListener(FullscreenEvent.type, this);
|
|
}
|
|
destroy() {
|
|
this.viewer.removeEventListener(FullscreenEvent.type, this);
|
|
super.destroy();
|
|
}
|
|
handleEvent(e) {
|
|
if (e instanceof FullscreenEvent) {
|
|
this.toggleActive(e.fullscreenEnabled);
|
|
}
|
|
}
|
|
onClick() {
|
|
this.viewer.toggleFullscreen();
|
|
}
|
|
};
|
|
FullscreenButton.id = "fullscreen";
|
|
|
|
// src/buttons/MenuButton.ts
|
|
var BUTTON_DATA = "psvButton";
|
|
var MENU_TEMPLATE = (buttons, title) => `
|
|
<div class="psv-panel-menu psv-panel-menu--stripped">
|
|
<h1 class="psv-panel-menu-title">${ICONS.menu} ${title}</h1>
|
|
<ul class="psv-panel-menu-list">
|
|
${buttons.map(
|
|
(button) => `
|
|
<li data-psv-button="${button.id}" class="psv-panel-menu-item" tabindex="0">
|
|
<span class="psv-panel-menu-item-icon">${button.content}</span>
|
|
<span class="psv-panel-menu-item-label">${button.title}</span>
|
|
</li>
|
|
`
|
|
).join("")}
|
|
</ul>
|
|
</div>
|
|
`;
|
|
var MenuButton = class extends AbstractButton {
|
|
constructor(navbar) {
|
|
super(navbar, {
|
|
className: "psv-menu-button",
|
|
hoverScale: true,
|
|
collapsable: false,
|
|
tabbable: true,
|
|
icon: ICONS.menu
|
|
});
|
|
this.viewer.addEventListener(ShowPanelEvent.type, this);
|
|
this.viewer.addEventListener(HidePanelEvent.type, this);
|
|
super.hide();
|
|
}
|
|
destroy() {
|
|
this.viewer.removeEventListener(ShowPanelEvent.type, this);
|
|
this.viewer.removeEventListener(HidePanelEvent.type, this);
|
|
super.destroy();
|
|
}
|
|
handleEvent(e) {
|
|
if (e instanceof ShowPanelEvent) {
|
|
this.toggleActive(e.panelId === IDS.MENU);
|
|
} else if (e instanceof HidePanelEvent) {
|
|
this.toggleActive(false);
|
|
}
|
|
}
|
|
onClick() {
|
|
if (this.state.active) {
|
|
this.__hideMenu();
|
|
} else {
|
|
this.__showMenu();
|
|
}
|
|
}
|
|
hide(refresh) {
|
|
super.hide(refresh);
|
|
this.__hideMenu();
|
|
}
|
|
show(refresh) {
|
|
super.show(refresh);
|
|
if (this.state.active) {
|
|
this.__showMenu();
|
|
}
|
|
}
|
|
__showMenu() {
|
|
this.viewer.panel.show({
|
|
id: IDS.MENU,
|
|
content: MENU_TEMPLATE(this.viewer.navbar.collapsed, this.viewer.config.lang.menu),
|
|
noMargin: true,
|
|
clickHandler: (target) => {
|
|
const li = target ? getClosest(target, "li") : void 0;
|
|
const buttonId = li ? li.dataset[BUTTON_DATA] : void 0;
|
|
if (buttonId) {
|
|
this.viewer.navbar.getButton(buttonId).onClick();
|
|
this.__hideMenu();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
__hideMenu() {
|
|
this.viewer.panel.hide(IDS.MENU);
|
|
}
|
|
};
|
|
MenuButton.id = "menu";
|
|
|
|
// src/buttons/AbstractMoveButton.ts
|
|
function getIcon(value) {
|
|
let angle2 = 0;
|
|
switch (value) {
|
|
case 0 /* UP */:
|
|
angle2 = 90;
|
|
break;
|
|
case 1 /* DOWN */:
|
|
angle2 = -90;
|
|
break;
|
|
case 3 /* RIGHT */:
|
|
angle2 = 180;
|
|
break;
|
|
default:
|
|
angle2 = 0;
|
|
break;
|
|
}
|
|
return ICONS.arrow.replace("rotate(0", `rotate(${angle2}`);
|
|
}
|
|
var AbstractMoveButton = class extends AbstractButton {
|
|
constructor(navbar, direction) {
|
|
super(navbar, {
|
|
className: "psv-move-button",
|
|
hoverScale: true,
|
|
collapsable: false,
|
|
tabbable: true,
|
|
icon: getIcon(direction)
|
|
});
|
|
this.direction = direction;
|
|
this.handler = new PressHandler();
|
|
this.container.addEventListener("mousedown", this);
|
|
this.container.addEventListener("keydown", this);
|
|
this.container.addEventListener("keyup", this);
|
|
this.viewer.container.addEventListener("mouseup", this);
|
|
this.viewer.container.addEventListener("touchend", this);
|
|
}
|
|
destroy() {
|
|
this.__onMouseUp();
|
|
this.viewer.container.removeEventListener("mouseup", this);
|
|
this.viewer.container.removeEventListener("touchend", this);
|
|
super.destroy();
|
|
}
|
|
handleEvent(e) {
|
|
switch (e.type) {
|
|
case "mousedown":
|
|
this.__onMouseDown();
|
|
break;
|
|
case "mouseup":
|
|
this.__onMouseUp();
|
|
break;
|
|
case "touchend":
|
|
this.__onMouseUp();
|
|
break;
|
|
case "keydown":
|
|
e.key === KEY_CODES.Enter && this.__onMouseDown();
|
|
break;
|
|
case "keyup":
|
|
e.key === KEY_CODES.Enter && this.__onMouseUp();
|
|
break;
|
|
}
|
|
}
|
|
onClick() {
|
|
}
|
|
isSupported() {
|
|
return invertResolvableBoolean(SYSTEM.isTouchEnabled);
|
|
}
|
|
__onMouseDown() {
|
|
if (!this.state.enabled) {
|
|
return;
|
|
}
|
|
const dynamicRoll = {};
|
|
switch (this.direction) {
|
|
case 0 /* UP */:
|
|
dynamicRoll.pitch = false;
|
|
break;
|
|
case 1 /* DOWN */:
|
|
dynamicRoll.pitch = true;
|
|
break;
|
|
case 3 /* RIGHT */:
|
|
dynamicRoll.yaw = false;
|
|
break;
|
|
default:
|
|
dynamicRoll.yaw = true;
|
|
break;
|
|
}
|
|
this.viewer.stopAll();
|
|
this.viewer.dynamics.position.roll(dynamicRoll);
|
|
this.handler.down();
|
|
}
|
|
__onMouseUp() {
|
|
if (!this.state.enabled) {
|
|
return;
|
|
}
|
|
this.handler.up(() => {
|
|
this.viewer.dynamics.position.stop();
|
|
this.viewer.resetIdleTimer();
|
|
});
|
|
}
|
|
};
|
|
AbstractMoveButton.groupId = "move";
|
|
|
|
// src/buttons/MoveDownButton.ts
|
|
var MoveDownButton = class extends AbstractMoveButton {
|
|
constructor(navbar) {
|
|
super(navbar, 1 /* DOWN */);
|
|
}
|
|
};
|
|
MoveDownButton.id = "moveDown";
|
|
|
|
// src/buttons/MoveLeftButton.ts
|
|
var MoveLeftButton = class extends AbstractMoveButton {
|
|
constructor(navbar) {
|
|
super(navbar, 2 /* LEFT */);
|
|
}
|
|
};
|
|
MoveLeftButton.id = "moveLeft";
|
|
|
|
// src/buttons/MoveRightButton.ts
|
|
var MoveRightButton = class extends AbstractMoveButton {
|
|
constructor(navbar) {
|
|
super(navbar, 3 /* RIGHT */);
|
|
}
|
|
};
|
|
MoveRightButton.id = "moveRight";
|
|
|
|
// src/buttons/MoveUpButton.ts
|
|
var MoveUpButton = class extends AbstractMoveButton {
|
|
constructor(navbar) {
|
|
super(navbar, 0 /* UP */);
|
|
}
|
|
};
|
|
MoveUpButton.id = "moveUp";
|
|
|
|
// src/buttons/AbstractZoomButton.ts
|
|
var AbstractZoomButton = class extends AbstractButton {
|
|
constructor(navbar, icon, direction) {
|
|
super(navbar, {
|
|
className: "psv-zoom-button",
|
|
hoverScale: true,
|
|
collapsable: false,
|
|
tabbable: true,
|
|
icon
|
|
});
|
|
this.direction = direction;
|
|
this.handler = new PressHandler();
|
|
this.container.addEventListener("mousedown", this);
|
|
this.container.addEventListener("keydown", this);
|
|
this.container.addEventListener("keyup", this);
|
|
this.viewer.container.addEventListener("mouseup", this);
|
|
this.viewer.container.addEventListener("touchend", this);
|
|
}
|
|
destroy() {
|
|
this.__onMouseUp();
|
|
this.viewer.container.removeEventListener("mouseup", this);
|
|
this.viewer.container.removeEventListener("touchend", this);
|
|
super.destroy();
|
|
}
|
|
handleEvent(e) {
|
|
switch (e.type) {
|
|
case "mousedown":
|
|
this.__onMouseDown();
|
|
break;
|
|
case "mouseup":
|
|
this.__onMouseUp();
|
|
break;
|
|
case "touchend":
|
|
this.__onMouseUp();
|
|
break;
|
|
case "keydown":
|
|
e.key === KEY_CODES.Enter && this.__onMouseDown();
|
|
break;
|
|
case "keyup":
|
|
e.key === KEY_CODES.Enter && this.__onMouseUp();
|
|
break;
|
|
}
|
|
}
|
|
onClick() {
|
|
}
|
|
isSupported() {
|
|
return invertResolvableBoolean(SYSTEM.isTouchEnabled);
|
|
}
|
|
__onMouseDown() {
|
|
if (!this.state.enabled) {
|
|
return;
|
|
}
|
|
this.viewer.dynamics.zoom.roll(this.direction === 1 /* OUT */);
|
|
this.handler.down();
|
|
}
|
|
__onMouseUp() {
|
|
if (!this.state.enabled) {
|
|
return;
|
|
}
|
|
this.handler.up(() => this.viewer.dynamics.zoom.stop());
|
|
}
|
|
};
|
|
AbstractZoomButton.groupId = "zoom";
|
|
|
|
// src/buttons/ZoomInButton.ts
|
|
var ZoomInButton = class extends AbstractZoomButton {
|
|
constructor(navbar) {
|
|
super(navbar, ICONS.zoomIn, 0 /* IN */);
|
|
}
|
|
};
|
|
ZoomInButton.id = "zoomIn";
|
|
|
|
// src/buttons/ZoomOutButton.ts
|
|
var ZoomOutButton = class extends AbstractZoomButton {
|
|
constructor(navbar) {
|
|
super(navbar, ICONS.zoomOut, 1 /* OUT */);
|
|
}
|
|
};
|
|
ZoomOutButton.id = "zoomOut";
|
|
|
|
// src/buttons/ZoomRangeButton.ts
|
|
var ZoomRangeButton = class extends AbstractButton {
|
|
constructor(navbar) {
|
|
super(navbar, {
|
|
className: "psv-zoom-range",
|
|
hoverScale: false,
|
|
collapsable: false,
|
|
tabbable: false
|
|
});
|
|
this.zoomRange = document.createElement("div");
|
|
this.zoomRange.className = "psv-zoom-range-line";
|
|
this.container.appendChild(this.zoomRange);
|
|
this.zoomValue = document.createElement("div");
|
|
this.zoomValue.className = "psv-zoom-range-handle";
|
|
this.zoomRange.appendChild(this.zoomValue);
|
|
this.slider = new Slider(this.container, "HORIZONTAL" /* HORIZONTAL */, (data) => this.__onSliderUpdate(data));
|
|
this.mediaMinWidth = parseInt(getStyleProperty(this.container, "max-width"), 10);
|
|
this.viewer.addEventListener(ZoomUpdatedEvent.type, this);
|
|
if (this.viewer.state.ready) {
|
|
this.__moveZoomValue(this.viewer.getZoomLevel());
|
|
} else {
|
|
this.viewer.addEventListener(ReadyEvent.type, this);
|
|
}
|
|
}
|
|
destroy() {
|
|
this.slider.destroy();
|
|
this.viewer.removeEventListener(ZoomUpdatedEvent.type, this);
|
|
this.viewer.removeEventListener(ReadyEvent.type, this);
|
|
super.destroy();
|
|
}
|
|
handleEvent(e) {
|
|
if (e instanceof ZoomUpdatedEvent) {
|
|
this.__moveZoomValue(e.zoomLevel);
|
|
} else if (e instanceof ReadyEvent) {
|
|
this.__moveZoomValue(this.viewer.getZoomLevel());
|
|
}
|
|
}
|
|
onClick() {
|
|
}
|
|
isSupported() {
|
|
return invertResolvableBoolean(SYSTEM.isTouchEnabled);
|
|
}
|
|
autoSize() {
|
|
if (this.state.supported) {
|
|
if (this.viewer.state.size.width <= this.mediaMinWidth && this.state.visible) {
|
|
this.hide(false);
|
|
} else if (this.viewer.state.size.width > this.mediaMinWidth && !this.state.visible) {
|
|
this.show(false);
|
|
}
|
|
}
|
|
}
|
|
__moveZoomValue(level) {
|
|
this.zoomValue.style.left = level / 100 * this.zoomRange.offsetWidth - this.zoomValue.offsetWidth / 2 + "px";
|
|
}
|
|
__onSliderUpdate(data) {
|
|
if (data.mousedown) {
|
|
this.viewer.zoom(data.value * 100);
|
|
}
|
|
}
|
|
};
|
|
ZoomRangeButton.id = "zoomRange";
|
|
ZoomRangeButton.groupId = "zoom";
|
|
|
|
// src/data/config.ts
|
|
var import_three4 = require_three();
|
|
|
|
// src/plugins/AbstractPlugin.ts
|
|
var AbstractPlugin = class extends TypedEventTarget {
|
|
constructor(viewer) {
|
|
super();
|
|
this.viewer = viewer;
|
|
}
|
|
/**
|
|
* Initializes the plugin
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
init() {
|
|
}
|
|
/**
|
|
* Destroys the plugin
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
destroy() {
|
|
}
|
|
};
|
|
var AbstractConfigurablePlugin = class extends AbstractPlugin {
|
|
constructor(viewer, config) {
|
|
super(viewer);
|
|
this.config = this.constructor.configParser(config);
|
|
}
|
|
/**
|
|
* Update options
|
|
*/
|
|
setOption(option, value) {
|
|
this.setOptions({ [option]: value });
|
|
}
|
|
/**
|
|
* Update options
|
|
*/
|
|
setOptions(options) {
|
|
const rawConfig = {
|
|
...this.config,
|
|
...options
|
|
};
|
|
const ctor = this.constructor;
|
|
const parser = ctor.configParser;
|
|
const readonly = ctor.readonlyOptions;
|
|
const id = ctor.id;
|
|
for (let [key, value] of Object.entries(options)) {
|
|
if (!(key in parser.defaults)) {
|
|
logWarn(`${id}: Unknown option "${key}"`);
|
|
continue;
|
|
}
|
|
if (readonly.includes(key)) {
|
|
logWarn(`${id}: Option "${key}" cannot be updated`);
|
|
continue;
|
|
}
|
|
if (key in parser.parsers) {
|
|
value = parser.parsers[key](value, {
|
|
rawConfig,
|
|
defValue: parser.defaults[key]
|
|
});
|
|
}
|
|
this.config[key] = value;
|
|
}
|
|
}
|
|
};
|
|
AbstractConfigurablePlugin.readonlyOptions = [];
|
|
function pluginInterop(plugin) {
|
|
if (plugin) {
|
|
for (const [, p] of [["_", plugin], ...Object.entries(plugin)]) {
|
|
if (p.prototype instanceof AbstractPlugin) {
|
|
checkVersion(p.id, p.VERSION, "5.7.3");
|
|
return p;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// src/data/config.ts
|
|
var DEFAULTS = {
|
|
panorama: null,
|
|
container: null,
|
|
adapter: [EquirectangularAdapter, null],
|
|
plugins: [],
|
|
caption: null,
|
|
description: null,
|
|
downloadUrl: null,
|
|
downloadName: null,
|
|
loadingImg: null,
|
|
loadingTxt: "",
|
|
// empty string => `lang.loading`
|
|
size: null,
|
|
fisheye: 0,
|
|
minFov: 30,
|
|
maxFov: 90,
|
|
defaultZoomLvl: 50,
|
|
defaultYaw: 0,
|
|
defaultPitch: 0,
|
|
sphereCorrection: null,
|
|
moveSpeed: 1,
|
|
zoomSpeed: 1,
|
|
moveInertia: true,
|
|
mousewheel: true,
|
|
mousemove: true,
|
|
mousewheelCtrlKey: false,
|
|
touchmoveTwoFingers: false,
|
|
panoData: null,
|
|
requestHeaders: null,
|
|
rendererParameters: { alpha: true, antialias: true },
|
|
withCredentials: false,
|
|
// prettier-ignore
|
|
navbar: [
|
|
"zoom",
|
|
"move",
|
|
"download",
|
|
"description",
|
|
"caption",
|
|
"fullscreen"
|
|
],
|
|
lang: {
|
|
zoom: "Zoom",
|
|
zoomOut: "Zoom out",
|
|
zoomIn: "Zoom in",
|
|
moveUp: "Move up",
|
|
moveDown: "Move down",
|
|
moveLeft: "Move left",
|
|
moveRight: "Move right",
|
|
description: "Description",
|
|
download: "Download",
|
|
fullscreen: "Fullscreen",
|
|
loading: "Loading...",
|
|
menu: "Menu",
|
|
close: "Close",
|
|
twoFingers: "Use two fingers to navigate",
|
|
ctrlZoom: "Use ctrl + scroll to zoom the image",
|
|
loadError: "The panorama cannot be loaded",
|
|
webglError: "Your browser does not seem to support WebGL"
|
|
},
|
|
keyboard: "fullscreen",
|
|
keyboardActions: {
|
|
[KEY_CODES.ArrowUp]: "ROTATE_UP" /* ROTATE_UP */,
|
|
[KEY_CODES.ArrowDown]: "ROTATE_DOWN" /* ROTATE_DOWN */,
|
|
[KEY_CODES.ArrowRight]: "ROTATE_RIGHT" /* ROTATE_RIGHT */,
|
|
[KEY_CODES.ArrowLeft]: "ROTATE_LEFT" /* ROTATE_LEFT */,
|
|
[KEY_CODES.PageUp]: "ZOOM_IN" /* ZOOM_IN */,
|
|
[KEY_CODES.PageDown]: "ZOOM_OUT" /* ZOOM_OUT */,
|
|
[KEY_CODES.Plus]: "ZOOM_IN" /* ZOOM_IN */,
|
|
[KEY_CODES.Minus]: "ZOOM_OUT" /* ZOOM_OUT */
|
|
}
|
|
};
|
|
var READONLY_OPTIONS = {
|
|
panorama: "Use setPanorama method to change the panorama",
|
|
panoData: "Use setPanorama method to change the panorama",
|
|
container: "Cannot change viewer container",
|
|
adapter: "Cannot change adapter",
|
|
plugins: "Cannot change plugins"
|
|
};
|
|
var CONFIG_PARSERS = {
|
|
container: (container) => {
|
|
if (!container) {
|
|
throw new PSVError("No value given for container.");
|
|
}
|
|
return container;
|
|
},
|
|
adapter: (adapter, { defValue }) => {
|
|
if (!adapter) {
|
|
adapter = defValue;
|
|
} else if (Array.isArray(adapter)) {
|
|
adapter = [adapterInterop(adapter[0]), adapter[1]];
|
|
} else {
|
|
adapter = [adapterInterop(adapter), null];
|
|
}
|
|
if (!adapter[0]) {
|
|
throw new PSVError("An undefined value was given for adapter.");
|
|
}
|
|
if (!adapter[0].id) {
|
|
throw new PSVError(`Adapter has no id.`);
|
|
}
|
|
return adapter;
|
|
},
|
|
defaultYaw: (defaultYaw) => {
|
|
return parseAngle(defaultYaw);
|
|
},
|
|
defaultPitch: (defaultPitch) => {
|
|
return parseAngle(defaultPitch, true);
|
|
},
|
|
defaultZoomLvl: (defaultZoomLvl) => {
|
|
return import_three4.MathUtils.clamp(defaultZoomLvl, 0, 100);
|
|
},
|
|
minFov: (minFov, { rawConfig }) => {
|
|
if (rawConfig.maxFov < minFov) {
|
|
logWarn("maxFov cannot be lower than minFov");
|
|
minFov = rawConfig.maxFov;
|
|
}
|
|
return import_three4.MathUtils.clamp(minFov, 1, 179);
|
|
},
|
|
maxFov: (maxFov, { rawConfig }) => {
|
|
if (maxFov < rawConfig.minFov) {
|
|
maxFov = rawConfig.minFov;
|
|
}
|
|
return import_three4.MathUtils.clamp(maxFov, 1, 179);
|
|
},
|
|
lang: (lang) => {
|
|
return {
|
|
...DEFAULTS.lang,
|
|
...lang
|
|
};
|
|
},
|
|
keyboard: (keyboard) => {
|
|
if (!keyboard) {
|
|
return false;
|
|
}
|
|
if (typeof keyboard === "object") {
|
|
logWarn(`Use keyboardActions to configure the keyboard actions, keyboard option must be either true, false, 'fullscreen' or 'always'`);
|
|
return "fullscreen";
|
|
}
|
|
return keyboard === "always" ? "always" : "fullscreen";
|
|
},
|
|
keyboardActions: (keyboardActions, { rawConfig }) => {
|
|
if (rawConfig.keyboard && typeof rawConfig.keyboard === "object") {
|
|
return rawConfig.keyboard;
|
|
}
|
|
return keyboardActions;
|
|
},
|
|
fisheye: (fisheye) => {
|
|
if (fisheye === true) {
|
|
return 1;
|
|
} else if (fisheye === false) {
|
|
return 0;
|
|
}
|
|
return fisheye;
|
|
},
|
|
requestHeaders: (requestHeaders) => {
|
|
if (requestHeaders && typeof requestHeaders === "object") {
|
|
return () => requestHeaders;
|
|
}
|
|
if (typeof requestHeaders === "function") {
|
|
return requestHeaders;
|
|
}
|
|
return null;
|
|
},
|
|
rendererParameters: (rendererParameters, { defValue }) => ({
|
|
...rendererParameters,
|
|
...defValue
|
|
}),
|
|
plugins: (plugins) => {
|
|
return plugins.map((plugin, i) => {
|
|
if (Array.isArray(plugin)) {
|
|
plugin = [pluginInterop(plugin[0]), plugin[1]];
|
|
} else {
|
|
plugin = [pluginInterop(plugin), null];
|
|
}
|
|
if (!plugin[0]) {
|
|
throw new PSVError(`An undefined value was given for plugin ${i}.`);
|
|
}
|
|
if (!plugin[0].id) {
|
|
throw new PSVError(`Plugin ${i} has no id.`);
|
|
}
|
|
return plugin;
|
|
});
|
|
},
|
|
navbar: (navbar) => {
|
|
if (navbar === false) {
|
|
return null;
|
|
}
|
|
if (navbar === true) {
|
|
return clone(DEFAULTS.navbar);
|
|
}
|
|
if (typeof navbar === "string") {
|
|
return navbar.split(/[ ,]/);
|
|
}
|
|
return navbar;
|
|
}
|
|
};
|
|
var getViewerConfig = getConfigParser(DEFAULTS, CONFIG_PARSERS);
|
|
|
|
// src/components/NavbarCaption.ts
|
|
var NavbarCaption = class extends AbstractButton {
|
|
constructor(navbar) {
|
|
super(navbar, {
|
|
className: "psv-caption",
|
|
hoverScale: false,
|
|
collapsable: false,
|
|
tabbable: true
|
|
});
|
|
this.contentWidth = 0;
|
|
this.state.width = 0;
|
|
this.contentElt = document.createElement("div");
|
|
this.contentElt.className = "psv-caption-content";
|
|
this.container.appendChild(this.contentElt);
|
|
this.setCaption(this.viewer.config.caption);
|
|
}
|
|
hide() {
|
|
this.contentElt.style.display = "none";
|
|
this.state.visible = false;
|
|
}
|
|
show() {
|
|
this.contentElt.style.display = "";
|
|
this.state.visible = true;
|
|
}
|
|
onClick() {
|
|
}
|
|
/**
|
|
* Changes the caption
|
|
*/
|
|
setCaption(html) {
|
|
this.show();
|
|
this.contentElt.innerHTML = html ?? "";
|
|
if (this.contentElt.innerHTML) {
|
|
this.contentWidth = this.contentElt.offsetWidth;
|
|
} else {
|
|
this.contentWidth = 0;
|
|
}
|
|
this.autoSize();
|
|
}
|
|
/**
|
|
* Toggles content and icon depending on available space
|
|
*/
|
|
autoSize() {
|
|
this.toggle(this.container.offsetWidth >= this.contentWidth);
|
|
this.__refreshButton();
|
|
}
|
|
__refreshButton() {
|
|
this.viewer.navbar.getButton(DescriptionButton.id, false)?.autoSize(true);
|
|
}
|
|
};
|
|
NavbarCaption.id = "caption";
|
|
|
|
// src/components/Navbar.ts
|
|
var AVAILABLE_BUTTONS = {};
|
|
var AVAILABLE_GROUPS = {};
|
|
function registerButton(button, defaultPosition) {
|
|
if (!button.id) {
|
|
throw new PSVError("Button id is required");
|
|
}
|
|
AVAILABLE_BUTTONS[button.id] = button;
|
|
if (button.groupId) {
|
|
(AVAILABLE_GROUPS[button.groupId] = AVAILABLE_GROUPS[button.groupId] || []).push(button);
|
|
}
|
|
if (defaultPosition) {
|
|
const navbar = DEFAULTS.navbar;
|
|
switch (defaultPosition) {
|
|
case "start":
|
|
navbar.unshift(button.id);
|
|
break;
|
|
case "end":
|
|
navbar.push(button.id);
|
|
break;
|
|
default: {
|
|
const [id, pos] = defaultPosition.split(":");
|
|
const idx = navbar.indexOf(id);
|
|
if (!id || !pos || idx === -1) {
|
|
throw new PSVError(`Invalid defaultPosition ${defaultPosition}`);
|
|
}
|
|
navbar.splice(idx + (pos === "right" ? 1 : 0), 0, button.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
[
|
|
ZoomOutButton,
|
|
ZoomRangeButton,
|
|
ZoomInButton,
|
|
DescriptionButton,
|
|
NavbarCaption,
|
|
DownloadButton,
|
|
FullscreenButton,
|
|
MoveLeftButton,
|
|
MoveRightButton,
|
|
MoveUpButton,
|
|
MoveDownButton
|
|
].forEach((btn) => registerButton(btn));
|
|
var Navbar = class extends AbstractComponent {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer) {
|
|
super(viewer, {
|
|
className: `psv-navbar ${CAPTURE_EVENTS_CLASS}`
|
|
});
|
|
/**
|
|
* @internal
|
|
*/
|
|
this.collapsed = [];
|
|
this.state.visible = false;
|
|
}
|
|
/**
|
|
* Shows the navbar
|
|
*/
|
|
show() {
|
|
this.viewer.container.classList.add("psv--has-navbar");
|
|
this.container.classList.add("psv-navbar--open");
|
|
this.state.visible = true;
|
|
}
|
|
/**
|
|
* Hides the navbar
|
|
*/
|
|
hide() {
|
|
this.viewer.container.classList.remove("psv--has-navbar");
|
|
this.container.classList.remove("psv-navbar--open");
|
|
this.state.visible = false;
|
|
}
|
|
/**
|
|
* Change the buttons visible on the navbar
|
|
*/
|
|
setButtons(buttons) {
|
|
this.children.slice().forEach((item) => item.destroy());
|
|
this.children.length = 0;
|
|
if (buttons.indexOf(NavbarCaption.id) !== -1 && buttons.indexOf(DescriptionButton.id) === -1) {
|
|
buttons.splice(buttons.indexOf(NavbarCaption.id), 0, DescriptionButton.id);
|
|
}
|
|
buttons.forEach((button) => {
|
|
if (typeof button === "object") {
|
|
new CustomButton(this, button);
|
|
} else if (AVAILABLE_BUTTONS[button]) {
|
|
new AVAILABLE_BUTTONS[button](this);
|
|
} else if (AVAILABLE_GROUPS[button]) {
|
|
AVAILABLE_GROUPS[button].forEach((buttonCtor) => {
|
|
new buttonCtor(this);
|
|
});
|
|
} else {
|
|
logWarn(`Unknown button ${button}`);
|
|
}
|
|
});
|
|
new MenuButton(this);
|
|
this.children.forEach((item) => {
|
|
if (item instanceof AbstractButton) {
|
|
item.checkSupported();
|
|
}
|
|
});
|
|
this.autoSize();
|
|
}
|
|
/**
|
|
* Changes the navbar caption
|
|
*/
|
|
setCaption(html) {
|
|
this.children.some((item) => {
|
|
if (item instanceof NavbarCaption) {
|
|
item.setCaption(html);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Returns a button by its identifier
|
|
*/
|
|
getButton(id, warnNotFound = true) {
|
|
const button = this.children.find((item) => {
|
|
return item instanceof AbstractButton && item.id === id;
|
|
});
|
|
if (!button && warnNotFound) {
|
|
logWarn(`button "${id}" not found in the navbar`);
|
|
}
|
|
return button;
|
|
}
|
|
/**
|
|
* Automatically collapses buttons
|
|
* @internal
|
|
*/
|
|
autoSize() {
|
|
this.children.forEach((child) => {
|
|
if (child instanceof AbstractButton) {
|
|
child.autoSize();
|
|
}
|
|
});
|
|
const availableWidth = this.container.offsetWidth;
|
|
let totalWidth = 0;
|
|
const collapsableButtons = [];
|
|
this.children.forEach((item) => {
|
|
if (item.isVisible() && item instanceof AbstractButton) {
|
|
totalWidth += item.width;
|
|
if (item.collapsable) {
|
|
collapsableButtons.push(item);
|
|
}
|
|
}
|
|
});
|
|
if (totalWidth === 0) {
|
|
return;
|
|
}
|
|
if (availableWidth < totalWidth && collapsableButtons.length > 0) {
|
|
collapsableButtons.forEach((item) => item.collapse());
|
|
this.collapsed = collapsableButtons;
|
|
this.getButton(MenuButton.id).show(false);
|
|
} else if (availableWidth >= totalWidth && this.collapsed.length > 0) {
|
|
this.collapsed.forEach((item) => item.uncollapse());
|
|
this.collapsed = [];
|
|
this.getButton(MenuButton.id).hide(false);
|
|
}
|
|
this.getButton(NavbarCaption.id, false)?.autoSize();
|
|
}
|
|
};
|
|
|
|
// src/data/cache.ts
|
|
var import_three5 = require_three();
|
|
import_three5.Cache.enabled = false;
|
|
var Cache = {
|
|
enabled: true,
|
|
maxItems: 10,
|
|
ttl: 10 * 60,
|
|
items: {},
|
|
purgeInterval: null,
|
|
init() {
|
|
if (import_three5.Cache.enabled) {
|
|
logWarn("ThreeJS cache should be disabled");
|
|
import_three5.Cache.enabled = false;
|
|
}
|
|
if (!this.purgeInterval && this.enabled) {
|
|
this.purgeInterval = setInterval(() => this.purge(), 60 * 1e3);
|
|
}
|
|
},
|
|
add(url, key, data) {
|
|
if (this.enabled && key) {
|
|
this.items[key] = this.items[key] ?? { files: {}, lastAccess: null };
|
|
this.items[key].files[url] = data;
|
|
this.items[key].lastAccess = Date.now();
|
|
}
|
|
},
|
|
get(url, key) {
|
|
if (this.enabled && key && this.items[key]) {
|
|
this.items[key].lastAccess = Date.now();
|
|
return this.items[key].files[url];
|
|
}
|
|
},
|
|
remove(url, key) {
|
|
if (this.enabled && key && this.items[key]) {
|
|
delete this.items[key].files[url];
|
|
if (Object.keys(this.items[key].files).length === 0) {
|
|
delete this.items[key];
|
|
}
|
|
}
|
|
},
|
|
purge() {
|
|
Object.entries(this.items).sort(([, a], [, b]) => {
|
|
return b.lastAccess - a.lastAccess;
|
|
}).forEach(([key, { lastAccess }], index) => {
|
|
if (index > 0 && (Date.now() - lastAccess >= this.ttl * 1e3 || index >= this.maxItems)) {
|
|
delete this.items[key];
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/components/Loader.ts
|
|
var Loader = class extends AbstractComponent {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer) {
|
|
super(viewer, { className: "psv-loader-container" });
|
|
this.loader = document.createElement("div");
|
|
this.loader.className = "psv-loader";
|
|
this.container.appendChild(this.loader);
|
|
this.size = this.loader.offsetWidth;
|
|
this.canvas = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
this.canvas.setAttribute("class", "psv-loader-canvas");
|
|
this.canvas.setAttribute("viewBox", `0 0 ${this.size} ${this.size}`);
|
|
this.loader.appendChild(this.canvas);
|
|
this.textColor = getStyleProperty(this.loader, "color");
|
|
this.color = getStyleProperty(this.canvas, "color");
|
|
this.border = parseInt(getStyleProperty(this.loader, "--psv-loader-border"), 10);
|
|
this.thickness = parseInt(getStyleProperty(this.loader, "--psv-loader-tickness"), 10);
|
|
this.viewer.addEventListener(ConfigChangedEvent.type, this);
|
|
this.__updateContent();
|
|
this.hide();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
destroy() {
|
|
this.viewer.removeEventListener(ConfigChangedEvent.type, this);
|
|
super.destroy();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
handleEvent(e) {
|
|
if (e instanceof ConfigChangedEvent) {
|
|
e.containsOptions("loadingImg", "loadingTxt") && this.__updateContent();
|
|
}
|
|
}
|
|
/**
|
|
* Sets the loader progression
|
|
*/
|
|
setProgress(value) {
|
|
const angle2 = Math.min(value, 99.999) / 100 * Math.PI * 2;
|
|
const halfSize = this.size / 2;
|
|
const startX = halfSize;
|
|
const startY = this.thickness / 2 + this.border;
|
|
const radius = (this.size - this.thickness) / 2 - this.border;
|
|
const endX = Math.sin(angle2) * radius + halfSize;
|
|
const endY = -Math.cos(angle2) * radius + halfSize;
|
|
const largeArc = value > 50 ? "1" : "0";
|
|
this.canvas.innerHTML = `
|
|
<circle cx="${halfSize}" cy="${halfSize}" r="${halfSize}" fill="${this.color}"/>
|
|
<path d="M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArc} 1 ${endX} ${endY}"
|
|
fill="none" stroke="${this.textColor}" stroke-width="${this.thickness}" stroke-linecap="round"/>
|
|
`;
|
|
this.viewer.dispatchEvent(new LoadProgressEvent(Math.round(value)));
|
|
}
|
|
__updateContent() {
|
|
const current = this.loader.querySelector(".psv-loader-image, .psv-loader-text");
|
|
if (current) {
|
|
this.loader.removeChild(current);
|
|
}
|
|
let inner;
|
|
if (this.viewer.config.loadingImg) {
|
|
inner = document.createElement("img");
|
|
inner.className = "psv-loader-image";
|
|
inner.src = this.viewer.config.loadingImg;
|
|
} else if (this.viewer.config.loadingTxt !== null) {
|
|
inner = document.createElement("div");
|
|
inner.className = "psv-loader-text";
|
|
inner.innerHTML = this.viewer.config.loadingTxt || this.viewer.config.lang.loading;
|
|
}
|
|
if (inner) {
|
|
const size = Math.round(Math.sqrt(2 * Math.pow(this.size / 2 - this.thickness / 2 - this.border, 2)));
|
|
inner.style.maxWidth = size + "px";
|
|
inner.style.maxHeight = size + "px";
|
|
this.loader.appendChild(inner);
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/components/Notification.ts
|
|
var Notification = class extends AbstractComponent {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer) {
|
|
super(viewer, {
|
|
className: "psv-notification"
|
|
});
|
|
/**
|
|
* @internal
|
|
*/
|
|
this.state = {
|
|
visible: false,
|
|
contentId: null,
|
|
timeout: null
|
|
};
|
|
this.content = document.createElement("div");
|
|
this.content.className = "psv-notification-content";
|
|
this.container.appendChild(this.content);
|
|
this.content.addEventListener("click", () => this.hide());
|
|
}
|
|
/**
|
|
* Checks if the notification is visible
|
|
*/
|
|
isVisible(id) {
|
|
return this.state.visible && (!id || !this.state.contentId || this.state.contentId === id);
|
|
}
|
|
/**
|
|
* @throws {@link PSVError} always
|
|
* @internal
|
|
*/
|
|
toggle() {
|
|
throw new PSVError("Notification cannot be toggled");
|
|
}
|
|
/**
|
|
* Displays a notification on the viewer
|
|
*
|
|
* @example
|
|
* viewer.showNotification({ content: 'Hello world', timeout: 5000 })
|
|
* @example
|
|
* viewer.showNotification('Hello world')
|
|
*/
|
|
show(config) {
|
|
if (this.state.timeout) {
|
|
clearTimeout(this.state.timeout);
|
|
this.state.timeout = null;
|
|
}
|
|
if (typeof config === "string") {
|
|
config = { content: config };
|
|
}
|
|
this.state.contentId = config.id || null;
|
|
this.content.innerHTML = config.content;
|
|
this.container.classList.add("psv-notification--visible");
|
|
this.state.visible = true;
|
|
this.viewer.dispatchEvent(new ShowNotificationEvent(config.id));
|
|
if (config.timeout) {
|
|
this.state.timeout = setTimeout(() => this.hide(this.state.contentId), config.timeout);
|
|
}
|
|
}
|
|
/**
|
|
* Hides the notification
|
|
*/
|
|
hide(id) {
|
|
if (this.isVisible(id)) {
|
|
const contentId = this.state.contentId;
|
|
this.container.classList.remove("psv-notification--visible");
|
|
this.state.visible = false;
|
|
this.state.contentId = null;
|
|
this.viewer.dispatchEvent(new HideNotificationEvent(contentId));
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/components/Overlay.ts
|
|
var Overlay = class extends AbstractComponent {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer) {
|
|
super(viewer, {
|
|
className: `psv-overlay ${CAPTURE_EVENTS_CLASS}`
|
|
});
|
|
/**
|
|
* @internal
|
|
*/
|
|
this.state = {
|
|
visible: false,
|
|
contentId: null,
|
|
dissmisable: true
|
|
};
|
|
this.image = document.createElement("div");
|
|
this.image.className = "psv-overlay-image";
|
|
this.container.appendChild(this.image);
|
|
this.title = document.createElement("div");
|
|
this.title.className = "psv-overlay-title";
|
|
this.container.appendChild(this.title);
|
|
this.text = document.createElement("div");
|
|
this.text.className = "psv-overlay-text";
|
|
this.container.appendChild(this.text);
|
|
this.container.addEventListener("click", this);
|
|
this.viewer.addEventListener(KeypressEvent.type, this);
|
|
super.hide();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
destroy() {
|
|
this.viewer.removeEventListener(KeypressEvent.type, this);
|
|
super.destroy();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
handleEvent(e) {
|
|
if (e.type === "click") {
|
|
if (this.isVisible() && this.state.dissmisable) {
|
|
this.hide();
|
|
e.stopPropagation();
|
|
}
|
|
} else if (e instanceof KeypressEvent) {
|
|
if (this.isVisible() && this.state.dissmisable && e.key === KEY_CODES.Escape) {
|
|
this.hide();
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Checks if the overlay is visible
|
|
*/
|
|
isVisible(id) {
|
|
return this.state.visible && (!id || !this.state.contentId || this.state.contentId === id);
|
|
}
|
|
/**
|
|
* @throws {@link PSVError} always
|
|
* @internal
|
|
*/
|
|
toggle() {
|
|
throw new PSVError("Overlay cannot be toggled");
|
|
}
|
|
/**
|
|
* Displays an overlay on the viewer
|
|
*/
|
|
show(config) {
|
|
if (typeof config === "string") {
|
|
config = { title: config };
|
|
}
|
|
this.state.contentId = config.id || null;
|
|
this.state.dissmisable = config.dissmisable !== false;
|
|
this.image.innerHTML = config.image || "";
|
|
this.title.innerHTML = config.title || "";
|
|
this.text.innerHTML = config.text || "";
|
|
super.show();
|
|
this.viewer.dispatchEvent(new ShowOverlayEvent(config.id));
|
|
}
|
|
/**
|
|
* Hides the overlay
|
|
*/
|
|
hide(id) {
|
|
if (this.isVisible(id)) {
|
|
const contentId = this.state.contentId;
|
|
super.hide();
|
|
this.state.contentId = null;
|
|
this.viewer.dispatchEvent(new HideOverlayEvent(contentId));
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/components/Panel.ts
|
|
var PANEL_MIN_WIDTH = 200;
|
|
var PANEL_CLASS_NO_INTERACTION = "psv-panel-content--no-interaction";
|
|
var Panel = class extends AbstractComponent {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer) {
|
|
super(viewer, {
|
|
className: `psv-panel ${CAPTURE_EVENTS_CLASS}`
|
|
});
|
|
/**
|
|
* @internal
|
|
*/
|
|
this.state = {
|
|
visible: false,
|
|
contentId: null,
|
|
mouseX: 0,
|
|
mouseY: 0,
|
|
mousedown: false,
|
|
clickHandler: null,
|
|
keyHandler: null,
|
|
width: {}
|
|
};
|
|
const resizer = document.createElement("div");
|
|
resizer.className = "psv-panel-resizer";
|
|
this.container.appendChild(resizer);
|
|
const closeBtn = document.createElement("div");
|
|
closeBtn.className = "psv-panel-close-button";
|
|
closeBtn.innerHTML = ICONS.close;
|
|
closeBtn.title = viewer.config.lang.close;
|
|
this.container.appendChild(closeBtn);
|
|
this.content = document.createElement("div");
|
|
this.content.className = "psv-panel-content";
|
|
this.container.appendChild(this.content);
|
|
this.container.addEventListener("wheel", (e) => e.stopPropagation());
|
|
closeBtn.addEventListener("click", () => this.hide());
|
|
resizer.addEventListener("mousedown", this);
|
|
resizer.addEventListener("touchstart", this);
|
|
this.viewer.container.addEventListener("mouseup", this);
|
|
this.viewer.container.addEventListener("touchend", this);
|
|
this.viewer.container.addEventListener("mousemove", this);
|
|
this.viewer.container.addEventListener("touchmove", this);
|
|
this.viewer.addEventListener(KeypressEvent.type, this);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
destroy() {
|
|
this.viewer.removeEventListener(KeypressEvent.type, this);
|
|
this.viewer.container.removeEventListener("mousemove", this);
|
|
this.viewer.container.removeEventListener("touchmove", this);
|
|
this.viewer.container.removeEventListener("mouseup", this);
|
|
this.viewer.container.removeEventListener("touchend", this);
|
|
super.destroy();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
handleEvent(e) {
|
|
switch (e.type) {
|
|
case "mousedown":
|
|
this.__onMouseDown(e);
|
|
break;
|
|
case "touchstart":
|
|
this.__onTouchStart(e);
|
|
break;
|
|
case "mousemove":
|
|
this.__onMouseMove(e);
|
|
break;
|
|
case "touchmove":
|
|
this.__onTouchMove(e);
|
|
break;
|
|
case "mouseup":
|
|
this.__onMouseUp(e);
|
|
break;
|
|
case "touchend":
|
|
this.__onTouchEnd(e);
|
|
break;
|
|
case KeypressEvent.type:
|
|
this.__onKeyPress(e);
|
|
break;
|
|
}
|
|
}
|
|
/**
|
|
* Checks if the panel is visible
|
|
*/
|
|
isVisible(id) {
|
|
return this.state.visible && (!id || !this.state.contentId || this.state.contentId === id);
|
|
}
|
|
/**
|
|
* @throws {@link PSVError} always
|
|
* @internal
|
|
*/
|
|
toggle() {
|
|
throw new PSVError("Panel cannot be toggled");
|
|
}
|
|
/**
|
|
* Shows the panel
|
|
*/
|
|
show(config) {
|
|
if (typeof config === "string") {
|
|
config = { content: config };
|
|
}
|
|
const wasVisible = this.isVisible(config.id);
|
|
this.state.contentId = config.id || null;
|
|
this.state.visible = true;
|
|
if (this.state.clickHandler) {
|
|
this.content.removeEventListener("click", this.state.clickHandler);
|
|
this.content.removeEventListener("keydown", this.state.keyHandler);
|
|
this.state.clickHandler = null;
|
|
this.state.keyHandler = null;
|
|
}
|
|
if (config.id && this.state.width[config.id]) {
|
|
this.container.style.width = this.state.width[config.id];
|
|
} else if (config.width) {
|
|
this.container.style.width = config.width;
|
|
} else {
|
|
this.container.style.width = null;
|
|
}
|
|
this.content.innerHTML = config.content;
|
|
this.content.scrollTop = 0;
|
|
this.container.classList.add("psv-panel--open");
|
|
toggleClass(this.content, "psv-panel-content--no-margin", config.noMargin === true);
|
|
if (config.clickHandler) {
|
|
this.state.clickHandler = (e) => {
|
|
config.clickHandler(e.target);
|
|
};
|
|
this.state.keyHandler = (e) => {
|
|
if (e.key === KEY_CODES.Enter) {
|
|
config.clickHandler(e.target);
|
|
}
|
|
};
|
|
this.content.addEventListener("click", this.state.clickHandler);
|
|
this.content.addEventListener("keydown", this.state.keyHandler);
|
|
if (!wasVisible) {
|
|
setTimeout(() => {
|
|
this.content.querySelector("a,button,[tabindex]")?.focus();
|
|
}, 300);
|
|
}
|
|
}
|
|
this.viewer.dispatchEvent(new ShowPanelEvent(config.id));
|
|
}
|
|
/**
|
|
* Hides the panel
|
|
*/
|
|
hide(id) {
|
|
if (this.isVisible(id)) {
|
|
const contentId = this.state.contentId;
|
|
this.state.visible = false;
|
|
this.state.contentId = null;
|
|
this.content.innerHTML = null;
|
|
this.container.classList.remove("psv-panel--open");
|
|
if (this.state.clickHandler) {
|
|
this.content.removeEventListener("click", this.state.clickHandler);
|
|
this.state.clickHandler = null;
|
|
}
|
|
this.viewer.dispatchEvent(new HidePanelEvent(contentId));
|
|
}
|
|
}
|
|
__onMouseDown(evt) {
|
|
evt.stopPropagation();
|
|
this.__startResize(evt.clientX, evt.clientY);
|
|
}
|
|
__onTouchStart(evt) {
|
|
evt.stopPropagation();
|
|
if (evt.touches.length === 1) {
|
|
const touch = evt.touches[0];
|
|
this.__startResize(touch.clientX, touch.clientY);
|
|
}
|
|
}
|
|
__onMouseUp(evt) {
|
|
if (this.state.mousedown) {
|
|
evt.stopPropagation();
|
|
this.state.mousedown = false;
|
|
this.content.classList.remove(PANEL_CLASS_NO_INTERACTION);
|
|
}
|
|
}
|
|
__onTouchEnd(evt) {
|
|
if (this.state.mousedown) {
|
|
evt.stopPropagation();
|
|
if (evt.touches.length === 0) {
|
|
this.state.mousedown = false;
|
|
this.content.classList.remove(PANEL_CLASS_NO_INTERACTION);
|
|
}
|
|
}
|
|
}
|
|
__onMouseMove(evt) {
|
|
if (this.state.mousedown) {
|
|
evt.stopPropagation();
|
|
this.__resize(evt.clientX, evt.clientY);
|
|
}
|
|
}
|
|
__onTouchMove(evt) {
|
|
if (this.state.mousedown) {
|
|
const touch = evt.touches[0];
|
|
this.__resize(touch.clientX, touch.clientY);
|
|
}
|
|
}
|
|
__onKeyPress(evt) {
|
|
if (this.isVisible() && evt.key === KEY_CODES.Escape) {
|
|
this.hide();
|
|
evt.preventDefault();
|
|
}
|
|
}
|
|
__startResize(clientX, clientY) {
|
|
this.state.mouseX = clientX;
|
|
this.state.mouseY = clientY;
|
|
this.state.mousedown = true;
|
|
this.content.classList.add(PANEL_CLASS_NO_INTERACTION);
|
|
}
|
|
__resize(clientX, clientY) {
|
|
const x = clientX;
|
|
const y = clientY;
|
|
const width = Math.max(PANEL_MIN_WIDTH, this.container.offsetWidth - (x - this.state.mouseX)) + "px";
|
|
if (this.state.contentId) {
|
|
this.state.width[this.state.contentId] = width;
|
|
}
|
|
this.container.style.width = width;
|
|
this.state.mouseX = x;
|
|
this.state.mouseY = y;
|
|
}
|
|
};
|
|
|
|
// src/components/Tooltip.ts
|
|
var Tooltip = class extends AbstractComponent {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer, config) {
|
|
super(viewer, {
|
|
className: "psv-tooltip"
|
|
});
|
|
/**
|
|
* @internal
|
|
*/
|
|
this.state = {
|
|
visible: true,
|
|
arrow: 0,
|
|
border: 0,
|
|
state: 0 /* NONE */,
|
|
width: 0,
|
|
height: 0,
|
|
pos: "",
|
|
config: null,
|
|
data: null
|
|
};
|
|
this.content = document.createElement("div");
|
|
this.content.className = "psv-tooltip-content";
|
|
this.container.appendChild(this.content);
|
|
this.arrow = document.createElement("div");
|
|
this.arrow.className = "psv-tooltip-arrow";
|
|
this.container.appendChild(this.arrow);
|
|
this.container.addEventListener("transitionend", this);
|
|
this.container.addEventListener("touchdown", (e) => e.stopPropagation());
|
|
this.container.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
this.container.style.top = "-1000px";
|
|
this.container.style.left = "-1000px";
|
|
this.show(config);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
handleEvent(e) {
|
|
if (e.type === "transitionend") {
|
|
this.__onTransitionEnd(e);
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
destroy() {
|
|
delete this.state.data;
|
|
super.destroy();
|
|
}
|
|
/**
|
|
* @throws {@link PSVError} always
|
|
* @internal
|
|
*/
|
|
toggle() {
|
|
throw new PSVError("Tooltip cannot be toggled");
|
|
}
|
|
/**
|
|
* Displays the tooltip on the viewer
|
|
* @internal
|
|
*/
|
|
show(config) {
|
|
if (this.state.state !== 0 /* NONE */) {
|
|
throw new PSVError("Initialized tooltip cannot be re-initialized");
|
|
}
|
|
if (config.className) {
|
|
addClasses(this.container, config.className);
|
|
}
|
|
if (config.style) {
|
|
Object.assign(this.container.style, config.style);
|
|
}
|
|
this.state.state = 3 /* READY */;
|
|
this.update(config.content, config);
|
|
this.state.data = config.data;
|
|
this.state.state = 1 /* SHOWING */;
|
|
this.viewer.dispatchEvent(new ShowTooltipEvent(this, this.state.data));
|
|
this.__waitImages();
|
|
}
|
|
/**
|
|
* Updates the content of the tooltip, optionally with a new position
|
|
* @throws {@link PSVError} if the configuration is invalid
|
|
*/
|
|
update(content, config) {
|
|
this.content.innerHTML = content;
|
|
const rect = this.container.getBoundingClientRect();
|
|
this.state.width = rect.right - rect.left;
|
|
this.state.height = rect.bottom - rect.top;
|
|
this.state.arrow = parseInt(getStyleProperty(this.arrow, "border-top-width"), 10);
|
|
this.state.border = parseInt(getStyleProperty(this.container, "border-top-left-radius"), 10);
|
|
this.move(config ?? this.state.config);
|
|
this.__waitImages();
|
|
}
|
|
/**
|
|
* Moves the tooltip to a new position
|
|
* @throws {@link PSVError} if the configuration is invalid
|
|
*/
|
|
move(config) {
|
|
if (this.state.state !== 1 /* SHOWING */ && this.state.state !== 3 /* READY */) {
|
|
throw new PSVError("Uninitialized tooltip cannot be moved");
|
|
}
|
|
config.box = config.box ?? this.state.config?.box ?? { width: 0, height: 0 };
|
|
this.state.config = config;
|
|
const t = this.container;
|
|
const a = this.arrow;
|
|
const style = {
|
|
posClass: cleanCssPosition(config.position, { allowCenter: false, cssOrder: false }) || ["top", "center"],
|
|
width: this.state.width,
|
|
height: this.state.height,
|
|
top: 0,
|
|
left: 0,
|
|
arrowTop: 0,
|
|
arrowLeft: 0
|
|
};
|
|
this.__computeTooltipPosition(style, config);
|
|
let swapY = null;
|
|
let swapX = null;
|
|
if (style.top < 0) {
|
|
swapY = "bottom";
|
|
} else if (style.top + style.height > this.viewer.state.size.height) {
|
|
swapY = "top";
|
|
}
|
|
if (style.left < 0) {
|
|
swapX = "right";
|
|
} else if (style.left + style.width > this.viewer.state.size.width) {
|
|
swapX = "left";
|
|
}
|
|
if (swapX || swapY) {
|
|
const ordered = cssPositionIsOrdered(style.posClass);
|
|
if (swapY) {
|
|
style.posClass[ordered ? 0 : 1] = swapY;
|
|
}
|
|
if (swapX) {
|
|
style.posClass[ordered ? 1 : 0] = swapX;
|
|
}
|
|
this.__computeTooltipPosition(style, config);
|
|
}
|
|
t.style.top = style.top + "px";
|
|
t.style.left = style.left + "px";
|
|
a.style.top = style.arrowTop + "px";
|
|
a.style.left = style.arrowLeft + "px";
|
|
const newPos = style.posClass.join("-");
|
|
if (newPos !== this.state.pos) {
|
|
t.classList.remove(`psv-tooltip--${this.state.pos}`);
|
|
this.state.pos = newPos;
|
|
t.classList.add(`psv-tooltip--${this.state.pos}`);
|
|
}
|
|
}
|
|
/**
|
|
* Hides the tooltip
|
|
*/
|
|
hide() {
|
|
this.container.classList.remove("psv-tooltip--visible");
|
|
this.state.state = 2 /* HIDING */;
|
|
this.viewer.dispatchEvent(new HideTooltipEvent(this.state.data));
|
|
}
|
|
/**
|
|
* Finalize transition
|
|
*/
|
|
__onTransitionEnd(e) {
|
|
if (e.propertyName === "transform") {
|
|
switch (this.state.state) {
|
|
case 1 /* SHOWING */:
|
|
this.container.classList.add("psv-tooltip--visible");
|
|
this.state.state = 3 /* READY */;
|
|
break;
|
|
case 2 /* HIDING */:
|
|
this.state.state = 0 /* NONE */;
|
|
this.destroy();
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Computes the position of the tooltip and its arrow
|
|
*/
|
|
__computeTooltipPosition(style, config) {
|
|
const arrow = this.state.arrow;
|
|
const top = config.top;
|
|
const height = style.height;
|
|
const left = config.left;
|
|
const width = style.width;
|
|
const offsetSide = arrow + this.state.border;
|
|
const offsetX = config.box.width / 2 + arrow * 2;
|
|
const offsetY = config.box.height / 2 + arrow * 2;
|
|
switch (style.posClass.join("-")) {
|
|
case "top-left":
|
|
style.top = top - offsetY - height;
|
|
style.left = left + offsetSide - width;
|
|
style.arrowTop = height;
|
|
style.arrowLeft = width - offsetSide - arrow;
|
|
break;
|
|
case "top-center":
|
|
style.top = top - offsetY - height;
|
|
style.left = left - width / 2;
|
|
style.arrowTop = height;
|
|
style.arrowLeft = width / 2 - arrow;
|
|
break;
|
|
case "top-right":
|
|
style.top = top - offsetY - height;
|
|
style.left = left - offsetSide;
|
|
style.arrowTop = height;
|
|
style.arrowLeft = arrow;
|
|
break;
|
|
case "bottom-left":
|
|
style.top = top + offsetY;
|
|
style.left = left + offsetSide - width;
|
|
style.arrowTop = -arrow * 2;
|
|
style.arrowLeft = width - offsetSide - arrow;
|
|
break;
|
|
case "bottom-center":
|
|
style.top = top + offsetY;
|
|
style.left = left - width / 2;
|
|
style.arrowTop = -arrow * 2;
|
|
style.arrowLeft = width / 2 - arrow;
|
|
break;
|
|
case "bottom-right":
|
|
style.top = top + offsetY;
|
|
style.left = left - offsetSide;
|
|
style.arrowTop = -arrow * 2;
|
|
style.arrowLeft = arrow;
|
|
break;
|
|
case "left-top":
|
|
style.top = top + offsetSide - height;
|
|
style.left = left - offsetX - width;
|
|
style.arrowTop = height - offsetSide - arrow;
|
|
style.arrowLeft = width;
|
|
break;
|
|
case "center-left":
|
|
style.top = top - height / 2;
|
|
style.left = left - offsetX - width;
|
|
style.arrowTop = height / 2 - arrow;
|
|
style.arrowLeft = width;
|
|
break;
|
|
case "left-bottom":
|
|
style.top = top - offsetSide;
|
|
style.left = left - offsetX - width;
|
|
style.arrowTop = arrow;
|
|
style.arrowLeft = width;
|
|
break;
|
|
case "right-top":
|
|
style.top = top + offsetSide - height;
|
|
style.left = left + offsetX;
|
|
style.arrowTop = height - offsetSide - arrow;
|
|
style.arrowLeft = -arrow * 2;
|
|
break;
|
|
case "center-right":
|
|
style.top = top - height / 2;
|
|
style.left = left + offsetX;
|
|
style.arrowTop = height / 2 - arrow;
|
|
style.arrowLeft = -arrow * 2;
|
|
break;
|
|
case "right-bottom":
|
|
style.top = top - offsetSide;
|
|
style.left = left + offsetX;
|
|
style.arrowTop = arrow;
|
|
style.arrowLeft = -arrow * 2;
|
|
break;
|
|
}
|
|
}
|
|
/**
|
|
* If the tooltip contains images, recompute its size once they are loaded
|
|
*/
|
|
__waitImages() {
|
|
const images = this.content.querySelectorAll("img");
|
|
if (images.length > 0) {
|
|
const promises = [];
|
|
images.forEach((image) => {
|
|
if (!image.complete) {
|
|
promises.push(
|
|
new Promise((resolve) => {
|
|
image.onload = resolve;
|
|
image.onerror = resolve;
|
|
})
|
|
);
|
|
}
|
|
});
|
|
if (promises.length) {
|
|
Promise.all(promises).then(() => {
|
|
if (this.state.state === 1 /* SHOWING */ || this.state.state === 3 /* READY */) {
|
|
const rect = this.container.getBoundingClientRect();
|
|
this.state.width = rect.right - rect.left;
|
|
this.state.height = rect.bottom - rect.top;
|
|
this.move(this.state.config);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/icons/error.svg
|
|
var error_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="15 15 70 70"><path fill="currentColor" d="M50,16.2c-18.6,0-33.8,15.1-33.8,33.8S31.4,83.7,50,83.7S83.8,68.6,83.8,50S68.6,16.2,50,16.2z M50,80.2c-16.7,0-30.2-13.6-30.2-30.2S33.3,19.7,50,19.7S80.3,33.3,80.3,50S66.7,80.2,50,80.2z"/><rect fill="currentColor" x="48" y="31.7" width="4" height="28"/><rect fill="currentColor" x="48" y="63.2" width="4" height="5"/><!--Created by Shastry from the Noun Project--></svg>\n';
|
|
|
|
// src/services/DataHelper.ts
|
|
var import_three6 = require_three();
|
|
|
|
// src/services/AbstractService.ts
|
|
var AbstractService = class {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer) {
|
|
this.viewer = viewer;
|
|
this.config = viewer.config;
|
|
this.state = viewer.state;
|
|
}
|
|
/**
|
|
* Destroys the service
|
|
* @internal
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
destroy() {
|
|
}
|
|
};
|
|
|
|
// src/services/DataHelper.ts
|
|
var vector3 = new import_three6.Vector3();
|
|
var EULER_ZERO = new import_three6.Euler(0, 0, 0, "ZXY");
|
|
var DataHelper = class extends AbstractService {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer) {
|
|
super(viewer);
|
|
}
|
|
/**
|
|
* Converts vertical FOV to zoom level
|
|
*/
|
|
fovToZoomLevel(fov) {
|
|
const temp = Math.round((fov - this.config.minFov) / (this.config.maxFov - this.config.minFov) * 100);
|
|
return temp - 2 * (temp - 50);
|
|
}
|
|
/**
|
|
* Converts zoom level to vertical FOV
|
|
*/
|
|
zoomLevelToFov(level) {
|
|
return this.config.maxFov + level / 100 * (this.config.minFov - this.config.maxFov);
|
|
}
|
|
/**
|
|
* Converts vertical FOV to horizontal FOV
|
|
*/
|
|
vFovToHFov(vFov) {
|
|
return import_three6.MathUtils.radToDeg(2 * Math.atan(Math.tan(import_three6.MathUtils.degToRad(vFov) / 2) * this.state.aspect));
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
getAnimationProperties(speed, targetPosition, targetZoom) {
|
|
const positionProvided = !isNil(targetPosition);
|
|
const zoomProvided = !isNil(targetZoom);
|
|
const properties = {};
|
|
let duration = null;
|
|
if (positionProvided) {
|
|
const currentPosition = this.viewer.getPosition();
|
|
const dYaw = getShortestArc(currentPosition.yaw, targetPosition.yaw);
|
|
properties.yaw = { start: currentPosition.yaw, end: currentPosition.yaw + dYaw };
|
|
properties.pitch = { start: currentPosition.pitch, end: targetPosition.pitch };
|
|
duration = speedToDuration(speed, getAngle(currentPosition, targetPosition));
|
|
}
|
|
if (zoomProvided) {
|
|
const currentZoom = this.viewer.getZoomLevel();
|
|
const dZoom = Math.abs(targetZoom - currentZoom);
|
|
properties.zoom = { start: currentZoom, end: targetZoom };
|
|
if (duration === null) {
|
|
duration = speedToDuration(speed, Math.PI / 4 * dZoom / 100);
|
|
}
|
|
}
|
|
if (duration === null) {
|
|
if (typeof speed === "number") {
|
|
duration = speed;
|
|
} else {
|
|
duration = ANIMATION_MIN_DURATION;
|
|
}
|
|
} else {
|
|
duration = Math.max(ANIMATION_MIN_DURATION, duration);
|
|
}
|
|
return { duration, properties };
|
|
}
|
|
/**
|
|
* Converts pixel texture coordinates to spherical radians coordinates
|
|
* @throws {@link PSVError} when the current adapter does not support texture coordinates
|
|
*/
|
|
textureCoordsToSphericalCoords(point) {
|
|
if (!this.state.textureData?.panoData) {
|
|
throw new PSVError("Current adapter does not support texture coordinates or no texture has been loaded");
|
|
}
|
|
const result = this.viewer.adapter.textureCoordsToSphericalCoords(point, this.state.textureData.panoData);
|
|
if (!EULER_ZERO.equals(this.viewer.renderer.panoramaPose) || !EULER_ZERO.equals(this.viewer.renderer.sphereCorrection)) {
|
|
this.sphericalCoordsToVector3(result, vector3);
|
|
vector3.applyEuler(this.viewer.renderer.panoramaPose);
|
|
vector3.applyEuler(this.viewer.renderer.sphereCorrection);
|
|
return this.vector3ToSphericalCoords(vector3);
|
|
} else {
|
|
return result;
|
|
}
|
|
}
|
|
/**
|
|
* Converts spherical radians coordinates to pixel texture coordinates
|
|
* @throws {@link PSVError} when the current adapter does not support texture coordinates
|
|
*/
|
|
sphericalCoordsToTextureCoords(position) {
|
|
if (!this.state.textureData?.panoData) {
|
|
throw new PSVError("Current adapter does not support texture coordinates or no texture has been loaded");
|
|
}
|
|
if (!EULER_ZERO.equals(this.viewer.renderer.panoramaPose) || !EULER_ZERO.equals(this.viewer.renderer.sphereCorrection)) {
|
|
this.sphericalCoordsToVector3(position, vector3);
|
|
applyEulerInverse(vector3, this.viewer.renderer.sphereCorrection);
|
|
applyEulerInverse(vector3, this.viewer.renderer.panoramaPose);
|
|
position = this.vector3ToSphericalCoords(vector3);
|
|
}
|
|
return this.viewer.adapter.sphericalCoordsToTextureCoords(position, this.state.textureData.panoData);
|
|
}
|
|
/**
|
|
* Converts spherical radians coordinates to a Vector3
|
|
*/
|
|
sphericalCoordsToVector3(position, vector, distance2 = SPHERE_RADIUS) {
|
|
if (!vector) {
|
|
vector = new import_three6.Vector3();
|
|
}
|
|
vector.x = distance2 * -Math.cos(position.pitch) * Math.sin(position.yaw);
|
|
vector.y = distance2 * Math.sin(position.pitch);
|
|
vector.z = distance2 * Math.cos(position.pitch) * Math.cos(position.yaw);
|
|
return vector;
|
|
}
|
|
/**
|
|
* Converts a Vector3 to spherical radians coordinates
|
|
*/
|
|
vector3ToSphericalCoords(vector) {
|
|
const phi = Math.acos(vector.y / Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z));
|
|
const theta = Math.atan2(vector.x, vector.z);
|
|
return {
|
|
yaw: theta < 0 ? -theta : Math.PI * 2 - theta,
|
|
pitch: Math.PI / 2 - phi
|
|
};
|
|
}
|
|
/**
|
|
* Converts position on the viewer to a THREE.Vector3
|
|
*/
|
|
viewerCoordsToVector3(viewerPoint) {
|
|
const sphereIntersect = this.viewer.renderer.getIntersections(viewerPoint).filter((i) => i.object.userData[VIEWER_DATA]);
|
|
if (sphereIntersect.length) {
|
|
return sphereIntersect[0].point;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Converts position on the viewer to spherical radians coordinates
|
|
*/
|
|
viewerCoordsToSphericalCoords(viewerPoint) {
|
|
const vector = this.viewerCoordsToVector3(viewerPoint);
|
|
return vector ? this.vector3ToSphericalCoords(vector) : null;
|
|
}
|
|
/**
|
|
* Converts a Vector3 to position on the viewer
|
|
*/
|
|
vector3ToViewerCoords(vector) {
|
|
const vectorClone = vector.clone();
|
|
vectorClone.project(this.viewer.renderer.camera);
|
|
return {
|
|
x: Math.round((vectorClone.x + 1) / 2 * this.state.size.width),
|
|
y: Math.round((1 - vectorClone.y) / 2 * this.state.size.height)
|
|
};
|
|
}
|
|
/**
|
|
* Converts spherical radians coordinates to position on the viewer
|
|
*/
|
|
sphericalCoordsToViewerCoords(position) {
|
|
this.sphericalCoordsToVector3(position, vector3);
|
|
return this.vector3ToViewerCoords(vector3);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
isPointVisible(point) {
|
|
let vector;
|
|
let viewerPoint;
|
|
if (point instanceof import_three6.Vector3) {
|
|
vector = point;
|
|
viewerPoint = this.vector3ToViewerCoords(point);
|
|
} else if (isExtendedPosition(point)) {
|
|
vector = this.sphericalCoordsToVector3(point, vector3);
|
|
viewerPoint = this.vector3ToViewerCoords(vector);
|
|
} else {
|
|
return false;
|
|
}
|
|
return vector.dot(this.viewer.state.direction) > 0 && viewerPoint.x >= 0 && viewerPoint.x <= this.viewer.state.size.width && viewerPoint.y >= 0 && viewerPoint.y <= this.viewer.state.size.height;
|
|
}
|
|
/**
|
|
* Converts pixel position to angles if present and ensure boundaries
|
|
*/
|
|
cleanPosition(position) {
|
|
if ("yaw" in position || "pitch" in position) {
|
|
if (!("yaw" in position) || !("pitch" in position)) {
|
|
throw new PSVError(`Position is missing 'yaw' or 'pitch'`);
|
|
}
|
|
return {
|
|
yaw: parseAngle(position.yaw),
|
|
pitch: parseAngle(position.pitch, !this.state.littlePlanet)
|
|
};
|
|
} else {
|
|
return this.textureCoordsToSphericalCoords(position);
|
|
}
|
|
}
|
|
/**
|
|
* Ensure a SphereCorrection object is valid
|
|
*/
|
|
cleanSphereCorrection(sphereCorrection) {
|
|
return {
|
|
pan: parseAngle(sphereCorrection?.pan || 0),
|
|
tilt: parseAngle(sphereCorrection?.tilt || 0, true),
|
|
roll: parseAngle(sphereCorrection?.roll || 0, true, false)
|
|
};
|
|
}
|
|
/**
|
|
* Parse the pose angles of the pano data
|
|
*/
|
|
cleanPanoramaPose(panoData) {
|
|
return {
|
|
pan: import_three6.MathUtils.degToRad(panoData?.poseHeading || 0),
|
|
tilt: import_three6.MathUtils.degToRad(panoData?.posePitch || 0),
|
|
roll: import_three6.MathUtils.degToRad(panoData?.poseRoll || 0)
|
|
};
|
|
}
|
|
};
|
|
|
|
// src/services/EventsHandler.ts
|
|
var import_three7 = require_three();
|
|
|
|
// src/icons/gesture.svg
|
|
var gesture_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="currentColor" d="M33.38 33.2a1.96 1.96 0 0 0 1.5-3.23 10.61 10.61 0 0 1 7.18-17.51c.7-.06 1.31-.49 1.61-1.12a13.02 13.02 0 0 1 11.74-7.43c7.14 0 12.96 5.8 12.96 12.9 0 3.07-1.1 6.05-3.1 8.38-.7.82-.61 2.05.21 2.76.83.7 2.07.6 2.78-.22a16.77 16.77 0 0 0 4.04-10.91C72.3 7.54 64.72 0 55.4 0a16.98 16.98 0 0 0-14.79 8.7 14.6 14.6 0 0 0-12.23 14.36c0 3.46 1.25 6.82 3.5 9.45.4.45.94.69 1.5.69m45.74 43.55a22.13 22.13 0 0 1-5.23 12.4c-4 4.55-9.53 6.86-16.42 6.86-12.6 0-20.1-10.8-20.17-10.91a1.82 1.82 0 0 0-.08-.1c-5.3-6.83-14.55-23.82-17.27-28.87-.05-.1 0-.21.02-.23a6.3 6.3 0 0 1 8.24 1.85l9.38 12.59a1.97 1.97 0 0 0 3.54-1.17V25.34a4 4 0 0 1 1.19-2.87 3.32 3.32 0 0 1 2.4-.95c1.88.05 3.4 1.82 3.4 3.94v24.32a1.96 1.96 0 0 0 3.93 0v-33.1a3.5 3.5 0 0 1 7 0v35.39a1.96 1.96 0 0 0 3.93 0v-.44c.05-2.05 1.6-3.7 3.49-3.7 1.93 0 3.5 1.7 3.5 3.82v5.63c0 .24.04.48.13.71l.1.26a1.97 1.97 0 0 0 3.76-.37c.33-1.78 1.77-3.07 3.43-3.07 1.9 0 3.45 1.67 3.5 3.74l-1.77 18.1zM77.39 51c-1.25 0-2.45.32-3.5.9v-.15c0-4.27-3.33-7.74-7.42-7.74-1.26 0-2.45.33-3.5.9V16.69a7.42 7.42 0 0 0-14.85 0v1.86a7 7 0 0 0-3.28-.94 7.21 7.21 0 0 0-5.26 2.07 7.92 7.92 0 0 0-2.38 5.67v37.9l-5.83-7.82a10.2 10.2 0 0 0-13.35-2.92 4.1 4.1 0 0 0-1.53 5.48C20 64.52 28.74 80.45 34.07 87.34c.72 1.04 9.02 12.59 23.4 12.59 7.96 0 14.66-2.84 19.38-8.2a26.06 26.06 0 0 0 6.18-14.6l1.78-18.2v-.2c0-4.26-3.32-7.73-7.42-7.73z"/><!--Created by AomAm from the Noun Project--></svg>\n';
|
|
|
|
// src/icons/mousewheel.svg
|
|
var mousewheel_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 17 79 79"><path fill="currentColor" d="M38.1 29.27c-.24 0-.44.2-.44.45v10.7a.45.45 0 00.9 0v-10.7c0-.25-.2-.45-.45-.45zm10.2 26.66a11.54 11.54 0 01-8.48-6.14.45.45 0 10-.8.41 12.45 12.45 0 009.22 6.62.45.45 0 00.07-.9zm24.55-13.08a23.04 23.04 0 00-22.56-23v7.07l-.01.05a2.83 2.83 0 012.39 2.78v14.03l.09-.02h8.84v-9.22a.45.45 0 11.9 0v9.22h10.35v-.9zm0 27.33V44.66H62.5c-.02 2.01-.52 4-1.47 5.76a.45.45 0 01-.61.18.45.45 0 01-.19-.61 11.54 11.54 0 001.36-5.33h-8.83l-.1-.01a2.83 2.83 0 01-2.83 2.84h-.04-.04a2.83 2.83 0 01-2.83-2.83v-14.9a2.82 2.82 0 012.47-2.8v-7.11a23.04 23.04 0 00-22.57 23v.91h14.72V29.88a8.2 8.2 0 015.02-7.57c.22-.1.5.01.59.24.1.23-.01.5-.24.6a7.3 7.3 0 00-4.47 6.73v13.88h3.9a.45.45 0 110 .9h-3.9v.15a7.32 7.32 0 0011.23 6.17.45.45 0 01.49.76 8.22 8.22 0 01-12.62-6.93v-.15H26.82v25.52a23.04 23.04 0 0023.01 23.01 23.04 23.04 0 0023.02-23.01zm1.8-27.33v27.33A24.85 24.85 0 0149.84 95a24.85 24.85 0 01-24.82-24.82V42.85a24.85 24.85 0 0124.82-24.82 24.85 24.85 0 0124.83 24.82zM57.98 29.88v9.36a.45.45 0 11-.9 0v-9.36a7.28 7.28 0 00-3.4-6.17.45.45 0 01.49-.76 8.18 8.18 0 013.8 6.93z"/><!-- Created by Icon Island from the Noun Project --></svg>\n';
|
|
|
|
// src/services/EventsHandler.ts
|
|
var _Step = class _Step {
|
|
constructor() {
|
|
this.$ = _Step.IDLE;
|
|
}
|
|
is(...steps) {
|
|
return steps.some((step) => this.$ & step);
|
|
}
|
|
set(step) {
|
|
this.$ = step;
|
|
}
|
|
add(step) {
|
|
this.$ |= step;
|
|
}
|
|
remove(step) {
|
|
this.$ &= ~step;
|
|
}
|
|
};
|
|
_Step.IDLE = 0;
|
|
_Step.CLICK = 1;
|
|
_Step.MOVING = 2;
|
|
_Step.INERTIA = 4;
|
|
var Step = _Step;
|
|
var EventsHandler = class extends AbstractService {
|
|
constructor(viewer) {
|
|
super(viewer);
|
|
this.data = {
|
|
/** start x position of the click/touch */
|
|
startMouseX: 0,
|
|
/** start y position of the click/touch */
|
|
startMouseY: 0,
|
|
/** current x position of the cursor */
|
|
mouseX: 0,
|
|
/** current y position of the cursor */
|
|
mouseY: 0,
|
|
/** list of latest positions of the cursor, [time, x, y] */
|
|
mouseHistory: [],
|
|
/** distance between fingers when zooming */
|
|
pinchDist: 0,
|
|
/** when the Ctrl key is pressed */
|
|
ctrlKeyDown: false,
|
|
/** temporary storage of click data between two clicks */
|
|
dblclickData: null,
|
|
dblclickTimeout: null,
|
|
longtouchTimeout: null,
|
|
twofingersTimeout: null,
|
|
ctrlZoomTimeout: null
|
|
};
|
|
this.step = new Step();
|
|
this.keyHandler = new PressHandler();
|
|
this.resizeObserver = new ResizeObserver(throttle(() => this.viewer.autoSize(), 50));
|
|
this.moveThreshold = MOVE_THRESHOLD * SYSTEM.pixelRatio;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
init() {
|
|
window.addEventListener("keydown", this, { passive: false });
|
|
window.addEventListener("keyup", this);
|
|
this.viewer.container.addEventListener("mousedown", this);
|
|
window.addEventListener("mousemove", this, { passive: false });
|
|
window.addEventListener("mouseup", this);
|
|
this.viewer.container.addEventListener("touchstart", this, { passive: false });
|
|
window.addEventListener("touchmove", this, { passive: false });
|
|
window.addEventListener("touchend", this, { passive: false });
|
|
this.viewer.container.addEventListener("wheel", this, { passive: false });
|
|
document.addEventListener(SYSTEM.fullscreenEvent, this);
|
|
this.resizeObserver.observe(this.viewer.container);
|
|
}
|
|
destroy() {
|
|
window.removeEventListener("keydown", this);
|
|
window.removeEventListener("keyup", this);
|
|
this.viewer.container.removeEventListener("mousedown", this);
|
|
window.removeEventListener("mousemove", this);
|
|
window.removeEventListener("mouseup", this);
|
|
this.viewer.container.removeEventListener("touchstart", this);
|
|
window.removeEventListener("touchmove", this);
|
|
window.removeEventListener("touchend", this);
|
|
this.viewer.container.removeEventListener("wheel", this);
|
|
document.removeEventListener(SYSTEM.fullscreenEvent, this);
|
|
this.resizeObserver.disconnect();
|
|
clearTimeout(this.data.dblclickTimeout);
|
|
clearTimeout(this.data.longtouchTimeout);
|
|
clearTimeout(this.data.twofingersTimeout);
|
|
clearTimeout(this.data.ctrlZoomTimeout);
|
|
super.destroy();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
handleEvent(evt) {
|
|
switch (evt.type) {
|
|
case "keydown":
|
|
this.__onKeyDown(evt);
|
|
break;
|
|
case "keyup":
|
|
this.__onKeyUp();
|
|
break;
|
|
case "mousemove":
|
|
this.__onMouseMove(evt);
|
|
break;
|
|
case "mouseup":
|
|
this.__onMouseUp(evt);
|
|
break;
|
|
case "touchmove":
|
|
this.__onTouchMove(evt);
|
|
break;
|
|
case "touchend":
|
|
this.__onTouchEnd(evt);
|
|
break;
|
|
case SYSTEM.fullscreenEvent:
|
|
this.__onFullscreenChange();
|
|
break;
|
|
}
|
|
if (!getClosest(evt.target, "." + CAPTURE_EVENTS_CLASS)) {
|
|
switch (evt.type) {
|
|
case "mousedown":
|
|
this.__onMouseDown(evt);
|
|
break;
|
|
case "touchstart":
|
|
this.__onTouchStart(evt);
|
|
break;
|
|
case "wheel":
|
|
this.__onMouseWheel(evt);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Handles keyboard events
|
|
*/
|
|
__onKeyDown(e) {
|
|
if (this.config.mousewheelCtrlKey) {
|
|
this.data.ctrlKeyDown = e.key === KEY_CODES.Control;
|
|
if (this.data.ctrlKeyDown) {
|
|
clearTimeout(this.data.ctrlZoomTimeout);
|
|
this.viewer.overlay.hide(IDS.CTRL_ZOOM);
|
|
}
|
|
}
|
|
if (!this.viewer.dispatchEvent(new KeypressEvent(e.key))) {
|
|
return;
|
|
}
|
|
if (!this.state.keyboardEnabled) {
|
|
return;
|
|
}
|
|
const action = this.config.keyboardActions?.[e.key];
|
|
if (typeof action === "function") {
|
|
action(this.viewer);
|
|
e.preventDefault();
|
|
} else if (action && !this.keyHandler.pending) {
|
|
if (action !== "ZOOM_IN" /* ZOOM_IN */ && action !== "ZOOM_OUT" /* ZOOM_OUT */) {
|
|
this.viewer.stopAll();
|
|
}
|
|
switch (action) {
|
|
case "ROTATE_UP" /* ROTATE_UP */:
|
|
this.viewer.dynamics.position.roll({ pitch: false });
|
|
break;
|
|
case "ROTATE_DOWN" /* ROTATE_DOWN */:
|
|
this.viewer.dynamics.position.roll({ pitch: true });
|
|
break;
|
|
case "ROTATE_RIGHT" /* ROTATE_RIGHT */:
|
|
this.viewer.dynamics.position.roll({ yaw: false });
|
|
break;
|
|
case "ROTATE_LEFT" /* ROTATE_LEFT */:
|
|
this.viewer.dynamics.position.roll({ yaw: true });
|
|
break;
|
|
case "ZOOM_IN" /* ZOOM_IN */:
|
|
this.viewer.dynamics.zoom.roll(false);
|
|
break;
|
|
case "ZOOM_OUT" /* ZOOM_OUT */:
|
|
this.viewer.dynamics.zoom.roll(true);
|
|
break;
|
|
}
|
|
this.keyHandler.down();
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
/**
|
|
* Handles keyboard events
|
|
*/
|
|
__onKeyUp() {
|
|
this.data.ctrlKeyDown = false;
|
|
if (!this.state.keyboardEnabled) {
|
|
return;
|
|
}
|
|
this.keyHandler.up(() => {
|
|
this.viewer.dynamics.position.stop();
|
|
this.viewer.dynamics.zoom.stop();
|
|
this.viewer.resetIdleTimer();
|
|
});
|
|
}
|
|
/**
|
|
* Handles mouse down events
|
|
*/
|
|
__onMouseDown(evt) {
|
|
this.step.add(Step.CLICK);
|
|
this.data.startMouseX = evt.clientX;
|
|
this.data.startMouseY = evt.clientY;
|
|
}
|
|
/**
|
|
*Handles mouse up events
|
|
*/
|
|
__onMouseUp(evt) {
|
|
if (this.step.is(Step.CLICK, Step.MOVING)) {
|
|
this.__stopMove(evt.clientX, evt.clientY, evt.target, evt.button === 2);
|
|
}
|
|
}
|
|
/**
|
|
* Handles mouse move events
|
|
*/
|
|
__onMouseMove(evt) {
|
|
if (this.config.mousemove && this.step.is(Step.CLICK, Step.MOVING)) {
|
|
evt.preventDefault();
|
|
this.__doMove(evt.clientX, evt.clientY);
|
|
}
|
|
this.__handleObjectsEvents(evt);
|
|
}
|
|
/**
|
|
* Handles touch events
|
|
*/
|
|
__onTouchStart(evt) {
|
|
if (evt.touches.length === 1) {
|
|
this.step.add(Step.CLICK);
|
|
this.data.startMouseX = evt.touches[0].clientX;
|
|
this.data.startMouseY = evt.touches[0].clientY;
|
|
if (!this.data.longtouchTimeout) {
|
|
this.data.longtouchTimeout = setTimeout(() => {
|
|
const touch = evt.touches[0];
|
|
this.__stopMove(touch.clientX, touch.clientY, touch.target, true);
|
|
this.data.longtouchTimeout = null;
|
|
}, LONGTOUCH_DELAY);
|
|
}
|
|
} else if (evt.touches.length === 2) {
|
|
this.step.set(Step.IDLE);
|
|
this.__cancelLongTouch();
|
|
if (this.config.mousemove) {
|
|
this.__cancelTwoFingersOverlay();
|
|
this.__startMoveZoom(evt);
|
|
evt.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Handles touch events
|
|
*/
|
|
__onTouchEnd(evt) {
|
|
this.__cancelLongTouch();
|
|
if (this.step.is(Step.CLICK, Step.MOVING)) {
|
|
evt.preventDefault();
|
|
this.__cancelTwoFingersOverlay();
|
|
if (evt.touches.length === 1) {
|
|
this.__stopMove(this.data.mouseX, this.data.mouseY);
|
|
} else if (evt.touches.length === 0) {
|
|
const touch = evt.changedTouches[0];
|
|
this.__stopMove(touch.clientX, touch.clientY, touch.target);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Handles touch move events
|
|
*/
|
|
__onTouchMove(evt) {
|
|
this.__cancelLongTouch();
|
|
if (!this.config.mousemove) {
|
|
return;
|
|
}
|
|
if (evt.touches.length === 1) {
|
|
if (this.config.touchmoveTwoFingers) {
|
|
if (this.step.is(Step.CLICK) && !this.data.twofingersTimeout) {
|
|
this.data.twofingersTimeout = setTimeout(() => {
|
|
this.viewer.overlay.show({
|
|
id: IDS.TWO_FINGERS,
|
|
image: gesture_default,
|
|
title: this.config.lang.twoFingers
|
|
});
|
|
}, TWOFINGERSOVERLAY_DELAY);
|
|
}
|
|
} else if (this.step.is(Step.CLICK, Step.MOVING)) {
|
|
evt.preventDefault();
|
|
const touch = evt.touches[0];
|
|
this.__doMove(touch.clientX, touch.clientY);
|
|
}
|
|
} else {
|
|
this.__doMoveZoom(evt);
|
|
this.__cancelTwoFingersOverlay();
|
|
}
|
|
}
|
|
/**
|
|
* Cancel the long touch timer if any
|
|
*/
|
|
__cancelLongTouch() {
|
|
if (this.data.longtouchTimeout) {
|
|
clearTimeout(this.data.longtouchTimeout);
|
|
this.data.longtouchTimeout = null;
|
|
}
|
|
}
|
|
/**
|
|
* Cancel the two fingers overlay timer if any
|
|
*/
|
|
__cancelTwoFingersOverlay() {
|
|
if (this.config.touchmoveTwoFingers) {
|
|
if (this.data.twofingersTimeout) {
|
|
clearTimeout(this.data.twofingersTimeout);
|
|
this.data.twofingersTimeout = null;
|
|
}
|
|
this.viewer.overlay.hide(IDS.TWO_FINGERS);
|
|
}
|
|
}
|
|
/**
|
|
* Handles mouse wheel events
|
|
*/
|
|
__onMouseWheel(evt) {
|
|
if (!this.config.mousewheel || !evt.deltaY) {
|
|
return;
|
|
}
|
|
if (this.config.mousewheelCtrlKey && !this.data.ctrlKeyDown) {
|
|
this.viewer.overlay.show({
|
|
id: IDS.CTRL_ZOOM,
|
|
image: mousewheel_default,
|
|
title: this.config.lang.ctrlZoom
|
|
});
|
|
clearTimeout(this.data.ctrlZoomTimeout);
|
|
this.data.ctrlZoomTimeout = setTimeout(() => this.viewer.overlay.hide(IDS.CTRL_ZOOM), CTRLZOOM_TIMEOUT);
|
|
return;
|
|
}
|
|
evt.preventDefault();
|
|
evt.stopPropagation();
|
|
const delta = evt.deltaY / Math.abs(evt.deltaY) * 5 * this.config.zoomSpeed;
|
|
if (delta !== 0) {
|
|
this.viewer.dynamics.zoom.step(-delta, 5);
|
|
}
|
|
}
|
|
/**
|
|
* Handles fullscreen events
|
|
*/
|
|
__onFullscreenChange() {
|
|
const fullscreen = this.viewer.isFullscreenEnabled();
|
|
if (this.config.keyboard === "fullscreen") {
|
|
if (fullscreen) {
|
|
this.viewer.startKeyboardControl();
|
|
} else {
|
|
this.viewer.stopKeyboardControl();
|
|
}
|
|
}
|
|
this.viewer.dispatchEvent(new FullscreenEvent(fullscreen));
|
|
}
|
|
/**
|
|
* Resets all state variables
|
|
*/
|
|
__resetMove() {
|
|
this.step.set(Step.IDLE);
|
|
this.data.mouseX = 0;
|
|
this.data.mouseY = 0;
|
|
this.data.startMouseX = 0;
|
|
this.data.startMouseY = 0;
|
|
this.data.mouseHistory.length = 0;
|
|
}
|
|
/**
|
|
* Initializes the combines move and zoom
|
|
*/
|
|
__startMoveZoom(evt) {
|
|
this.viewer.stopAll();
|
|
this.__resetMove();
|
|
const touchData = getTouchData(evt);
|
|
this.step.set(Step.MOVING);
|
|
({
|
|
distance: this.data.pinchDist,
|
|
center: { x: this.data.mouseX, y: this.data.mouseY }
|
|
} = touchData);
|
|
this.__logMouseMove(this.data.mouseX, this.data.mouseY);
|
|
}
|
|
/**
|
|
* Stops the movement
|
|
* @description If the move threshold was not reached a click event is triggered, otherwise an animation is launched to simulate inertia
|
|
*/
|
|
__stopMove(clientX, clientY, target, rightclick = false) {
|
|
if (this.step.is(Step.MOVING)) {
|
|
if (this.config.moveInertia) {
|
|
this.__logMouseMove(clientX, clientY);
|
|
this.__stopMoveInertia(clientX, clientY);
|
|
} else {
|
|
this.__resetMove();
|
|
this.viewer.resetIdleTimer();
|
|
}
|
|
} else {
|
|
if (this.step.is(Step.CLICK) && !this.__moveThresholdReached(clientX, clientY)) {
|
|
this.__doClick(clientX, clientY, target, rightclick);
|
|
}
|
|
this.step.remove(Step.CLICK);
|
|
if (!this.step.is(Step.INERTIA)) {
|
|
this.__resetMove();
|
|
this.viewer.resetIdleTimer();
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Performs an animation to simulate inertia when the movement stops
|
|
*/
|
|
__stopMoveInertia(clientX, clientY) {
|
|
const curve = new import_three7.SplineCurve(this.data.mouseHistory.map(([, x, y]) => new import_three7.Vector2(x, y)));
|
|
const direction = curve.getTangent(1);
|
|
const speed = this.data.mouseHistory.reduce(({ total, prev }, curr) => ({
|
|
total: !prev ? 0 : total + distance({ x: prev[1], y: prev[2] }, { x: curr[1], y: curr[2] }) / (curr[0] - prev[0]),
|
|
prev: curr
|
|
}), {
|
|
total: 0,
|
|
prev: null
|
|
}).total / this.data.mouseHistory.length;
|
|
if (!speed) {
|
|
this.__resetMove();
|
|
this.viewer.resetIdleTimer();
|
|
return;
|
|
}
|
|
this.step.set(Step.INERTIA);
|
|
let currentClientX = clientX;
|
|
let currentClientY = clientY;
|
|
this.state.animation = new Animation({
|
|
properties: {
|
|
speed: { start: speed, end: 0 }
|
|
},
|
|
duration: 1e3,
|
|
easing: "outQuad",
|
|
onTick: (properties) => {
|
|
currentClientX += properties.speed * direction.x * 3 * SYSTEM.pixelRatio;
|
|
currentClientY += properties.speed * direction.y * 3 * SYSTEM.pixelRatio;
|
|
this.__applyMove(currentClientX, currentClientY);
|
|
}
|
|
});
|
|
this.state.animation.then((done) => {
|
|
this.state.animation = null;
|
|
if (done) {
|
|
this.__resetMove();
|
|
this.viewer.resetIdleTimer();
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Triggers an event with all coordinates when a simple click is performed
|
|
*/
|
|
__doClick(clientX, clientY, target, rightclick = false) {
|
|
const boundingRect = this.viewer.container.getBoundingClientRect();
|
|
const viewerX = clientX - boundingRect.left;
|
|
const viewerY = clientY - boundingRect.top;
|
|
const intersections = this.viewer.renderer.getIntersections({ x: viewerX, y: viewerY });
|
|
const sphereIntersection = intersections.find((i) => i.object.userData[VIEWER_DATA]);
|
|
if (sphereIntersection) {
|
|
const sphericalCoords = this.viewer.dataHelper.vector3ToSphericalCoords(sphereIntersection.point);
|
|
const data = {
|
|
rightclick,
|
|
target,
|
|
clientX,
|
|
clientY,
|
|
viewerX,
|
|
viewerY,
|
|
yaw: sphericalCoords.yaw,
|
|
pitch: sphericalCoords.pitch,
|
|
objects: intersections.map((i) => i.object).filter((o) => !o.userData[VIEWER_DATA])
|
|
};
|
|
try {
|
|
const textureCoords = this.viewer.dataHelper.sphericalCoordsToTextureCoords(data);
|
|
Object.assign(data, textureCoords);
|
|
} catch (e) {
|
|
}
|
|
if (!this.data.dblclickTimeout) {
|
|
this.viewer.dispatchEvent(new ClickEvent(data));
|
|
this.data.dblclickData = clone(data);
|
|
this.data.dblclickTimeout = setTimeout(() => {
|
|
this.data.dblclickTimeout = null;
|
|
this.data.dblclickData = null;
|
|
}, DBLCLICK_DELAY);
|
|
} else {
|
|
if (Math.abs(this.data.dblclickData.clientX - data.clientX) < this.moveThreshold && Math.abs(this.data.dblclickData.clientY - data.clientY) < this.moveThreshold) {
|
|
this.viewer.dispatchEvent(new DoubleClickEvent(this.data.dblclickData));
|
|
}
|
|
clearTimeout(this.data.dblclickTimeout);
|
|
this.data.dblclickTimeout = null;
|
|
this.data.dblclickData = null;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Trigger events for observed THREE objects
|
|
*/
|
|
__handleObjectsEvents(evt) {
|
|
if (!isEmpty(this.state.objectsObservers) && hasParent(evt.target, this.viewer.container)) {
|
|
const viewerPos = getPosition(this.viewer.container);
|
|
const viewerPoint = {
|
|
x: evt.clientX - viewerPos.x,
|
|
y: evt.clientY - viewerPos.y
|
|
};
|
|
const intersections = this.viewer.renderer.getIntersections(viewerPoint);
|
|
const emit = (object, key, evtCtor) => {
|
|
this.viewer.dispatchEvent(new evtCtor(evt, object, viewerPoint, key));
|
|
};
|
|
for (const [key, object] of Object.entries(this.state.objectsObservers)) {
|
|
const intersection = intersections.find((i) => i.object.userData[key]);
|
|
if (intersection) {
|
|
if (object && intersection.object !== object) {
|
|
emit(object, key, ObjectLeaveEvent);
|
|
this.state.objectsObservers[key] = null;
|
|
}
|
|
if (!object) {
|
|
this.state.objectsObservers[key] = intersection.object;
|
|
emit(intersection.object, key, ObjectEnterEvent);
|
|
} else {
|
|
emit(intersection.object, key, ObjectHoverEvent);
|
|
}
|
|
} else if (object) {
|
|
emit(object, key, ObjectLeaveEvent);
|
|
this.state.objectsObservers[key] = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Starts moving when crossing moveThreshold and performs movement
|
|
*/
|
|
__doMove(clientX, clientY) {
|
|
if (this.step.is(Step.CLICK) && this.__moveThresholdReached(clientX, clientY)) {
|
|
this.viewer.stopAll();
|
|
this.__resetMove();
|
|
this.step.set(Step.MOVING);
|
|
this.data.mouseX = clientX;
|
|
this.data.mouseY = clientY;
|
|
this.__logMouseMove(clientX, clientY);
|
|
} else if (this.step.is(Step.MOVING)) {
|
|
this.__applyMove(clientX, clientY);
|
|
this.__logMouseMove(clientX, clientY);
|
|
}
|
|
}
|
|
/**
|
|
* Checks if the cursor was move beyond the move threshold
|
|
*/
|
|
__moveThresholdReached(clientX, clientY) {
|
|
return Math.abs(clientX - this.data.startMouseX) >= this.moveThreshold || Math.abs(clientY - this.data.startMouseY) >= this.moveThreshold;
|
|
}
|
|
/**
|
|
* Raw method for movement, called from mouse event and move inertia
|
|
*/
|
|
__applyMove(clientX, clientY) {
|
|
const x = (clientX - this.data.mouseX) * Math.cos(this.state.roll) - (clientY - this.data.mouseY) * Math.sin(this.state.roll);
|
|
const y = (clientY - this.data.mouseY) * Math.cos(this.state.roll) + (clientX - this.data.mouseX) * Math.sin(this.state.roll);
|
|
const rotation = {
|
|
yaw: this.config.moveSpeed * (x / this.state.size.width) * import_three7.MathUtils.degToRad(this.state.littlePlanet ? 90 : this.state.hFov),
|
|
pitch: this.config.moveSpeed * (y / this.state.size.height) * import_three7.MathUtils.degToRad(this.state.littlePlanet ? 90 : this.state.vFov)
|
|
};
|
|
const currentPosition = this.viewer.getPosition();
|
|
this.viewer.rotate({
|
|
yaw: currentPosition.yaw - rotation.yaw,
|
|
pitch: currentPosition.pitch + rotation.pitch
|
|
});
|
|
this.data.mouseX = clientX;
|
|
this.data.mouseY = clientY;
|
|
}
|
|
/**
|
|
* Perfoms combined move and zoom
|
|
*/
|
|
__doMoveZoom(evt) {
|
|
if (this.step.is(Step.MOVING)) {
|
|
evt.preventDefault();
|
|
const touchData = getTouchData(evt);
|
|
const delta = (touchData.distance - this.data.pinchDist) / SYSTEM.pixelRatio * this.config.zoomSpeed;
|
|
this.viewer.zoom(this.viewer.getZoomLevel() + delta);
|
|
this.__doMove(touchData.center.x, touchData.center.y);
|
|
this.data.pinchDist = touchData.distance;
|
|
}
|
|
}
|
|
/**
|
|
* Stores each mouse position during a mouse move
|
|
* @description Positions older than "INERTIA_WINDOW" are removed<br>
|
|
* Positions before a pause of "INERTIA_WINDOW" / 10 are removed
|
|
*/
|
|
__logMouseMove(clientX, clientY) {
|
|
const now = Date.now();
|
|
const last = this.data.mouseHistory.length ? this.data.mouseHistory[this.data.mouseHistory.length - 1] : [0, -1, -1];
|
|
if (last[1] === clientX && last[2] === clientY) {
|
|
last[0] = now;
|
|
} else if (now === last[0]) {
|
|
last[1] = clientX;
|
|
last[2] = clientY;
|
|
} else {
|
|
this.data.mouseHistory.push([now, clientX, clientY]);
|
|
}
|
|
let previous = null;
|
|
for (let i = 0; i < this.data.mouseHistory.length; ) {
|
|
if (this.data.mouseHistory[i][0] < now - INERTIA_WINDOW) {
|
|
this.data.mouseHistory.splice(i, 1);
|
|
} else if (previous && this.data.mouseHistory[i][0] - previous > INERTIA_WINDOW / 10) {
|
|
this.data.mouseHistory.splice(0, i);
|
|
i = 0;
|
|
previous = this.data.mouseHistory[i][0];
|
|
} else {
|
|
previous = this.data.mouseHistory[i][0];
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/services/Renderer.ts
|
|
var import_three8 = require_three();
|
|
import_three8.ColorManagement.enabled = false;
|
|
var vector2 = new import_three8.Vector2();
|
|
var matrix4 = new import_three8.Matrix4();
|
|
var box3 = new import_three8.Box3();
|
|
var Renderer = class extends AbstractService {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer) {
|
|
super(viewer);
|
|
this.frustumNeedsUpdate = true;
|
|
this.renderer = new import_three8.WebGLRenderer(this.config.rendererParameters);
|
|
this.renderer.setPixelRatio(SYSTEM.pixelRatio);
|
|
this.renderer.outputColorSpace = import_three8.LinearSRGBColorSpace;
|
|
this.renderer.domElement.className = "psv-canvas";
|
|
this.scene = new import_three8.Scene();
|
|
this.camera = new import_three8.PerspectiveCamera(50, 16 / 9, 0.1, 2 * SPHERE_RADIUS);
|
|
this.camera.matrixWorldAutoUpdate = false;
|
|
this.mesh = this.viewer.adapter.createMesh();
|
|
this.mesh.userData = { [VIEWER_DATA]: true };
|
|
this.meshContainer = new import_three8.Group();
|
|
this.meshContainer.add(this.mesh);
|
|
this.scene.add(this.meshContainer);
|
|
this.raycaster = new import_three8.Raycaster();
|
|
this.frustum = new import_three8.Frustum();
|
|
this.container = document.createElement("div");
|
|
this.container.className = "psv-canvas-container";
|
|
this.container.appendChild(this.renderer.domElement);
|
|
this.viewer.container.appendChild(this.container);
|
|
this.viewer.addEventListener(SizeUpdatedEvent.type, this);
|
|
this.viewer.addEventListener(ZoomUpdatedEvent.type, this);
|
|
this.viewer.addEventListener(PositionUpdatedEvent.type, this);
|
|
this.viewer.addEventListener(RollUpdatedEvent.type, this);
|
|
this.viewer.addEventListener(ConfigChangedEvent.type, this);
|
|
this.hide();
|
|
}
|
|
get panoramaPose() {
|
|
return this.mesh.rotation;
|
|
}
|
|
get sphereCorrection() {
|
|
return this.meshContainer.rotation;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
init() {
|
|
this.show();
|
|
this.renderer.setAnimationLoop((t) => this.__renderLoop(t));
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
destroy() {
|
|
this.renderer.setAnimationLoop(null);
|
|
this.cleanScene(this.scene);
|
|
this.viewer.container.removeChild(this.container);
|
|
this.viewer.removeEventListener(SizeUpdatedEvent.type, this);
|
|
this.viewer.removeEventListener(ZoomUpdatedEvent.type, this);
|
|
this.viewer.removeEventListener(PositionUpdatedEvent.type, this);
|
|
this.viewer.removeEventListener(RollUpdatedEvent.type, this);
|
|
this.viewer.removeEventListener(ConfigChangedEvent.type, this);
|
|
super.destroy();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
handleEvent(e) {
|
|
switch (e.type) {
|
|
case SizeUpdatedEvent.type:
|
|
this.__onSizeUpdated();
|
|
break;
|
|
case ZoomUpdatedEvent.type:
|
|
this.__onZoomUpdated();
|
|
break;
|
|
case PositionUpdatedEvent.type:
|
|
this.__onPositionUpdated();
|
|
break;
|
|
case RollUpdatedEvent.type:
|
|
this.__onPositionUpdated();
|
|
break;
|
|
case ConfigChangedEvent.type:
|
|
if (e.containsOptions("fisheye")) {
|
|
this.__onPositionUpdated();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/**
|
|
* Hides the viewer
|
|
*/
|
|
hide() {
|
|
this.container.style.opacity = "0";
|
|
}
|
|
/**
|
|
* Shows the viewer
|
|
*/
|
|
show() {
|
|
this.container.style.opacity = "1";
|
|
}
|
|
/**
|
|
* Resets or replaces the THREE renderer by a custom one
|
|
*/
|
|
setCustomRenderer(factory) {
|
|
if (factory) {
|
|
this.customRenderer = factory(this.renderer);
|
|
} else {
|
|
this.customRenderer = null;
|
|
}
|
|
this.viewer.needsUpdate();
|
|
}
|
|
/**
|
|
* Updates the size of the renderer and the aspect of the camera
|
|
*/
|
|
__onSizeUpdated() {
|
|
this.renderer.setSize(this.state.size.width, this.state.size.height);
|
|
this.camera.aspect = this.state.aspect;
|
|
this.camera.updateProjectionMatrix();
|
|
this.viewer.needsUpdate();
|
|
this.frustumNeedsUpdate = true;
|
|
}
|
|
/**
|
|
* Updates the fov of the camera
|
|
*/
|
|
__onZoomUpdated() {
|
|
this.camera.fov = this.state.vFov;
|
|
this.camera.updateProjectionMatrix();
|
|
this.viewer.needsUpdate();
|
|
this.frustumNeedsUpdate = true;
|
|
}
|
|
/**
|
|
* Updates the position of the camera
|
|
*/
|
|
__onPositionUpdated() {
|
|
this.camera.position.set(0, 0, 0);
|
|
this.camera.lookAt(this.state.direction);
|
|
if (this.config.fisheye) {
|
|
this.camera.position.copy(this.state.direction).multiplyScalar(this.config.fisheye / 2).negate();
|
|
}
|
|
this.camera.rotateZ(-this.state.roll);
|
|
this.camera.updateMatrixWorld();
|
|
this.viewer.needsUpdate();
|
|
this.frustumNeedsUpdate = true;
|
|
}
|
|
/**
|
|
* Main event loop, performs a render if `state.needsUpdate` is true
|
|
*/
|
|
__renderLoop(timestamp) {
|
|
const elapsed = !this.timestamp ? 0 : timestamp - this.timestamp;
|
|
this.timestamp = timestamp;
|
|
this.viewer.dispatchEvent(new BeforeRenderEvent(timestamp, elapsed));
|
|
this.viewer.dynamics.update(elapsed);
|
|
if (this.state.needsUpdate || this.state.continuousUpdateCount > 0) {
|
|
this.state.needsUpdate = false;
|
|
(this.customRenderer || this.renderer).render(this.scene, this.camera);
|
|
this.viewer.dispatchEvent(new RenderEvent());
|
|
}
|
|
}
|
|
/**
|
|
* Applies the texture to the scene, creates the scene if needed
|
|
* @internal
|
|
*/
|
|
setTexture(textureData) {
|
|
if (this.state.textureData) {
|
|
this.viewer.adapter.disposeTexture(this.state.textureData);
|
|
}
|
|
this.state.textureData = textureData;
|
|
this.viewer.adapter.setTexture(this.mesh, textureData);
|
|
this.viewer.needsUpdate();
|
|
}
|
|
/**
|
|
* Applies a panorama data pose to a Mesh
|
|
* @internal
|
|
*/
|
|
setPanoramaPose(panoData, mesh = this.mesh) {
|
|
const cleanCorrection = this.viewer.dataHelper.cleanPanoramaPose(panoData);
|
|
mesh.rotation.set(-cleanCorrection.tilt, -cleanCorrection.pan, -cleanCorrection.roll, "ZXY");
|
|
}
|
|
/**
|
|
* Applies a SphereCorrection to a Group
|
|
* @internal
|
|
*/
|
|
setSphereCorrection(sphereCorrection, group = this.meshContainer) {
|
|
const cleanCorrection = this.viewer.dataHelper.cleanSphereCorrection(sphereCorrection);
|
|
group.rotation.set(cleanCorrection.tilt, cleanCorrection.pan, cleanCorrection.roll, "ZXY");
|
|
}
|
|
/**
|
|
* Performs transition between the current and a new texture
|
|
* @internal
|
|
*/
|
|
transition(textureData, options) {
|
|
const positionProvided = !isNil(options.position);
|
|
const zoomProvided = !isNil(options.zoom);
|
|
const e = new BeforeAnimateEvent(
|
|
positionProvided ? this.viewer.dataHelper.cleanPosition(options.position) : void 0,
|
|
options.zoom
|
|
);
|
|
this.viewer.dispatchEvent(e);
|
|
const group = new import_three8.Group();
|
|
const mesh = this.viewer.adapter.createMesh(0.5);
|
|
this.viewer.adapter.setTexture(mesh, textureData, true);
|
|
this.viewer.adapter.setTextureOpacity(mesh, 0);
|
|
this.setPanoramaPose(textureData.panoData, mesh);
|
|
this.setSphereCorrection(options.sphereCorrection, group);
|
|
if (positionProvided && options.transition === "fade-only") {
|
|
const currentPosition = this.viewer.getPosition();
|
|
const verticalAxis = new import_three8.Vector3(0, 1, 0);
|
|
group.rotateOnWorldAxis(verticalAxis, e.position.yaw - currentPosition.yaw);
|
|
const horizontalAxis = new import_three8.Vector3(0, 1, 0).cross(this.camera.getWorldDirection(new import_three8.Vector3())).normalize();
|
|
group.rotateOnWorldAxis(horizontalAxis, e.position.pitch - currentPosition.pitch);
|
|
}
|
|
group.add(mesh);
|
|
this.scene.add(group);
|
|
this.renderer.setRenderTarget(new import_three8.WebGLRenderTarget());
|
|
this.renderer.render(this.scene, this.camera);
|
|
this.renderer.setRenderTarget(null);
|
|
const { duration, properties } = this.viewer.dataHelper.getAnimationProperties(
|
|
options.speed,
|
|
options.transition === true ? e.position : null,
|
|
e.zoomLevel
|
|
);
|
|
const animation = new Animation({
|
|
properties: {
|
|
...properties,
|
|
opacity: { start: 0, end: 1 }
|
|
},
|
|
duration,
|
|
easing: "inOutCubic",
|
|
onTick: (props) => {
|
|
this.viewer.adapter.setTextureOpacity(mesh, props.opacity);
|
|
if (positionProvided && options.transition === true) {
|
|
this.viewer.dynamics.position.setValue({
|
|
yaw: props.yaw,
|
|
pitch: props.pitch
|
|
});
|
|
}
|
|
if (zoomProvided) {
|
|
this.viewer.dynamics.zoom.setValue(props.zoom);
|
|
}
|
|
this.viewer.needsUpdate();
|
|
}
|
|
});
|
|
animation.then((completed) => {
|
|
if (completed) {
|
|
this.setTexture(textureData);
|
|
this.viewer.adapter.setTextureOpacity(this.mesh, 1);
|
|
this.setPanoramaPose(textureData.panoData);
|
|
this.setSphereCorrection(options.sphereCorrection);
|
|
if (positionProvided && options.transition === "fade-only") {
|
|
this.viewer.rotate(options.position);
|
|
}
|
|
} else {
|
|
this.viewer.adapter.disposeTexture(textureData);
|
|
}
|
|
this.scene.remove(group);
|
|
mesh.geometry.dispose();
|
|
mesh.geometry = null;
|
|
});
|
|
return animation;
|
|
}
|
|
/**
|
|
* Returns intersections with objects in the scene
|
|
*/
|
|
getIntersections(viewerPoint) {
|
|
vector2.x = 2 * viewerPoint.x / this.state.size.width - 1;
|
|
vector2.y = -2 * viewerPoint.y / this.state.size.height + 1;
|
|
this.raycaster.setFromCamera(vector2, this.camera);
|
|
const intersections = this.raycaster.intersectObjects(this.scene.children, true).filter((i) => i.object.visible).filter((i) => i.object.isMesh && !!i.object.userData);
|
|
if (this.customRenderer?.getIntersections) {
|
|
intersections.push(...this.customRenderer.getIntersections(this.raycaster, vector2));
|
|
}
|
|
return intersections;
|
|
}
|
|
/**
|
|
* Checks if an object/point is currently visible
|
|
*/
|
|
isObjectVisible(value) {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
if (this.frustumNeedsUpdate) {
|
|
matrix4.multiplyMatrices(this.camera.projectionMatrix, this.camera.matrixWorldInverse);
|
|
this.frustum.setFromProjectionMatrix(matrix4);
|
|
this.frustumNeedsUpdate = false;
|
|
}
|
|
if (value.isVector3) {
|
|
return this.frustum.containsPoint(value);
|
|
} else if (value.isMesh && value.geometry) {
|
|
const mesh = value;
|
|
if (!mesh.geometry.boundingBox) {
|
|
mesh.geometry.computeBoundingBox();
|
|
}
|
|
box3.copy(mesh.geometry.boundingBox).applyMatrix4(mesh.matrixWorld);
|
|
return this.frustum.intersectsBox(box3);
|
|
} else if (value.isObject3D) {
|
|
return this.frustum.intersectsObject(value);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Adds an object to the THREE scene
|
|
*/
|
|
addObject(object) {
|
|
this.scene.add(object);
|
|
}
|
|
/**
|
|
* Removes an object from the THREE scene
|
|
*/
|
|
removeObject(object) {
|
|
this.scene.remove(object);
|
|
}
|
|
/**
|
|
* Calls `dispose` on all objects and textures
|
|
* @internal
|
|
*/
|
|
cleanScene(object) {
|
|
const disposeMaterial = (material) => {
|
|
material.map?.dispose();
|
|
if (material.uniforms) {
|
|
Object.values(material.uniforms).forEach((uniform) => {
|
|
uniform.value?.dispose?.();
|
|
});
|
|
}
|
|
material.dispose();
|
|
};
|
|
object.traverse((item) => {
|
|
item.geometry?.dispose();
|
|
if (item.material) {
|
|
if (Array.isArray(item.material)) {
|
|
item.material.forEach((material) => {
|
|
disposeMaterial(material);
|
|
});
|
|
} else {
|
|
disposeMaterial(item.material);
|
|
}
|
|
}
|
|
if (!(item instanceof import_three8.Scene)) {
|
|
item.dispose?.();
|
|
}
|
|
if (item !== object) {
|
|
this.cleanScene(item);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/lib/BlobLoader.ts
|
|
var import_three9 = require_three();
|
|
var BlobLoader = class extends import_three9.Loader {
|
|
// @ts-ignore
|
|
load(url, onLoad, onProgress, onError, abortSignal) {
|
|
const req = new Request(url, {
|
|
headers: new Headers(this.requestHeader),
|
|
credentials: this.withCredentials ? "include" : "same-origin"
|
|
});
|
|
fetch(req, {
|
|
signal: abortSignal
|
|
}).then((response) => {
|
|
if (response.status === 200 || response.status === 0) {
|
|
const reader = response.body.getReader();
|
|
const contentLength = response.headers.get("Content-Length") || response.headers.get("X-File-Size");
|
|
const total = contentLength ? parseInt(contentLength) : 0;
|
|
const lengthComputable = total !== 0;
|
|
let loaded = 0;
|
|
const stream = new ReadableStream({
|
|
start(controller) {
|
|
readData();
|
|
function readData() {
|
|
reader.read().then(({ done, value }) => {
|
|
if (done) {
|
|
controller.close();
|
|
} else {
|
|
loaded += value.byteLength;
|
|
const event = new ProgressEvent("progress", { lengthComputable, loaded, total });
|
|
onProgress(event);
|
|
controller.enqueue(value);
|
|
readData();
|
|
}
|
|
}).catch((err) => {
|
|
onError(err);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
return new Response(stream);
|
|
} else {
|
|
throw new Error(`fetch for "${response.url}" responded with ${response.status}: ${response.statusText}`);
|
|
}
|
|
}).then((response) => {
|
|
return response.blob();
|
|
}).then((data) => {
|
|
onLoad(data);
|
|
}).catch((err) => {
|
|
onError(err);
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/lib/ImageLoader.ts
|
|
var import_three10 = require_three();
|
|
var ImageLoader = class extends import_three10.Loader {
|
|
// @ts-ignore
|
|
load(url, onLoad, onError, abortSignal) {
|
|
const image = document.createElement("img");
|
|
function onImageLoad() {
|
|
removeEventListeners();
|
|
onLoad(this);
|
|
}
|
|
function onImageError(event) {
|
|
removeEventListeners();
|
|
if (abortSignal?.aborted) {
|
|
const e = new Error();
|
|
e.name = "AbortError";
|
|
e.message = "The operation was aborted.";
|
|
onError(e);
|
|
} else {
|
|
onError(event);
|
|
}
|
|
}
|
|
function onAbortSignal() {
|
|
image.src = "";
|
|
}
|
|
function removeEventListeners() {
|
|
image.removeEventListener("load", onImageLoad, false);
|
|
image.removeEventListener("error", onImageError, false);
|
|
abortSignal?.removeEventListener("abort", onAbortSignal, false);
|
|
}
|
|
image.addEventListener("load", onImageLoad, false);
|
|
image.addEventListener("error", onImageError, false);
|
|
abortSignal?.addEventListener("abort", onAbortSignal, false);
|
|
if (!url.startsWith("data:") && this.crossOrigin !== void 0) {
|
|
image.crossOrigin = this.crossOrigin;
|
|
}
|
|
image.src = url;
|
|
return image;
|
|
}
|
|
};
|
|
|
|
// src/services/TextureLoader.ts
|
|
var TextureLoader = class extends AbstractService {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer) {
|
|
super(viewer);
|
|
this.abortCtrl = {};
|
|
this.fileLoader = new BlobLoader();
|
|
this.imageLoader = new ImageLoader();
|
|
if (this.config.withCredentials) {
|
|
this.fileLoader.setWithCredentials(true);
|
|
this.imageLoader.setCrossOrigin("use-credentials");
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
destroy() {
|
|
this.abortLoading();
|
|
super.destroy();
|
|
}
|
|
/**
|
|
* Cancels current HTTP requests
|
|
* @internal
|
|
*/
|
|
abortLoading() {
|
|
Object.values(this.abortCtrl).forEach((ctrl) => ctrl.abort());
|
|
this.abortCtrl = {};
|
|
}
|
|
/**
|
|
* Loads a Blob with FileLoader
|
|
*/
|
|
loadFile(url, onProgress, cacheKey) {
|
|
const cached = Cache.get(url, cacheKey);
|
|
if (cached) {
|
|
if (cached instanceof Blob) {
|
|
onProgress?.(100);
|
|
return Promise.resolve(cached);
|
|
} else {
|
|
Cache.remove(url, cacheKey);
|
|
}
|
|
}
|
|
if (this.config.requestHeaders) {
|
|
this.fileLoader.setRequestHeader(this.config.requestHeaders(url));
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
let progress = 0;
|
|
onProgress?.(progress);
|
|
this.fileLoader.load(
|
|
url,
|
|
(result) => {
|
|
progress = 100;
|
|
onProgress?.(progress);
|
|
Cache.add(url, cacheKey, result);
|
|
resolve(result);
|
|
},
|
|
(e) => {
|
|
if (e.lengthComputable) {
|
|
const newProgress = e.loaded / e.total * 100;
|
|
if (newProgress > progress) {
|
|
progress = newProgress;
|
|
onProgress?.(progress);
|
|
}
|
|
}
|
|
},
|
|
(err) => {
|
|
reject(err);
|
|
},
|
|
this.__getAbortSignal(cacheKey)
|
|
);
|
|
});
|
|
}
|
|
/**
|
|
* Loads an image with ImageLoader or with FileLoader if progress is tracked or if request headers are configured
|
|
*/
|
|
loadImage(url, onProgress, cacheKey) {
|
|
const cached = Cache.get(url, cacheKey);
|
|
if (cached) {
|
|
onProgress?.(100);
|
|
if (cached instanceof Blob) {
|
|
return this.blobToImage(cached);
|
|
} else {
|
|
return Promise.resolve(cached);
|
|
}
|
|
}
|
|
if (!onProgress && !this.config.requestHeaders) {
|
|
return new Promise((resolve, reject) => {
|
|
this.imageLoader.load(
|
|
url,
|
|
(result) => {
|
|
Cache.add(url, cacheKey, result);
|
|
resolve(result);
|
|
},
|
|
(err) => {
|
|
reject(err);
|
|
},
|
|
this.__getAbortSignal(cacheKey)
|
|
);
|
|
});
|
|
} else {
|
|
return this.loadFile(url, onProgress, cacheKey).then((blob) => this.blobToImage(blob));
|
|
}
|
|
}
|
|
/**
|
|
* Converts a file loaded with {@link loadFile} into an image
|
|
*/
|
|
blobToImage(blob) {
|
|
return new Promise((resolve, reject) => {
|
|
const img = document.createElement("img");
|
|
img.onload = () => {
|
|
URL.revokeObjectURL(img.src);
|
|
resolve(img);
|
|
};
|
|
img.onerror = reject;
|
|
img.src = URL.createObjectURL(blob);
|
|
});
|
|
}
|
|
/**
|
|
* Preload a panorama file without displaying it
|
|
*/
|
|
preloadPanorama(panorama) {
|
|
if (this.viewer.adapter.supportsPreload(panorama)) {
|
|
return this.viewer.adapter.loadTexture(panorama, false);
|
|
} else {
|
|
return Promise.reject(new PSVError("Current adapter does not support preload"));
|
|
}
|
|
}
|
|
/**
|
|
* Get an abort signal
|
|
* the signal is shared accross all requests with the same cache key (for tiles adapters)
|
|
*/
|
|
__getAbortSignal(cacheKey) {
|
|
if (cacheKey) {
|
|
if (this.abortCtrl[cacheKey]?.signal.aborted) {
|
|
delete this.abortCtrl[cacheKey];
|
|
}
|
|
if (!this.abortCtrl[cacheKey]) {
|
|
this.abortCtrl[cacheKey] = new AbortController();
|
|
}
|
|
return this.abortCtrl[cacheKey].signal;
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// src/services/ViewerDynamics.ts
|
|
var import_three11 = require_three();
|
|
var ViewerDynamics = class extends AbstractService {
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(viewer) {
|
|
super(viewer);
|
|
this.zoom = new Dynamic(
|
|
(zoomLevel) => {
|
|
this.viewer.state.vFov = this.viewer.dataHelper.zoomLevelToFov(zoomLevel);
|
|
this.viewer.state.hFov = this.viewer.dataHelper.vFovToHFov(this.viewer.state.vFov);
|
|
this.viewer.dispatchEvent(new ZoomUpdatedEvent(zoomLevel));
|
|
},
|
|
{
|
|
defaultValue: this.viewer.config.defaultZoomLvl,
|
|
min: 0,
|
|
max: 100,
|
|
wrap: false
|
|
}
|
|
);
|
|
this.position = new MultiDynamic(
|
|
(position) => {
|
|
this.viewer.dataHelper.sphericalCoordsToVector3(position, this.viewer.state.direction);
|
|
this.viewer.dispatchEvent(new PositionUpdatedEvent(position));
|
|
},
|
|
{
|
|
yaw: new Dynamic(null, {
|
|
defaultValue: this.config.defaultYaw,
|
|
min: 0,
|
|
max: 2 * Math.PI,
|
|
wrap: true
|
|
}),
|
|
pitch: new Dynamic(null, {
|
|
defaultValue: this.config.defaultPitch,
|
|
min: !this.viewer.state.littlePlanet ? -Math.PI / 2 : 0,
|
|
max: !this.viewer.state.littlePlanet ? Math.PI / 2 : Math.PI * 2,
|
|
wrap: this.viewer.state.littlePlanet
|
|
})
|
|
}
|
|
);
|
|
this.roll = new Dynamic(
|
|
(roll) => {
|
|
this.viewer.state.roll = roll;
|
|
this.viewer.dispatchEvent(new RollUpdatedEvent(roll));
|
|
},
|
|
{
|
|
defaultValue: 0,
|
|
min: -Math.PI,
|
|
max: Math.PI,
|
|
wrap: false
|
|
}
|
|
);
|
|
this.updateSpeeds();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
updateSpeeds() {
|
|
this.zoom.setSpeed(this.config.zoomSpeed * 50);
|
|
this.position.setSpeed(import_three11.MathUtils.degToRad(this.config.moveSpeed * 50));
|
|
this.roll.setSpeed(import_three11.MathUtils.degToRad(this.config.moveSpeed * 50));
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
update(elapsed) {
|
|
this.zoom.update(elapsed);
|
|
this.position.update(elapsed);
|
|
this.roll.update(elapsed);
|
|
}
|
|
};
|
|
|
|
// src/services/ViewerState.ts
|
|
var import_three12 = require_three();
|
|
var ViewerState = class {
|
|
/**
|
|
* @internal
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
constructor() {
|
|
/**
|
|
* when all components are loaded
|
|
*/
|
|
this.ready = false;
|
|
/**
|
|
* if the view needs to be renderer
|
|
*/
|
|
this.needsUpdate = false;
|
|
/**
|
|
* number of plugins requesting to continuously render the scene
|
|
*/
|
|
this.continuousUpdateCount = 0;
|
|
/**
|
|
* if the keyboard events are currently listened to
|
|
*/
|
|
this.keyboardEnabled = false;
|
|
/**
|
|
* direction of the camera
|
|
*/
|
|
this.direction = new import_three12.Vector3(0, 0, SPHERE_RADIUS);
|
|
/**
|
|
* current camera roll
|
|
*/
|
|
this.roll = 0;
|
|
/**
|
|
* vertical FOV
|
|
*/
|
|
this.vFov = 60;
|
|
/**
|
|
* horizontal FOV
|
|
*/
|
|
this.hFov = 60;
|
|
/**
|
|
* renderer aspect ratio
|
|
*/
|
|
this.aspect = 1;
|
|
/**
|
|
* currently running animation
|
|
*/
|
|
this.animation = null;
|
|
/**
|
|
* currently running transition
|
|
*/
|
|
this.transitionAnimation = null;
|
|
/**
|
|
* promise of the last "setPanorama()" call
|
|
*/
|
|
this.loadingPromise = null;
|
|
/**
|
|
* special tweaks for LittlePlanetAdapter
|
|
*/
|
|
this.littlePlanet = false;
|
|
/**
|
|
* time of the last user action
|
|
*/
|
|
this.idleTime = -1;
|
|
/**
|
|
* registered THREE objects observer
|
|
*/
|
|
this.objectsObservers = {};
|
|
/**
|
|
* size of the container
|
|
*/
|
|
this.size = {
|
|
width: 0,
|
|
height: 0
|
|
};
|
|
}
|
|
};
|
|
|
|
// src/Viewer.ts
|
|
var Viewer = class extends TypedEventTarget {
|
|
constructor(config) {
|
|
super();
|
|
/** @internal */
|
|
this.plugins = {};
|
|
/** @internal */
|
|
this.children = [];
|
|
this.onResize = throttle(() => this.navbar.autoSize(), 500);
|
|
this.parent = getElement(config.container);
|
|
if (!this.parent) {
|
|
throw new PSVError(`"container" element not found.`);
|
|
}
|
|
this.parent[VIEWER_DATA] = this;
|
|
this.container = document.createElement("div");
|
|
this.container.classList.add("psv-container");
|
|
this.parent.appendChild(this.container);
|
|
checkStylesheet(this.container, "core");
|
|
this.state = new ViewerState();
|
|
this.config = getViewerConfig(config);
|
|
this.__setSize(this.config.size);
|
|
this.overlay = new Overlay(this);
|
|
try {
|
|
SYSTEM.load();
|
|
} catch (err) {
|
|
console.error(err);
|
|
this.showError(this.config.lang.webglError);
|
|
return;
|
|
}
|
|
Cache.init();
|
|
this.adapter = new this.config.adapter[0](this, this.config.adapter[1]);
|
|
this.renderer = new Renderer(this);
|
|
this.textureLoader = new TextureLoader(this);
|
|
this.eventsHandler = new EventsHandler(this);
|
|
this.dataHelper = new DataHelper(this);
|
|
this.dynamics = new ViewerDynamics(this);
|
|
this.adapter.init?.();
|
|
this.loader = new Loader(this);
|
|
this.navbar = new Navbar(this);
|
|
this.panel = new Panel(this);
|
|
this.notification = new Notification(this);
|
|
this.autoSize();
|
|
this.setCursor(null);
|
|
resolveBoolean(SYSTEM.isTouchEnabled, (enabled) => {
|
|
toggleClass(this.container, "psv--is-touch", enabled);
|
|
});
|
|
this.config.plugins.forEach(([plugin, opts]) => {
|
|
this.plugins[plugin.id] = new plugin(this, opts);
|
|
});
|
|
for (const plugin of Object.values(this.plugins)) {
|
|
plugin.init?.();
|
|
}
|
|
if (this.config.navbar) {
|
|
this.navbar.setButtons(this.config.navbar);
|
|
}
|
|
if (!this.state.loadingPromise) {
|
|
if (this.config.panorama) {
|
|
this.setPanorama(this.config.panorama);
|
|
} else {
|
|
this.loader.show();
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Destroys the viewer
|
|
*/
|
|
destroy() {
|
|
this.stopAll();
|
|
this.stopKeyboardControl();
|
|
this.exitFullscreen();
|
|
for (const [id, plugin] of Object.entries(this.plugins)) {
|
|
plugin.destroy();
|
|
delete this.plugins[id];
|
|
}
|
|
this.children.slice().forEach((child) => child.destroy());
|
|
this.children.length = 0;
|
|
this.eventsHandler?.destroy();
|
|
this.renderer?.destroy();
|
|
this.textureLoader?.destroy();
|
|
this.dataHelper?.destroy();
|
|
this.adapter?.destroy();
|
|
this.dynamics?.destroy();
|
|
this.parent.removeChild(this.container);
|
|
delete this.parent[VIEWER_DATA];
|
|
}
|
|
init() {
|
|
this.eventsHandler.init();
|
|
this.renderer.init();
|
|
if (this.config.navbar) {
|
|
this.navbar.show();
|
|
}
|
|
if (this.config.keyboard === "always") {
|
|
this.startKeyboardControl();
|
|
}
|
|
this.resetIdleTimer();
|
|
this.state.ready = true;
|
|
this.dispatchEvent(new ReadyEvent());
|
|
}
|
|
/**
|
|
* Restarts the idle timer
|
|
* @internal
|
|
*/
|
|
resetIdleTimer() {
|
|
this.state.idleTime = performance.now();
|
|
}
|
|
/**
|
|
* Stops the idle timer
|
|
* @internal
|
|
*/
|
|
disableIdleTimer() {
|
|
this.state.idleTime = -1;
|
|
}
|
|
/**
|
|
* Returns the instance of a plugin if it exists
|
|
* @example By plugin identifier
|
|
* ```js
|
|
* viewer.getPlugin('markers')
|
|
* ```
|
|
* @example By plugin class with TypeScript support
|
|
* ```ts
|
|
* viewer.getPlugin<MarkersPlugin>(MarkersPlugin)
|
|
* ```
|
|
*/
|
|
getPlugin(pluginId) {
|
|
if (typeof pluginId === "string") {
|
|
return this.plugins[pluginId];
|
|
} else {
|
|
const pluginCtor = pluginInterop(pluginId);
|
|
return pluginCtor ? this.plugins[pluginCtor.id] : null;
|
|
}
|
|
}
|
|
/**
|
|
* Returns the current position of the camera
|
|
*/
|
|
getPosition() {
|
|
return this.dataHelper.cleanPosition(this.dynamics.position.current);
|
|
}
|
|
/**
|
|
* Returns the current zoom level
|
|
*/
|
|
getZoomLevel() {
|
|
return this.dynamics.zoom.current;
|
|
}
|
|
/**
|
|
* Returns the current viewer size
|
|
*/
|
|
getSize() {
|
|
return { ...this.state.size };
|
|
}
|
|
/**
|
|
* Checks if the viewer is in fullscreen
|
|
*/
|
|
isFullscreenEnabled() {
|
|
return isFullscreenEnabled(this.parent);
|
|
}
|
|
/**
|
|
* Request a new render of the scene
|
|
*/
|
|
needsUpdate() {
|
|
this.state.needsUpdate = true;
|
|
}
|
|
/**
|
|
* Request the scene to be continuously renderer (when using videos)
|
|
*/
|
|
needsContinuousUpdate(enabled) {
|
|
if (enabled) {
|
|
this.state.continuousUpdateCount++;
|
|
} else if (this.state.continuousUpdateCount > 0) {
|
|
this.state.continuousUpdateCount--;
|
|
}
|
|
}
|
|
/**
|
|
* Resizes the scene if the viewer is resized
|
|
*/
|
|
autoSize() {
|
|
if (this.container.clientWidth !== this.state.size.width || this.container.clientHeight !== this.state.size.height) {
|
|
this.state.size.width = Math.round(this.container.clientWidth);
|
|
this.state.size.height = Math.round(this.container.clientHeight);
|
|
this.state.aspect = this.state.size.width / this.state.size.height;
|
|
this.state.hFov = this.dataHelper.vFovToHFov(this.state.vFov);
|
|
this.dispatchEvent(new SizeUpdatedEvent(this.getSize()));
|
|
this.onResize();
|
|
}
|
|
}
|
|
/**
|
|
* Loads a new panorama file
|
|
* @description Loads a new panorama file, optionally changing the camera position/zoom and activating the transition animation.<br>
|
|
* If the "options" parameter is not defined, the camera will not move and the ongoing animation will continue.<br>
|
|
* If another loading is already in progress it will be aborted.
|
|
* @returns promise resolved with false if the loading was aborted by another call
|
|
*/
|
|
setPanorama(path, options = {}) {
|
|
this.textureLoader.abortLoading();
|
|
this.state.transitionAnimation?.cancel();
|
|
if (!this.state.ready) {
|
|
["sphereCorrection", "panoData"].forEach((opt) => {
|
|
if (!(opt in options)) {
|
|
options[opt] = this.config[opt];
|
|
}
|
|
});
|
|
}
|
|
if (options.transition === void 0) {
|
|
options.transition = true;
|
|
}
|
|
if (options.speed === void 0) {
|
|
options.speed = DEFAULT_TRANSITION;
|
|
}
|
|
if (options.showLoader === void 0) {
|
|
options.showLoader = true;
|
|
}
|
|
if (options.caption === void 0) {
|
|
options.caption = this.config.caption;
|
|
}
|
|
if (options.description === void 0) {
|
|
options.description = this.config.description;
|
|
}
|
|
if (!options.panoData && typeof this.config.panoData === "function") {
|
|
options.panoData = this.config.panoData;
|
|
}
|
|
const positionProvided = !isNil(options.position);
|
|
const zoomProvided = !isNil(options.zoom);
|
|
if (positionProvided || zoomProvided) {
|
|
this.stopAll();
|
|
}
|
|
this.hideError();
|
|
this.resetIdleTimer();
|
|
this.config.panorama = path;
|
|
this.config.caption = options.caption;
|
|
this.config.description = options.description;
|
|
const done = (err) => {
|
|
if (isAbortError(err)) {
|
|
return false;
|
|
}
|
|
this.loader.hide();
|
|
this.state.loadingPromise = null;
|
|
if (err) {
|
|
this.navbar.setCaption("");
|
|
this.showError(this.config.lang.loadError);
|
|
console.error(err);
|
|
this.dispatchEvent(new PanoramaErrorEvent(path, err));
|
|
throw err;
|
|
} else {
|
|
this.navbar.setCaption(this.config.caption);
|
|
return true;
|
|
}
|
|
};
|
|
this.navbar.setCaption(`<em>${this.config.lang.loading}</em>`);
|
|
if (options.showLoader || !this.state.ready) {
|
|
this.loader.show();
|
|
}
|
|
this.dispatchEvent(new PanoramaLoadEvent(path));
|
|
const loadingPromise = this.adapter.loadTexture(this.config.panorama, true, options.panoData).then((textureData) => {
|
|
if (textureData.panorama !== this.config.panorama) {
|
|
this.adapter.disposeTexture(textureData);
|
|
throw getAbortError();
|
|
}
|
|
return textureData;
|
|
});
|
|
if (!options.transition || !this.state.ready || !this.adapter.supportsTransition(this.config.panorama)) {
|
|
this.state.loadingPromise = loadingPromise.then((textureData) => {
|
|
this.renderer.show();
|
|
this.renderer.setTexture(textureData);
|
|
this.renderer.setPanoramaPose(textureData.panoData);
|
|
this.renderer.setSphereCorrection(options.sphereCorrection);
|
|
if (!this.state.ready) {
|
|
this.init();
|
|
}
|
|
this.dispatchEvent(new PanoramaLoadedEvent(textureData));
|
|
if (zoomProvided) {
|
|
this.zoom(options.zoom);
|
|
}
|
|
if (positionProvided) {
|
|
this.rotate(options.position);
|
|
}
|
|
}).then(
|
|
() => done(),
|
|
(err) => done(err)
|
|
);
|
|
} else {
|
|
this.state.loadingPromise = loadingPromise.then((textureData) => {
|
|
this.loader.hide();
|
|
this.dispatchEvent(new PanoramaLoadedEvent(textureData));
|
|
this.state.transitionAnimation = this.renderer.transition(textureData, options);
|
|
return this.state.transitionAnimation;
|
|
}).then((completed) => {
|
|
this.state.transitionAnimation = null;
|
|
if (!completed) {
|
|
throw getAbortError();
|
|
}
|
|
}).then(
|
|
() => done(),
|
|
(err) => done(err)
|
|
);
|
|
}
|
|
return this.state.loadingPromise;
|
|
}
|
|
/**
|
|
* Update options
|
|
* @throws {@link PSVError} if the configuration is invalid
|
|
*/
|
|
setOptions(options) {
|
|
const rawConfig = {
|
|
...this.config,
|
|
...options
|
|
};
|
|
for (let [key, value] of Object.entries(options)) {
|
|
if (!(key in DEFAULTS)) {
|
|
logWarn(`Unknown option ${key}`);
|
|
continue;
|
|
}
|
|
if (key in READONLY_OPTIONS) {
|
|
logWarn(READONLY_OPTIONS[key]);
|
|
continue;
|
|
}
|
|
if (key in CONFIG_PARSERS) {
|
|
value = CONFIG_PARSERS[key](value, {
|
|
rawConfig,
|
|
defValue: DEFAULTS[key]
|
|
});
|
|
}
|
|
this.config[key] = value;
|
|
switch (key) {
|
|
case "mousemove":
|
|
if (!this.state.cursorOverride) {
|
|
this.setCursor(null);
|
|
}
|
|
break;
|
|
case "caption":
|
|
this.navbar.setCaption(this.config.caption);
|
|
break;
|
|
case "size":
|
|
this.resize(this.config.size);
|
|
break;
|
|
case "sphereCorrection":
|
|
this.renderer.setSphereCorrection(this.config.sphereCorrection);
|
|
break;
|
|
case "navbar":
|
|
case "lang":
|
|
this.navbar.setButtons(this.config.navbar);
|
|
break;
|
|
case "moveSpeed":
|
|
case "zoomSpeed":
|
|
this.dynamics.updateSpeeds();
|
|
break;
|
|
case "minFov":
|
|
case "maxFov":
|
|
this.dynamics.zoom.setValue(this.dataHelper.fovToZoomLevel(this.state.vFov));
|
|
this.dispatchEvent(new ZoomUpdatedEvent(this.getZoomLevel()));
|
|
break;
|
|
case "keyboard":
|
|
if (this.config.keyboard === "always") {
|
|
this.startKeyboardControl();
|
|
} else {
|
|
this.stopKeyboardControl();
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
this.needsUpdate();
|
|
this.dispatchEvent(new ConfigChangedEvent(Object.keys(options)));
|
|
}
|
|
/**
|
|
* Update options
|
|
* @throws {@link PSVError} if the configuration is invalid
|
|
*/
|
|
setOption(option, value) {
|
|
this.setOptions({ [option]: value });
|
|
}
|
|
/**
|
|
* Displays an error message over the viewer
|
|
*/
|
|
showError(message) {
|
|
this.overlay.show({
|
|
id: IDS.ERROR,
|
|
image: error_default,
|
|
title: message,
|
|
dissmisable: false
|
|
});
|
|
}
|
|
/**
|
|
* Hides the error message
|
|
*/
|
|
hideError() {
|
|
this.overlay.hide(IDS.ERROR);
|
|
}
|
|
/**
|
|
* Rotates the view to specific position
|
|
*/
|
|
rotate(position) {
|
|
const e = new BeforeRotateEvent(this.dataHelper.cleanPosition(position));
|
|
this.dispatchEvent(e);
|
|
if (e.defaultPrevented) {
|
|
return;
|
|
}
|
|
this.dynamics.position.setValue(e.position);
|
|
}
|
|
/**
|
|
* Zooms to a specific level between `maxFov` and `minFov`
|
|
*/
|
|
zoom(level) {
|
|
this.dynamics.zoom.setValue(level);
|
|
}
|
|
/**
|
|
* Increases the zoom level
|
|
*/
|
|
zoomIn(step = 1) {
|
|
this.dynamics.zoom.step(step);
|
|
}
|
|
/**
|
|
* Decreases the zoom level
|
|
*/
|
|
zoomOut(step = 1) {
|
|
this.dynamics.zoom.step(-step);
|
|
}
|
|
/**
|
|
* Rotates and zooms the view with a smooth animation
|
|
*/
|
|
animate(options) {
|
|
const positionProvided = isExtendedPosition(options);
|
|
const zoomProvided = !isNil(options.zoom);
|
|
const e = new BeforeAnimateEvent(
|
|
positionProvided ? this.dataHelper.cleanPosition(options) : void 0,
|
|
options.zoom
|
|
);
|
|
this.dispatchEvent(e);
|
|
if (e.defaultPrevented) {
|
|
return;
|
|
}
|
|
this.stopAll();
|
|
const { duration, properties } = this.dataHelper.getAnimationProperties(options.speed, e.position, e.zoomLevel);
|
|
if (!duration) {
|
|
if (positionProvided) {
|
|
this.rotate(e.position);
|
|
}
|
|
if (zoomProvided) {
|
|
this.zoom(e.zoomLevel);
|
|
}
|
|
return new Animation(null);
|
|
}
|
|
this.state.animation = new Animation({
|
|
properties,
|
|
duration,
|
|
easing: options.easing || "inOutSine",
|
|
onTick: (props) => {
|
|
if (positionProvided) {
|
|
this.dynamics.position.setValue({
|
|
yaw: props.yaw,
|
|
pitch: props.pitch
|
|
});
|
|
}
|
|
if (zoomProvided) {
|
|
this.dynamics.zoom.setValue(props.zoom);
|
|
}
|
|
}
|
|
});
|
|
this.state.animation.then(() => {
|
|
this.state.animation = null;
|
|
this.resetIdleTimer();
|
|
});
|
|
return this.state.animation;
|
|
}
|
|
/**
|
|
* Stops the ongoing animation
|
|
* @description The return value is a Promise because the is no guaranty the animation can be stopped synchronously.
|
|
*/
|
|
stopAnimation() {
|
|
if (this.state.animation) {
|
|
this.state.animation.cancel();
|
|
return this.state.animation;
|
|
} else {
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
/**
|
|
* Resizes the viewer
|
|
*/
|
|
resize(size) {
|
|
this.__setSize(size);
|
|
this.autoSize();
|
|
}
|
|
__setSize(size) {
|
|
const s = size;
|
|
["width", "height"].forEach((dim) => {
|
|
if (size && s[dim]) {
|
|
if (/^[0-9.]+$/.test(s[dim])) {
|
|
s[dim] += "px";
|
|
}
|
|
this.parent.style[dim] = s[dim];
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Enters the fullscreen mode
|
|
*/
|
|
enterFullscreen() {
|
|
if (!this.isFullscreenEnabled()) {
|
|
requestFullscreen(this.parent);
|
|
}
|
|
}
|
|
/**
|
|
* Exits the fullscreen mode
|
|
*/
|
|
exitFullscreen() {
|
|
if (this.isFullscreenEnabled()) {
|
|
exitFullscreen();
|
|
}
|
|
}
|
|
/**
|
|
* Enters or exits the fullscreen mode
|
|
*/
|
|
toggleFullscreen() {
|
|
if (!this.isFullscreenEnabled()) {
|
|
this.enterFullscreen();
|
|
} else {
|
|
this.exitFullscreen();
|
|
}
|
|
}
|
|
/**
|
|
* Enables the keyboard controls
|
|
*/
|
|
startKeyboardControl() {
|
|
this.state.keyboardEnabled = true;
|
|
}
|
|
/**
|
|
* Disables the keyboard controls
|
|
*/
|
|
stopKeyboardControl() {
|
|
this.state.keyboardEnabled = false;
|
|
}
|
|
/**
|
|
* Creates a new tooltip
|
|
* @description Use {@link Tooltip.move} to update the tooltip without re-create
|
|
* @throws {@link PSVError} if the configuration is invalid
|
|
*/
|
|
createTooltip(config) {
|
|
return new Tooltip(this, config);
|
|
}
|
|
/**
|
|
* Changes the global mouse cursor
|
|
*/
|
|
setCursor(cursor) {
|
|
this.state.cursorOverride = cursor;
|
|
if (!cursor) {
|
|
this.container.style.cursor = this.config.mousemove ? "move" : "default";
|
|
} else {
|
|
this.container.style.cursor = cursor;
|
|
}
|
|
}
|
|
/**
|
|
* Subscribes to events on objects in the three.js scene
|
|
* @param userDataKey - only objects with the following `userData` will be observed
|
|
*/
|
|
observeObjects(userDataKey) {
|
|
if (!this.state.objectsObservers[userDataKey]) {
|
|
this.state.objectsObservers[userDataKey] = null;
|
|
}
|
|
}
|
|
/**
|
|
* Unsubscribes to events on objects
|
|
*/
|
|
unobserveObjects(userDataKey) {
|
|
delete this.state.objectsObservers[userDataKey];
|
|
}
|
|
/**
|
|
* Stops all current animations
|
|
* @internal
|
|
*/
|
|
stopAll() {
|
|
this.dispatchEvent(new StopAllEvent());
|
|
this.disableIdleTimer();
|
|
return this.stopAnimation();
|
|
}
|
|
};
|
|
|
|
// src/index.ts
|
|
var VERSION = "5.7.3";
|
|
__copyProps(__defProp(exports, "__esModule", { value: true }), src_exports);
|
|
|
|
}));//# sourceMappingURL=index.js.map
|