viewer_sgos / interface.js
MikaFil's picture
Update interface.js
eaace97 verified
raw
history blame
17.8 kB
// interface.js
// ==============================
// Garde global: si ce module a déjà initialisé toutes les balises, on ne refait pas
if (!window.__PLY_IFACE_INIT_DONE__) {
window.__PLY_IFACE_INIT_DONE__ = true;
(async function () {
// Trouve toutes les balises <script src=".../interface.js" data-config="...">
const scriptTags = Array.from(
document.querySelectorAll('script[type="module"][src*="interface.js"][data-config]')
);
if (!scriptTags.length) return;
// Initialise une instance par balise
for (const scriptTag of scriptTags) {
try {
await initViewerInstance(scriptTag);
} catch (e) {
// Évite qu'une erreur d'instance empêche les autres de se créer
console.error("[interface.js] Instance init error:", e);
}
}
})();
}
async function initViewerInstance(scriptTag) {
// 1) Lire la config
const configUrl = scriptTag.getAttribute("data-config");
if (!configUrl) return;
let config = {};
try {
const response = await fetch(configUrl, { credentials: "omit", cache: "no-store" });
config = await response.json();
} catch (error) {
console.error("[interface.js] Failed to fetch config:", error);
return;
}
// 2) Injecter la CSS si nécessaire (une seule fois par href)
try {
if (
config.css_url &&
!document.querySelector(
`link[rel="stylesheet"][href="${(window.CSS && CSS.escape) ? CSS.escape(config.css_url) : config.css_url}"]`
)
) {
const linkEl = document.createElement("link");
linkEl.rel = "stylesheet";
linkEl.href = config.css_url;
document.head.appendChild(linkEl);
}
} catch (e) {
// si CSS.escape indisponible, on ignore la vérif et on insère quand même
if (config.css_url && !document.querySelector(`link[rel="stylesheet"][href="${config.css_url}"]`)) {
const linkEl = document.createElement("link");
linkEl.rel = "stylesheet";
linkEl.href = config.css_url;
document.head.appendChild(linkEl);
}
}
// 3) ID unique pour l’instance
const instanceId = Math.random().toString(36).substr(2, 8);
// 4) Aspect ratio
let aspectPercent = "100%";
if (config.aspect) {
if (config.aspect.includes(":")) {
const parts = config.aspect.split(":");
const w = parseFloat(parts[0]);
const h = parseFloat(parts[1]);
if (!isNaN(w) && !isNaN(h) && w > 0) {
aspectPercent = (h / w) * 100 + "%";
}
} else {
const aspectValue = parseFloat(config.aspect);
if (!isNaN(aspectValue) && aspectValue > 0) {
aspectPercent = (100 / aspectValue) + "%";
}
}
} else {
const parentContainer = scriptTag.parentNode;
const containerWidth = parentContainer.offsetWidth;
const containerHeight = parentContainer.offsetHeight;
if (containerWidth > 0 && containerHeight > 0) {
aspectPercent = (containerHeight / containerWidth) * 100 + "%";
}
}
// 5) Créer le container du widget
const widgetContainer = document.createElement("div");
widgetContainer.id = "ply-widget-container-" + instanceId;
widgetContainer.classList.add("ply-widget-container");
widgetContainer.style.height = "0";
widgetContainer.style.paddingBottom = aspectPercent;
widgetContainer.setAttribute("data-original-aspect", aspectPercent);
const tooltipsButtonHTML = config.tooltips_url
? `<button id="tooltips-toggle-${instanceId}" class="widget-button tooltips-toggle">⦿</button>`
: "";
// NB: on rend le tooltip-panel unique par instance
widgetContainer.innerHTML = `
<div id="viewer-container-${instanceId}" class="viewer-container">
<div id="progress-dialog-${instanceId}" class="progress-dialog">
<progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
</div>
<button id="fullscreen-toggle-${instanceId}" class="widget-button fullscreen-toggle">⇱</button>
<button id="help-toggle-${instanceId}" class="widget-button help-toggle">?</button>
<button id="reset-camera-btn-${instanceId}" class="widget-button reset-camera-btn">
<span class="reset-icon">⟲</span>
</button>
${tooltipsButtonHTML}
<div id="menu-content-${instanceId}" class="menu-content">
<span id="help-close-${instanceId}" class="help-close">×</span>
<div class="help-text"></div>
</div>
</div>
<div id="tooltip-panel-${instanceId}" class="tooltip-panel" style="display: none;">
<div class="tooltip-content">
<span id="tooltip-close-${instanceId}" class="tooltip-close">×</span>
<div id="tooltip-text-${instanceId}" class="tooltip-text"></div>
<img id="tooltip-image-${instanceId}" class="tooltip-image" src="" alt="" style="display: none;" />
</div>
</div>
`;
// Insérer juste après la balise script courante
scriptTag.parentNode.appendChild(widgetContainer);
// 6) Références DOM pour cette instance
const viewerContainerElem = document.getElementById("viewer-container-" + instanceId);
const fullscreenToggle = document.getElementById("fullscreen-toggle-" + instanceId);
const helpToggle = document.getElementById("help-toggle-" + instanceId);
const helpCloseBtn = document.getElementById("help-close-" + instanceId);
const resetCameraBtn = document.getElementById("reset-camera-btn-" + instanceId);
const tooltipsToggleBtn = document.getElementById("tooltips-toggle-" + instanceId);
const menuContent = document.getElementById("menu-content-" + instanceId);
const helpTextDiv = menuContent.querySelector(".help-text");
const tooltipPanel = document.getElementById("tooltip-panel-" + instanceId);
const tooltipTextDiv = document.getElementById("tooltip-text-" + instanceId);
const tooltipImage = document.getElementById("tooltip-image-" + instanceId);
const tooltipCloseBtn = document.getElementById("tooltip-close-" + instanceId);
// Détecteurs (déclarés UNE SEULE FOIS)
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const isMobile = isIOS || /Android/i.test(navigator.userAgent);
const tooltipInstruction = config.tooltips_url
? "- Cliquez sur ⦿ pour afficher/masquer les tooltips.<br>"
: "";
if (isMobile) {
helpTextDiv.innerHTML =
"- Déplacez vous en glissant deux doigts sur l'écran.<br>" +
"- Orbitez en glissant un doigt.<br>" +
"- Zoomez en pinçant avec deux doigts.<br>" +
tooltipInstruction +
"- Cliquez sur ⟲ pour réinitialiser la caméra.<br>" +
"- Cliquez sur ⇱ pour passer en plein écran.<br>";
} else {
helpTextDiv.innerHTML =
"- Orbitez avec le clic droit ou shift + ←↑↓→<br>" +
"- Zoomez avec la molette ou ctrl + ↑↓<br>" +
"- Déplacez vous avec le clic gauche ou ←↑↓→<br>" +
tooltipInstruction +
"- Cliquez sur ⟲ pour réinitialiser la caméra.<br>" +
"- Cliquez sur ⇱ pour passer en plein écran.<br>";
}
// --- DYNAMIC MENU SIZING ---
function setMenuContentMaxSize() {
if (!isMobile) {
menuContent.style.maxWidth = "";
menuContent.style.maxHeight = "";
menuContent.style.width = "";
menuContent.style.height = "";
menuContent.style.overflowY = "";
menuContent.style.overflowX = "";
return;
}
const parent = viewerContainerElem;
if (parent) {
const vw = parent.offsetWidth;
const vh = parent.offsetHeight;
if (vw && vh) {
menuContent.style.maxWidth = Math.round(vw * 0.8) + "px";
menuContent.style.maxHeight = Math.round(vh * 0.8) + "px";
menuContent.style.width = "";
menuContent.style.height = "";
menuContent.style.overflowY = "auto";
menuContent.style.overflowX = "auto";
} else {
menuContent.style.maxWidth = "80vw";
menuContent.style.maxHeight = "80vh";
menuContent.style.overflowY = "auto";
menuContent.style.overflowX = "auto";
}
}
}
setMenuContentMaxSize();
window.addEventListener("resize", setMenuContentMaxSize);
document.addEventListener("fullscreenchange", setMenuContentMaxSize);
window.addEventListener("orientationchange", setMenuContentMaxSize);
// --- HELP PANEL DEFAULT VISIBILITY ---
menuContent.style.display = "block";
viewerContainerElem.style.display = "block";
// 7) Charger viewer.js avec un paramètre unique (évite le cache ESM)
// -> chaque instance obtient son propre module (et son propre état).
let viewerModule;
try {
const viewerUrl = `https://mikafil-viewer-sgos.static.hf.space/viewer.js?inst=${instanceId}`;
viewerModule = await import(viewerUrl);
await viewerModule.initializeViewer(config, instanceId);
} catch (err) {
console.error("[interface.js] viewer.js load/init error:", err);
return;
}
const canvasId = "canvas-" + instanceId;
const canvasEl = document.getElementById(canvasId);
// 8) Bouton tooltips : cacher si URL non valide
if (tooltipsToggleBtn) {
if (!config.tooltips_url) {
tooltipsToggleBtn.style.display = "none";
} else {
fetch(config.tooltips_url)
.then((resp) => {
if (!resp.ok) tooltipsToggleBtn.style.display = "none";
})
.catch(() => {
tooltipsToggleBtn.style.display = "none";
});
}
}
// 9) Panneau tooltips & interactions locales
let dragHide = null;
function hideTooltipPanel() {
if (dragHide) {
viewerContainerElem.removeEventListener("pointermove", dragHide);
dragHide = null;
}
tooltipPanel.style.display = "none";
}
function hideHelpPanel() {
menuContent.style.display = "none";
}
if (canvasEl) {
canvasEl.addEventListener("wheel", hideTooltipPanel, { passive: true });
}
// NB : l’événement 'tooltip-selected' est global (tooltips.js l’émet sur document).
// Toutes les instances l’écoutent. Pour un découplage fin, il faudrait faire
// passer un identifiant d’instance dans detail (modif. de tooltips.js).
document.addEventListener("tooltip-selected", (evt) => {
// Affiche le panneau, annule un hide différé si présent
if (dragHide) {
viewerContainerElem.removeEventListener("pointermove", dragHide);
dragHide = null;
}
const { title, description, imgUrl } = evt.detail || {};
tooltipTextDiv.innerHTML = `<strong>${title || ""}</strong><br>${description || ""}`;
tooltipImage.style.display = "none";
tooltipImage.src = "";
if (imgUrl) {
tooltipImage.onload = () => {
tooltipImage.style.display = "block";
};
tooltipImage.src = imgUrl;
}
tooltipPanel.style.display = "flex";
// Fermer en cas de drag (après un petit délai pour éviter un flicker)
setTimeout(() => {
dragHide = (e) => {
if ((e.pointerType === "mouse" && e.buttons !== 0) || e.pointerType === "touch") {
hideTooltipPanel();
}
};
viewerContainerElem.addEventListener("pointermove", dragHide);
}, 100);
});
let isFullscreen = false;
let savedState = null;
function saveCurrentState() {
if (isFullscreen) return;
const originalAspect = widgetContainer.getAttribute("data-original-aspect") || aspectPercent;
savedState = {
widget: {
position: widgetContainer.style.position,
top: widgetContainer.style.top,
left: widgetContainer.style.left,
width: widgetContainer.style.width,
height: widgetContainer.style.height,
maxWidth: widgetContainer.style.maxWidth,
maxHeight: widgetContainer.style.maxHeight,
paddingBottom: widgetContainer.style.paddingBottom || originalAspect,
margin: widgetContainer.style.margin
},
viewer: {
borderRadius: viewerContainerElem.style.borderRadius,
border: viewerContainerElem.style.border
}
};
}
function restoreOriginalStyles() {
if (!savedState) return;
const aspectToUse = savedState.widget.paddingBottom;
widgetContainer.style.position = savedState.widget.position || "";
widgetContainer.style.top = savedState.widget.top || "";
widgetContainer.style.left = savedState.widget.left || "";
widgetContainer.style.width = "100%";
widgetContainer.style.height = "0";
widgetContainer.style.maxWidth = savedState.widget.maxWidth || "";
widgetContainer.style.maxHeight = savedState.widget.maxHeight || "";
widgetContainer.style.paddingBottom = aspectToUse;
widgetContainer.style.margin = savedState.widget.margin || "";
widgetContainer.classList.remove("fake-fullscreen");
viewerContainerElem.style.position = "absolute";
viewerContainerElem.style.top = "0";
viewerContainerElem.style.left = "0";
viewerContainerElem.style.right = "0";
viewerContainerElem.style.bottom = "0";
viewerContainerElem.style.width = "100%";
viewerContainerElem.style.height = "100%";
viewerContainerElem.style.borderRadius = savedState.viewer.borderRadius || "";
viewerContainerElem.style.border = savedState.viewer.border || "";
if (viewerModule.app) {
viewerModule.app.resizeCanvas(
viewerContainerElem.clientWidth,
viewerContainerElem.clientHeight
);
}
if (fullscreenToggle) fullscreenToggle.textContent = "⇱";
savedState = null;
setMenuContentMaxSize();
}
function applyFullscreenStyles() {
widgetContainer.style.position = "fixed";
widgetContainer.style.top = "0";
widgetContainer.style.left = "0";
widgetContainer.style.width = "100vw";
widgetContainer.style.height = "100vh";
widgetContainer.style.maxWidth = "100vw";
widgetContainer.style.maxHeight = "100vh";
widgetContainer.style.paddingBottom = "0";
widgetContainer.style.margin = "0";
widgetContainer.style.border = "none";
widgetContainer.style.borderRadius = "0";
viewerContainerElem.style.width = "100%";
viewerContainerElem.style.height = "100%";
viewerContainerElem.style.borderRadius = "0";
viewerContainerElem.style.border = "none";
if (viewerModule.app) {
viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
}
if (fullscreenToggle) fullscreenToggle.textContent = "⇲";
isFullscreen = true;
setMenuContentMaxSize();
}
function enterFullscreen() {
if (!savedState) saveCurrentState();
if (isIOS) {
applyFullscreenStyles();
widgetContainer.classList.add("fake-fullscreen");
} else if (widgetContainer.requestFullscreen) {
widgetContainer
.requestFullscreen()
.then(applyFullscreenStyles)
.catch(() => {
applyFullscreenStyles();
widgetContainer.classList.add("fake-fullscreen");
});
} else {
applyFullscreenStyles();
widgetContainer.classList.add("fake-fullscreen");
}
}
function exitFullscreen() {
if (document.fullscreenElement === widgetContainer && document.exitFullscreen) {
document.exitFullscreen().catch(() => {});
}
widgetContainer.classList.remove("fake-fullscreen");
restoreOriginalStyles();
isFullscreen = false;
if (fullscreenToggle) fullscreenToggle.textContent = "⇱";
setMenuContentMaxSize();
}
fullscreenToggle.addEventListener("click", () => {
hideTooltipPanel();
isFullscreen ? exitFullscreen() : enterFullscreen();
});
document.addEventListener("fullscreenchange", () => {
if (!document.fullscreenElement && isFullscreen) {
isFullscreen = false;
restoreOriginalStyles();
if (fullscreenToggle) fullscreenToggle.textContent = "⇱";
} else if (document.fullscreenElement === widgetContainer) {
if (fullscreenToggle) fullscreenToggle.textContent = "⇲";
}
setMenuContentMaxSize();
});
helpToggle.addEventListener("click", (e) => {
hideTooltipPanel();
e.stopPropagation();
if (menuContent.style.display === "block") {
menuContent.style.display = "none";
} else {
menuContent.style.display = "block";
setMenuContentMaxSize();
}
});
helpCloseBtn.addEventListener("click", hideHelpPanel);
resetCameraBtn.addEventListener("click", () => {
hideTooltipPanel();
if (viewerModule.resetViewerCamera) {
viewerModule.resetViewerCamera();
}
});
if (tooltipsToggleBtn) {
let tooltipsVisible = !!config.showTooltipsDefault;
tooltipsToggleBtn.style.opacity = tooltipsVisible ? "1" : "0.5";
tooltipsToggleBtn.addEventListener("click", () => {
hideTooltipPanel();
tooltipsVisible = !tooltipsVisible;
tooltipsToggleBtn.style.opacity = tooltipsVisible ? "1" : "0.5";
document.dispatchEvent(
new CustomEvent("toggle-tooltips", { detail: { visible: tooltipsVisible } })
);
});
}
tooltipCloseBtn.addEventListener("click", hideTooltipPanel);
document.addEventListener("keydown", (e) => {
if ((e.key === "Escape" || e.key === "Esc") && isFullscreen) exitFullscreen();
});
window.addEventListener("resize", () => {
if (viewerModule.app) {
if (isFullscreen) {
viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
} else {
viewerModule.app.resizeCanvas(
viewerContainerElem.clientWidth,
viewerContainerElem.clientHeight
);
}
}
setMenuContentMaxSize();
});
// Init par défaut
setTimeout(() => {
// Sauvegarder l'état non-fullscreen courant
saveCurrentState();
// Propager l'état par défaut des tooltips (global pour l’instant)
document.dispatchEvent(
new CustomEvent("toggle-tooltips", { detail: { visible: !!config.showTooltipsDefault } })
);
setMenuContentMaxSize();
}, 200);
}