Spaces:
Running
Running
| // ============================== | |
| // interface.js | |
| // ============================== | |
| // Store a reference to the <script> tag that loaded this file | |
| const currentScriptTag = document.currentScript; | |
| (async function() { | |
| // βββ 1. Locate the <script> 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) { | |
| console.error("Error loading config file:", error); | |
| return; | |
| } | |
| } else { | |
| console.error("No config file provided. Please set a data-config attribute on the <script> tag."); | |
| return; | |
| } | |
| // βββ 2. If config.css_url is provided, inject a <link> to that CSS βββββββββββββ | |
| if (config.css_url) { | |
| const linkEl = document.createElement('link'); | |
| linkEl.rel = "stylesheet"; | |
| linkEl.href = config.css_url; | |
| document.head.appendChild(linkEl); | |
| } | |
| // βββ 3. Generate a unique instanceId for this widget βββββββββββββββββββββββββββ | |
| const instanceId = Math.random().toString(36).substr(2, 8); | |
| // βββ 4. Compute the aspect ratio (padding-bottom %) ββββββββββββββββββββββββββββ | |
| 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. Create the widget container (no GIF preview, no close button) βββββββββββ | |
| 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); | |
| // Add the 3D-viewer HTML + β¦Ώ toggle + tooltip + help HTML | |
| 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> | |
| <button id="points-toggle-${instanceId}" class="widget-button points-toggle">β¦Ώ</button> | |
| <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="point-tooltip" class="point-tooltip" style="display: none;"> | |
| <div class="point-tooltip-content"> | |
| <span id="point-tooltip-close" class="point-tooltip-close">Γ</span> | |
| <div id="point-tooltip-text" class="point-tooltip-text"></div> | |
| <img id="point-tooltip-image" class="point-tooltip-image" src="" alt="" style="display: none;" /> | |
| </div> | |
| </div> | |
| `; | |
| // Append the widget container immediately after the <script> tag | |
| scriptTag.parentNode.appendChild(widgetContainer); | |
| // βββ 6. Grab references to new DOM elements ββββββββββββββββββββββββββββββββββ | |
| 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 pointsToggleBtn = document.getElementById('points-toggle-' + instanceId); | |
| const menuContent = document.getElementById('menu-content-' + instanceId); | |
| const helpTextDiv = menuContent.querySelector('.help-text'); | |
| // Tooltip elements | |
| const tooltipDiv = document.getElementById('point-tooltip'); | |
| const tooltipTextDiv = document.getElementById('point-tooltip-text'); | |
| const tooltipImage = document.getElementById('point-tooltip-image'); | |
| const tooltipCloseBtn = document.getElementById('point-tooltip-close'); | |
| // βββ 6a. Detect mobile vs. desktop ββββββββββββββββββββββββββββββββββββββββββββ | |
| const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; | |
| const isMobile = isIOS || /Android/i.test(navigator.userAgent); | |
| // Conditionally include the French tooltip instruction line if points_url exists | |
| const tooltipInstruction = config.points_url | |
| ? '- Cliquez sur β¦Ώ pour afficher/masquer les points dβinformation.<br>' | |
| : ''; | |
| // Fill help text with appropriate instructions | |
| if (isMobile) { | |
| helpTextDiv.innerHTML = ` | |
| - Pour vous dΓ©placer, glissez deux doigts sur l'Γ©cran.<br> | |
| - Pour orbiter, utilisez un doigt.<br> | |
| - Pour zoomer, pincez avec deux doigts.<br> | |
| ${tooltipInstruction} | |
| `; | |
| } else { | |
| helpTextDiv.innerHTML = ` | |
| - orbitez avec le clic droit<br> | |
| - zoomez avec la molette<br> | |
| - dΓ©placez vous avec le clic gauche<br> | |
| ${tooltipInstruction} | |
| `; | |
| } | |
| // Ensure instructions panel is visible by default | |
| menuContent.style.display = 'block'; | |
| viewerContainerElem.style.display = 'block'; | |
| // Variable to hold the drag-hide listener reference | |
| let dragHide = null; | |
| // Utility: hide tooltip and remove drag-hide listener | |
| function hideTooltip() { | |
| if (dragHide) { | |
| viewerContainerElem.removeEventListener('pointermove', dragHide); | |
| dragHide = null; | |
| } | |
| tooltipDiv.style.display = 'none'; | |
| } | |
| // Utility: hide instructions panel | |
| function hideHelpPanel() { | |
| menuContent.style.display = 'none'; | |
| } | |
| // βββ 7. Dynamically load viewer.js βββββββββββββββββββββββββββββββββββββββββ | |
| let viewerModule; | |
| try { | |
| viewerModule = await import('./viewer.js'); | |
| await viewerModule.initializeViewer(config, instanceId); | |
| } catch (err) { | |
| console.error("Failed to load viewer.js or initialize the 3D viewer:", err); | |
| return; | |
| } | |
| // After viewer is ready, grab the canvas element | |
| const canvasId = 'canvas-' + instanceId; | |
| const canvasEl = document.getElementById(canvasId); | |
| // βββ 8. Conditional display of points-toggle button βββββββββββββββββββββββββ | |
| if (!config.points_url) { | |
| pointsToggleBtn.style.display = 'none'; | |
| } else { | |
| fetch(config.points_url) | |
| .then(resp => { | |
| if (!resp.ok) { | |
| pointsToggleBtn.style.display = 'none'; | |
| } | |
| }) | |
| .catch(() => { | |
| pointsToggleBtn.style.display = 'none'; | |
| }); | |
| } | |
| // βββ 9. Fullscreen / state-preservation logic βββββββββββββββββββββββββββββββ | |
| let isFullscreen = false; | |
| let savedState = null; | |
| function saveCurrentState() { | |
| if (isFullscreen) return; | |
| const computedWidget = window.getComputedStyle(widgetContainer); | |
| const computedViewer = window.getComputedStyle(viewerContainerElem); | |
| const originalAspect = widgetContainer.getAttribute('data-original-aspect') || aspectPercent; | |
| const containerWidth = widgetContainer.offsetWidth; | |
| const containerHeight = widgetContainer.clientHeight || viewerContainerElem.offsetHeight; | |
| const calculatedRatio = (containerHeight / containerWidth * 100) + '%'; | |
| 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, | |
| aspectPercent: originalAspect, | |
| calculatedAspect: calculatedRatio, | |
| computedWidth: computedWidget.width, | |
| computedHeight: computedWidget.height, | |
| offsetWidth: containerWidth, | |
| offsetHeight: containerHeight | |
| }, | |
| viewer: { | |
| borderRadius: viewerContainerElem.style.borderRadius, | |
| border: viewerContainerElem.style.border, | |
| computedWidth: computedViewer.width, | |
| computedHeight: computedViewer.height | |
| } | |
| }; | |
| } | |
| function restoreOriginalStyles() { | |
| if (!savedState) return; | |
| let aspectToUse = aspectPercent; | |
| if (savedState.widget.offsetWidth && savedState.widget.offsetHeight) { | |
| const actualRatio = (savedState.widget.offsetHeight / savedState.widget.offsetWidth * 100) + '%'; | |
| aspectToUse = actualRatio; | |
| } else if (savedState.widget.calculatedAspect) { | |
| aspectToUse = savedState.widget.calculatedAspect; | |
| } else if (savedState.widget.aspectPercent) { | |
| aspectToUse = savedState.widget.aspectPercent; | |
| } else if (savedState.widget.paddingBottom) { | |
| 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.style.border = savedState.widget.border || ""; | |
| widgetContainer.style.borderRadius = savedState.widget.borderRadius|| ""; | |
| widgetContainer.style.overflow = savedState.widget.overflow || ""; | |
| 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) { | |
| const cw = viewerContainerElem.clientWidth; | |
| const ch = viewerContainerElem.clientHeight; | |
| viewerModule.app.resizeCanvas(cw, ch); | |
| } | |
| 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(err => { | |
| console.error("Fullscreen request failed:", err); | |
| applyFullscreenStyles(); | |
| widgetContainer.classList.add('fake-fullscreen'); | |
| }); | |
| } else if (widgetContainer.webkitRequestFullscreen) { | |
| widgetContainer.webkitRequestFullscreen(); | |
| applyFullscreenStyles(); | |
| } else if (widgetContainer.mozRequestFullScreen) { | |
| widgetContainer.mozRequestFullScreen(); | |
| applyFullscreenStyles(); | |
| } else if (widgetContainer.msRequestFullscreen) { | |
| widgetContainer.msRequestFullscreen(); | |
| applyFullscreenStyles(); | |
| } else { | |
| applyFullscreenStyles(); | |
| widgetContainer.classList.add('fake-fullscreen'); | |
| } | |
| } | |
| } | |
| function exitFullscreen() { | |
| if (document.fullscreenElement === widgetContainer) { | |
| if (document.exitFullscreen) { | |
| document.exitFullscreen().catch(err => { | |
| console.error("Error exiting fullscreen:", err); | |
| }); | |
| } else if (document.webkitExitFullscreen) { | |
| document.webkitExitFullscreen(); | |
| } else if (document.mozCancelFullScreen) { | |
| document.mozCancelFullScreen(); | |
| } else if (document.msExitFullscreen) { | |
| document.msExitFullscreen(); | |
| } | |
| } | |
| widgetContainer.classList.remove('fake-fullscreen'); | |
| restoreOriginalStyles(); | |
| isFullscreen = false; | |
| } | |
| // βββ 10. Hook up event listeners βββββββββββββββββββββββββββββββββββββββββββ | |
| fullscreenToggle.addEventListener('click', () => { | |
| hideTooltip(); | |
| if (!isFullscreen) { | |
| enterFullscreen(); | |
| } else { | |
| exitFullscreen(); | |
| } | |
| }); | |
| document.addEventListener('fullscreenchange', () => { | |
| if (document.fullscreenElement === widgetContainer) { | |
| isFullscreen = true; | |
| applyFullscreenStyles(); | |
| } else if (isFullscreen) { | |
| isFullscreen = false; | |
| restoreOriginalStyles(); | |
| } | |
| }); | |
| document.addEventListener('webkitfullscreenchange', () => { | |
| if (document.webkitFullscreenElement === widgetContainer) { | |
| isFullscreen = true; | |
| applyFullscreenStyles(); | |
| } else if (isFullscreen) { | |
| isFullscreen = false; | |
| restoreOriginalStyles(); | |
| } | |
| }); | |
| document.addEventListener('mozfullscreenchange', () => { | |
| if (document.mozFullScreenElement === widgetContainer) { | |
| isFullscreen = true; | |
| applyFullscreenStyles(); | |
| } else if (isFullscreen) { | |
| isFullscreen = false; | |
| restoreOriginalStyles(); | |
| } | |
| }); | |
| document.addEventListener('MSFullscreenChange', () => { | |
| if (document.msFullscreenElement === widgetContainer) { | |
| isFullscreen = true; | |
| applyFullscreenStyles(); | |
| } else if (isFullscreen) { | |
| isFullscreen = false; | |
| restoreOriginalStyles(); | |
| } | |
| }); | |
| helpToggle.addEventListener('click', (e) => { | |
| hideTooltip(); | |
| e.stopPropagation(); | |
| menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block'; | |
| }); | |
| helpCloseBtn.addEventListener('click', () => { | |
| hideHelpPanel(); | |
| }); | |
| resetCameraBtn.addEventListener('click', () => { | |
| hideTooltip(); | |
| if (viewerModule.resetViewerCamera) { | |
| viewerModule.resetViewerCamera(); | |
| } | |
| }); | |
| // β¦Ώ toggle button | |
| let pointsVisible = !!config.showPointsDefault; | |
| if (pointsToggleBtn.style.display !== 'none') { | |
| if (!pointsVisible) { | |
| pointsToggleBtn.style.opacity = '0.5'; | |
| } | |
| pointsToggleBtn.addEventListener('click', () => { | |
| hideTooltip(); | |
| pointsVisible = !pointsVisible; | |
| pointsToggleBtn.style.opacity = pointsVisible ? '1' : '0.5'; | |
| document.dispatchEvent(new CustomEvent('toggle-points', { detail: { visible: pointsVisible } })); | |
| }); | |
| } | |
| // Close tooltip on button click | |
| tooltipCloseBtn.addEventListener('click', () => { | |
| hideTooltip(); | |
| }); | |
| // Listen for point-selection events and show tooltip | |
| document.addEventListener('point-selected', (evt) => { | |
| const { text, imageUrl } = evt.detail; | |
| tooltipTextDiv.textContent = text || ""; | |
| if (imageUrl) { | |
| tooltipImage.src = imageUrl; | |
| tooltipImage.style.display = 'block'; | |
| } else { | |
| tooltipImage.style.display = 'none'; | |
| } | |
| tooltipDiv.style.display = 'flex'; | |
| // Attach drag-based hide listener | |
| dragHide = (e) => { | |
| if ((e.pointerType === 'mouse' && e.buttons !== 0) || | |
| (e.pointerType === 'touch')) { | |
| hideTooltip(); | |
| } | |
| }; | |
| viewerContainerElem.addEventListener('pointermove', dragHide); | |
| }); | |
| // Hide on any wheel event (zoom) | |
| if (canvasEl) { | |
| canvasEl.addEventListener('wheel', () => { | |
| hideTooltip(); | |
| }, { passive: true }); | |
| } | |
| // Escape key also exits fullscreen | |
| document.addEventListener('keydown', (e) => { | |
| if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) { | |
| exitFullscreen(); | |
| } | |
| }); | |
| // Window resize β resize PlayCanvas canvas | |
| window.addEventListener('resize', () => { | |
| if (isFullscreen && viewerModule.app) { | |
| viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight); | |
| } else if (viewerModule.app) { | |
| const cw = viewerContainerElem.clientWidth; | |
| const ch = viewerContainerElem.clientHeight; | |
| viewerModule.app.resizeCanvas(cw, ch); | |
| } | |
| }); | |
| // Save βinitial stateβ after a brief delay | |
| setTimeout(() => { | |
| saveCurrentState(); | |
| document.dispatchEvent(new CustomEvent('toggle-points', { detail: { visible: pointsVisible } })); | |
| }, 200); | |
| })(); | |