(async function() { // Retrieve the current script tag and load the JSON configuration file from the data-config attribute. const scriptTag = document.currentScript; 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; } // Load the external CSS file if provided in the config. if (config.css_url) { const linkEl = document.createElement("link"); linkEl.rel = "stylesheet"; linkEl.href = config.css_url; document.head.appendChild(linkEl); } // --- Outer scope variables for camera state --- let cameraInstance = null; let controlsInstance = null; let initialCameraPosition = null; let initialCameraRotation = null; let rendererInstance = null; let sceneInstance = null; let SPLAT = null; // We'll save the imported SPLAT module here. let animFrameId = null; let resizeHandler = null; let hasInitializedViewer = false; let loadedPlyModel = null; // Generate a unique identifier for this widget instance. const instanceId = Math.random().toString(36).substr(2, 8); // Read configuration values from the JSON file. const gifUrl = config.gif_url; const plyUrl = config.ply_url; const minZoom = parseFloat(config.minZoom || "0"); const maxZoom = parseFloat(config.maxZoom || "20"); const minAngle = parseFloat(config.minAngle || "0"); const maxAngle = parseFloat(config.maxAngle || "360"); const minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -Infinity; const maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : Infinity; // Read initial orbit parameters for desktop. const initAlphaDesktop = config.initAlpha !== undefined ? parseFloat(config.initAlpha) : 0.5; const initBetaDesktop = config.initBeta !== undefined ? parseFloat(config.initBeta) : 0.5; const initRadiusDesktop = config.initRadius !== undefined ? parseFloat(config.initRadius) : 5; // Read initial orbit parameters for phone. const initAlphaPhone = config.initAlphaPhone !== undefined ? parseFloat(config.initAlphaPhone) : initAlphaDesktop; const initBetaPhone = config.initBetaPhone !== undefined ? parseFloat(config.initBetaPhone) : initBetaDesktop; const initRadiusPhone = config.initRadiusPhone !== undefined ? parseFloat(config.initRadiusPhone) : initRadiusDesktop; // Detect if the device is iOS. const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; // Also detect Android devices. const isMobile = isIOS || /Android/i.test(navigator.userAgent); // Choose the appropriate initial orbit values based on device type. const chosenInitAlpha = isMobile ? initAlphaPhone : initAlphaDesktop; const chosenInitBeta = isMobile ? initBetaPhone : initBetaDesktop; const chosenInitRadius = isMobile ? initRadiusPhone : initRadiusDesktop; // Determine the aspect ratio. let aspectPercent = "100%"; if (config.aspect) { if (config.aspect.indexOf(":") !== -1) { 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) + "%"; } } // Create the widget container. const widgetContainer = document.createElement('div'); widgetContainer.id = 'ply-widget-container-' + instanceId; widgetContainer.classList.add('ply-widget-container'); // Add a mobile class if on a phone. if (isMobile) { widgetContainer.classList.add('mobile'); } // Set inline style for aspect ratio. widgetContainer.style.height = "0"; widgetContainer.style.paddingBottom = aspectPercent; widgetContainer.innerHTML = `
Preview
`; scriptTag.parentNode.appendChild(widgetContainer); // Grab element references. const gifPreview = document.getElementById('gif-preview-container-' + instanceId); const viewerContainer = document.getElementById('viewer-container-' + instanceId); const previewImage = document.getElementById('preview-image-' + instanceId); const closeBtn = document.getElementById('close-btn-' + instanceId); const fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId); const helpToggle = document.getElementById('help-toggle-' + instanceId); const resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId); const menuContent = document.getElementById('menu-content-' + instanceId); const canvas = document.getElementById('canvas-' + instanceId); const progressDialog = document.getElementById('progress-dialog-' + instanceId); const progressIndicator = document.getElementById('progress-indicator-' + instanceId); // Set help instructions based on device type. if (isMobile) { menuContent.innerHTML = ` - Pour vous déplacer, glissez deux doigts sur l'écran.
- Pour orbiter, utilisez un doigt.
- Pour zoomer, pincez avec deux doigts. `; } else { menuContent.innerHTML = ` - orbitez avec le clic droit
- zoomez avec la molette
- déplacez vous avec le clic gauche `; } // If a gif_url is provided, set the preview image. // Otherwise, hide the preview container, show the viewer immediately, // and hide the "close" button since there's no preview to return to. if (gifUrl) { previewImage.src = gifUrl; gifPreview.style.display = 'block'; viewerContainer.style.display = 'none'; } else { gifPreview.style.display = 'none'; viewerContainer.style.display = 'block'; closeBtn.style.display = 'none'; setTimeout(() => { initializeViewer(); }, 100); } // --- Button Event Handlers --- if (gifUrl) { gifPreview.addEventListener('click', function() { console.log("GIF preview clicked, showing 3D viewer"); gifPreview.style.display = 'none'; viewerContainer.style.display = 'block'; initializeViewer(); }); } // Function to clean up the viewer function cleanupViewer() { console.log("Cleaning up viewer resources..."); // Stop animation frame if (animFrameId) { cancelAnimationFrame(animFrameId); animFrameId = null; } // Remove resize event handler if (resizeHandler) { window.removeEventListener("resize", resizeHandler); resizeHandler = null; } // Dispose controls if (controlsInstance && typeof controlsInstance.dispose === 'function') { try { controlsInstance.dispose(); controlsInstance = null; } catch (e) { console.error("Error disposing controls:", e); } } // Clear references but preserve loaded objects // We don't fully dispose the renderer to preserve WebGL context rendererInstance = null; sceneInstance = null; cameraInstance = null; initialCameraPosition = null; initialCameraRotation = null; // Mark viewer as not initialized hasInitializedViewer = false; console.log("Viewer cleanup complete - renderer and PLY model data preserved for fast reload"); } closeBtn.addEventListener('click', function() { console.log("Close button clicked"); // Handle fullscreen exit if (document.fullscreenElement === widgetContainer) { if (document.exitFullscreen) { document.exitFullscreen(); } } if (widgetContainer.classList.contains('fake-fullscreen')) { widgetContainer.classList.remove('fake-fullscreen'); fullscreenToggle.textContent = '⇱'; } // Clean up the viewer cleanupViewer(); // Hide viewer and show GIF viewerContainer.style.display = 'none'; gifPreview.style.display = 'block'; }); fullscreenToggle.addEventListener('click', function() { if (isIOS) { if (!widgetContainer.classList.contains('fake-fullscreen')) { widgetContainer.classList.add('fake-fullscreen'); } else { widgetContainer.classList.remove('fake-fullscreen'); resetCamera(); } fullscreenToggle.textContent = widgetContainer.classList.contains('fake-fullscreen') ? '⇲' : '⇱'; } else { if (!document.fullscreenElement) { if (widgetContainer.requestFullscreen) { widgetContainer.requestFullscreen(); } else if (widgetContainer.webkitRequestFullscreen) { widgetContainer.webkitRequestFullscreen(); } else if (widgetContainer.mozRequestFullScreen) { widgetContainer.mozRequestFullScreen(); } else if (widgetContainer.msRequestFullscreen) { widgetContainer.msRequestFullscreen(); } } else { if (document.exitFullscreen) { document.exitFullscreen(); } } } }); // Listen for native fullscreen changes. document.addEventListener('fullscreenchange', function() { if (document.fullscreenElement === widgetContainer) { fullscreenToggle.textContent = '⇲'; widgetContainer.style.height = '100%'; widgetContainer.style.paddingBottom = '0'; resetCamera(); } else { fullscreenToggle.textContent = '⇱'; widgetContainer.style.height = '0'; widgetContainer.style.paddingBottom = aspectPercent; resetCamera(); } }); helpToggle.addEventListener('click', function(e) { e.stopPropagation(); menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block'; }); // --- Camera Reset Function --- function resetCamera() { console.log("Resetting camera to initial position"); if (!cameraInstance || !SPLAT) { console.log("Cannot reset camera - missing camera instance"); return; } try { // Create a new camera to reset its position cameraInstance = new SPLAT.Camera(); // Dispose previous controls if they exist if (controlsInstance && typeof controlsInstance.dispose === 'function') { controlsInstance.dispose(); } // Create new controls controlsInstance = new SPLAT.OrbitControls( cameraInstance, canvas, 0.5, 0.5, 5, true, new SPLAT.Vector3(), chosenInitAlpha, chosenInitBeta, chosenInitRadius ); // Set control constraints controlsInstance.maxZoom = maxZoom; controlsInstance.minZoom = minZoom; controlsInstance.minAngle = minAngle; controlsInstance.maxAngle = maxAngle; controlsInstance.minAzimuth = minAzimuth; controlsInstance.maxAzimuth = maxAzimuth; controlsInstance.panSpeed = isMobile ? 0.5 : 1.2; // Update controls controlsInstance.update(); console.log("Camera reset successful"); } catch (error) { console.error("Error resetting camera:", error); } } resetCameraBtn.addEventListener('click', async function() { console.log("Reset camera button clicked"); resetCamera(); }); document.addEventListener('keydown', function(e) { if (e.key === 'Escape' || e.key === 'Esc') { let wasFullscreen = false; if (document.fullscreenElement === widgetContainer) { wasFullscreen = true; if (document.exitFullscreen) { document.exitFullscreen(); } } if (widgetContainer.classList.contains('fake-fullscreen')) { wasFullscreen = true; widgetContainer.classList.remove('fake-fullscreen'); fullscreenToggle.textContent = '⇱'; } if (wasFullscreen) { resetCamera(); } } }); // --- Initialize the 3D PLY Viewer --- async function initializeViewer() { // Skip initialization if already initialized if (hasInitializedViewer) { console.log("Viewer already initialized, skipping initialization"); return; } console.log("Initializing PLY viewer..."); progressDialog.style.display = 'block'; progressIndicator.value = 0; try { // Load the SPLAT library if not already loaded if (!SPLAT) { console.log("Loading SPLAT library..."); SPLAT = await import("https://bilca-gsplat-library.static.hf.space/dist/index.js"); console.log("SPLAT library loaded successfully"); } // Create new renderer or clear existing one console.log("Creating WebGL renderer..."); rendererInstance = new SPLAT.WebGLRenderer(canvas); // Create new scene console.log("Creating scene..."); sceneInstance = new SPLAT.Scene(); // Create camera console.log("Creating camera..."); cameraInstance = new SPLAT.Camera(); // Store initial camera state initialCameraPosition = cameraInstance.position.clone(); initialCameraRotation = cameraInstance.rotation.clone(); // Set canvas background canvas.style.background = config.canvas_background || "#FEFEFD"; // Create controls console.log("Creating orbit controls..."); controlsInstance = new SPLAT.OrbitControls( cameraInstance, canvas, 0.5, 0.5, 5, true, new SPLAT.Vector3(), chosenInitAlpha, chosenInitBeta, chosenInitRadius ); // Set control constraints controlsInstance.maxZoom = maxZoom; controlsInstance.minZoom = minZoom; controlsInstance.minAngle = minAngle; controlsInstance.maxAngle = maxAngle; controlsInstance.minAzimuth = minAzimuth; controlsInstance.maxAzimuth = maxAzimuth; controlsInstance.panSpeed = isMobile ? 0.5 : 1.2; controlsInstance.update(); // Handle resize const handleResize = () => { if (rendererInstance) { rendererInstance.setSize(canvas.clientWidth, canvas.clientHeight); } }; // Set up resize handler handleResize(); resizeHandler = handleResize; window.addEventListener("resize", resizeHandler); // Load PLY file if not already loaded or reload it console.log("Loading or reusing PLY model..."); try { // Always load fresh PLY data console.log(`Loading PLY file from ${plyUrl}`); loadedPlyModel = await SPLAT.PLYLoader.LoadAsync( plyUrl, sceneInstance, (progress) => { progressIndicator.value = progress * 100; } ); console.log("PLY file loaded successfully, model:", loadedPlyModel); } catch (error) { console.error("Error loading PLY file:", error); progressDialog.innerHTML = `

Error loading model: ${error.message}

`; return; } // Hide progress dialog progressDialog.style.display = 'none'; // Debug: Check scene content console.log("Scene contents:", sceneInstance); if (sceneInstance && typeof sceneInstance.getChildren === 'function') { const children = sceneInstance.getChildren(); console.log("Scene children:", children); if (children && children.length === 0) { console.warn("Warning: Scene has no children!"); } } else { console.log("Cannot check scene children - getChildren method not available"); } // Start animation loop console.log("Starting animation loop..."); function animate() { if (!controlsInstance || !sceneInstance || !cameraInstance || !rendererInstance) { console.log("Animation stopped - resources cleaned up"); return; } controlsInstance.update(); rendererInstance.render(sceneInstance, cameraInstance); animFrameId = requestAnimationFrame(animate); } // Start the animation animFrameId = requestAnimationFrame(animate); // Mark viewer as initialized hasInitializedViewer = true; console.log("PLY viewer initialization complete"); } catch (error) { console.error("Error initializing PLY viewer:", error); progressDialog.innerHTML = `

Error initializing viewer: ${error.message}

`; hasInitializedViewer = false; } } })();