Spaces:
Running
Running
| // 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); | |
| } | |