// interface.js const currentScriptTag = document.currentScript; (async function() { // 1. Locate script tag and read data-config let scriptTag = currentScriptTag; if (!scriptTag) { const scripts = document.getElementsByTagName('script'); for (let i = 0; i < scripts.length; i++) { if (scripts[i].src.includes('interface.js') && scripts[i].hasAttribute('data-config')) { scriptTag = scripts[i]; break; } } if (!scriptTag && scripts.length > 0) { scriptTag = scripts[scripts.length - 1]; } } const configUrl = scriptTag.getAttribute('data-config'); let config = {}; if (configUrl) { try { const response = await fetch(configUrl); config = await response.json(); } catch (error) { return; } } else { return; } // 2. CSS injection if (config.css_url) { const linkEl = document.createElement('link'); linkEl.rel = "stylesheet"; linkEl.href = config.css_url; document.head.appendChild(linkEl); } // 3. Unique instanceId const instanceId = Math.random().toString(36).substr(2, 8); // 4. Aspect ratio calc let aspectPercent = "100%"; if (config.aspect) { if (config.aspect.includes(":")) { const [w, h] = config.aspect.split(":").map(Number); if (w > 0 && h > 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. Widget container 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); // 5a. Ensure touch-action disables iOS Safari pinch/zoom/scroll on viewer widgetContainer.style.touchAction = 'none'; // 5b. Set iOS meta viewport to prevent zoom if (!document.querySelector('meta[name="viewport"]')) { const meta = document.createElement('meta'); meta.name = "viewport"; meta.content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"; document.head.appendChild(meta); } // Tooltips toggle button (if configured) const tooltipsButtonHTML = config.tooltips_url ? `` : ''; // Viewer HTML widgetContainer.innerHTML = `
${tooltipsButtonHTML}
`; scriptTag.parentNode.appendChild(widgetContainer); // 6. DOM references 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'); const tooltipTextDiv = document.getElementById('tooltip-text'); const tooltipImage = document.getElementById('tooltip-image'); const tooltipCloseBtn = document.getElementById('tooltip-close'); // Platform detection const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const isMobile = isIOS || /Android/i.test(navigator.userAgent); // Help text const tooltipInstruction = config.tooltips_url ? '- Cliquez sur ⦿ pour afficher/masquer les tooltips.
' : ''; helpTextDiv.innerHTML = isMobile ? '- Pour vous déplacer, glissez deux doigts sur l\'écran.
- Pour orbiter, utilisez un doigt.
- Pour zoomer, pincez avec deux doigts.
' + tooltipInstruction + '- ⟲ Réinitialise la caméra.
- ⇱ Passe en plein écran.
' : '- orbitez avec le clic droit
- zoomez avec la molette
- déplacez vous avec le clic gauche
' + tooltipInstruction + '- ⟲ Réinitialise la caméra.
- ⇱ Passe en plein écran.
'; menuContent.style.display = 'block'; viewerContainerElem.style.display = 'block'; let dragHide = null; function hideTooltipPanel() { if (dragHide) { viewerContainerElem.removeEventListener('pointermove', dragHide); dragHide = null; } tooltipPanel.style.display = 'none'; } function hideHelpPanel() { menuContent.style.display = 'none'; } // 7. Load viewer.js let viewerModule; try { viewerModule = await import('https://mikafil-viewer-gs.static.hf.space/viewer.js'); await viewerModule.initializeViewer(config, instanceId); } catch (err) { return; } const canvasId = 'canvas-' + instanceId; const canvasEl = document.getElementById(canvasId); // 7b. Fix for iOS: always use correct canvas style and prevent double-scaling if (canvasEl) { canvasEl.style.touchAction = 'none'; canvasEl.style.width = '100%'; canvasEl.style.height = '100%'; canvasEl.style.display = 'block'; // Remove any tab focus from canvas to prevent scroll-zoom canvasEl.setAttribute('tabindex', '-1'); } // 8. Tooltips button conditional display 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. Fullscreen/state logic 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 ); } savedState = null; } 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); } fullscreenToggle.textContent = '⇲'; isFullscreen = true; } 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; } // 10. Event listeners fullscreenToggle.addEventListener('click', () => { hideTooltipPanel(); isFullscreen ? exitFullscreen() : enterFullscreen(); }); document.addEventListener('fullscreenchange', () => { if (!document.fullscreenElement && isFullscreen) { isFullscreen = false; restoreOriginalStyles(); } }); helpToggle.addEventListener('click', (e) => { hideTooltipPanel(); e.stopPropagation(); menuContent.style.display = menuContent.style.display === 'block' ? 'none' : 'block'; }); 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('tooltip-selected', (evt) => { const { title, description, imgUrl } = evt.detail; tooltipTextDiv.innerHTML = `${title}
${description}`; if (imgUrl) { tooltipImage.src = imgUrl; tooltipImage.style.display = 'block'; } else { tooltipImage.style.display = 'none'; } tooltipPanel.style.display = 'flex'; dragHide = (e) => { if ((e.pointerType === 'mouse' && e.buttons !== 0) || e.pointerType === 'touch') { hideTooltipPanel(); } }; viewerContainerElem.addEventListener('pointermove', dragHide); }); if (canvasEl) { canvasEl.addEventListener('wheel', hideTooltipPanel, { passive: true }); // iOS pinch zoom prevention canvasEl.addEventListener('touchmove', function(e) { if (e.touches.length > 1) { e.preventDefault(); } }, { passive: false }); } 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 ); } } }); // iOS: unlock WebGL/audio on first user gesture (if needed) if (isIOS && canvasEl) { let firstUserAction = false; function unlockWebGLAndAudio() { if (!firstUserAction) { if (viewerModule.app && viewerModule.app.graphicsDevice && viewerModule.app.graphicsDevice.canvas) { // iOS hack: force focus and dummy audio play viewerModule.app.graphicsDevice.canvas.focus && viewerModule.app.graphicsDevice.canvas.focus(); try { const ctx = new AudioContext(); const buffer = ctx.createBuffer(1, 1, 22050); const src = ctx.createBufferSource(); src.buffer = buffer; src.connect(ctx.destination); src.start(0); } catch {} } firstUserAction = true; } } canvasEl.addEventListener('touchstart', unlockWebGLAndAudio, { once: true }); canvasEl.addEventListener('mousedown', unlockWebGLAndAudio, { once: true }); } setTimeout(() => { saveCurrentState(); document.dispatchEvent(new CustomEvent('toggle-tooltips', { detail: { visible: !!config.showTooltipsDefault } })); }, 200); // Ensure ResizeObserver always triggers a correct canvas resize (iOS) if (canvasEl && window.ResizeObserver) { const resizeObs = new ResizeObserver(() => { if (viewerModule.app) { viewerModule.app.resizeCanvas( viewerContainerElem.clientWidth, viewerContainerElem.clientHeight ); } }); resizeObs.observe(viewerContainerElem); } })();