(function() { // --- Outer scope variables for camera state --- let cameraInstance = null; let controlsInstance = null; let initialCameraPosition = null; let initialCameraRotation = null; // Generate a unique identifier for this widget instance. const instanceId = Math.random().toString(36).substr(2, 8); // Helper: Get query parameters from THIS script’s src URL. function getScriptQueryParam(param) { var params = new URLSearchParams(""); if (document.currentScript && document.currentScript.src.indexOf('?') !== -1) { var queryString = document.currentScript.src.split('?')[1]; params = new URLSearchParams(queryString); } else { params = new URLSearchParams(window.location.search); } return params.get(param); } // Read required URLs. var gifUrl = getScriptQueryParam("gif_url"); var plyUrl = getScriptQueryParam("ply_url"); // Determine the aspect ratio. // Default aspect: 1:1 (i.e. 100% padding-bottom) var aspectPercent = "100%"; var aspectParam = getScriptQueryParam("aspect"); if (aspectParam) { // Expected format: "width:height" (e.g. "16:9") or a decimal number. if (aspectParam.indexOf(":") !== -1) { var parts = aspectParam.split(":"); var w = parseFloat(parts[0]); var h = parseFloat(parts[1]); if (!isNaN(w) && !isNaN(h) && w > 0) { aspectPercent = (h / w * 100) + "%"; } } else { var aspectValue = parseFloat(aspectParam); if (!isNaN(aspectValue) && aspectValue > 0) { aspectPercent = (100 / aspectValue) + "%"; } } } else { // If no aspect parameter is provided, compute the aspect ratio from the parent element. var parentContainer = document.currentScript.parentNode; var containerWidth = parentContainer.offsetWidth; var containerHeight = parentContainer.offsetHeight; if (containerWidth > 0 && containerHeight > 0) { aspectPercent = (containerHeight / containerWidth * 100) + "%"; } } // Optional parameters for zoom and rotation limits. // Defaults: zoom from 0 to 20; rotation from 0 to 360. var minZoom = parseFloat(getScriptQueryParam("minZoom") || "0"); var maxZoom = parseFloat(getScriptQueryParam("maxZoom") || "20"); var minAngle = parseFloat(getScriptQueryParam("minAngle") || "0"); var maxAngle = parseFloat(getScriptQueryParam("maxAngle") || "360"); // Detect if the device is iOS. var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; // Inject CSS styles into the document head, scoped with the unique id. var styleEl = document.createElement('style'); styleEl.textContent = ` /* Widget container styling */ #ply-widget-container-${instanceId} { position: relative; width: 100%; height: 0; padding-bottom: ${aspectPercent}; } /* When in fake fullscreen mode (iOS fallback) */ #ply-widget-container-${instanceId}.fake-fullscreen { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; padding-bottom: 0 !important; z-index: 9999 !important; } /* GIF Preview styling */ #gif-preview-container-${instanceId} { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 1px solid #474558; border-radius: 10px; overflow: hidden; cursor: pointer; } #gif-preview-container-${instanceId} img { width: 100%; height: 100%; object-fit: cover; } /* Viewer Container styling */ #viewer-container-${instanceId} { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #FEFEFD; border: 1px solid #474558; border-radius: 10px; } /* Canvas fills the viewer container */ #canvas-${instanceId} { width: 100%; height: 100%; display: block; } /* Progress dialog styling (as a centered div) */ #progress-dialog-${instanceId} { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); border: none; background: rgba(255,255,255,0.9); padding: 20px; border-radius: 5px; z-index: 1000; display: none; } /* Menu (instructions) content styling */ #menu-content-${instanceId} { display: none; position: absolute; top: 62px; right: 70px; background: #FFFEF4; border: 1px solid #474558; border-radius: 5px; padding: 10px; font-size: 15px; line-height: 1.4; color: #474558; } /* Button styling */ .widget-button { position: absolute; width: 45px; height: 45px; background-color: #FFFEF4; border: 1px solid #474558; border-radius: 50%; cursor: pointer; font-size: 14px; color: #474558; display: flex; align-items: center; justify-content: center; } /* Positions: Close at top-left, fullscreen at top-right, help (instructions) below fullscreen */ #close-btn-${instanceId} { top: 17px; left: 15px; } #fullscreen-toggle-${instanceId} { top: 17px; right: 15px; } #help-toggle-${instanceId} { top: 72px; right: 15px; } /* Reset Camera Button below the help button */ #reset-camera-btn-${instanceId} { top: 127px; right: 15px; } /* Adjust the reset icon position within the reset camera button */ .reset-icon { display: inline-block; transform: translateY(-3px); } `; document.head.appendChild(styleEl); // Create the widget container and set its inner HTML. var widgetContainer = document.createElement('div'); widgetContainer.id = 'ply-widget-container-' + instanceId; widgetContainer.innerHTML = `
Preview
`; document.currentScript.parentNode.appendChild(widgetContainer); // Grab element references using the unique IDs. var gifPreview = document.getElementById('gif-preview-container-' + instanceId); var viewerContainer = document.getElementById('viewer-container-' + instanceId); var previewImage = document.getElementById('preview-image-' + instanceId); var closeBtn = document.getElementById('close-btn-' + instanceId); var fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId); var helpToggle = document.getElementById('help-toggle-' + instanceId); var resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId); var menuContent = document.getElementById('menu-content-' + instanceId); var canvas = document.getElementById('canvas-' + instanceId); var progressDialog = document.getElementById('progress-dialog-' + instanceId); var progressIndicator = document.getElementById('progress-indicator-' + instanceId); // Set the preview image if provided. if (gifUrl) { previewImage.src = gifUrl; } // --- Button Event Handlers --- gifPreview.addEventListener('click', function() { gifPreview.style.display = 'none'; viewerContainer.style.display = 'block'; initializeViewer(); }); closeBtn.addEventListener('click', function() { if (document.fullscreenElement === widgetContainer) { if (document.exitFullscreen) { document.exitFullscreen(); } } widgetContainer.classList.remove('fake-fullscreen'); 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'); } 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(); } } } }); document.addEventListener('fullscreenchange', function() { if (document.fullscreenElement === widgetContainer) { fullscreenToggle.textContent = '⇲'; } else { fullscreenToggle.textContent = '⇱'; } }); helpToggle.addEventListener('click', function(e) { e.stopPropagation(); menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block'; }); resetCameraBtn.addEventListener('click', function() { console.log("Reset camera button clicked."); if (cameraInstance && initialCameraPosition && initialCameraRotation) { cameraInstance.position = initialCameraPosition.clone(); cameraInstance.rotation = initialCameraRotation.clone(); if (typeof cameraInstance.update === 'function') { cameraInstance.update(); } if (controlsInstance && typeof controlsInstance.update === 'function') { controlsInstance.update(); } } }); // --- Initialize the 3D PLY Viewer --- async function initializeViewer() { const SPLAT = await import("https://cdn.jsdelivr.net/npm/gsplat@latest"); progressDialog.style.display = 'block'; const renderer = new SPLAT.WebGLRenderer(canvas); const scene = new SPLAT.Scene(); const camera = new SPLAT.Camera(); const controls = new SPLAT.OrbitControls(camera, canvas); cameraInstance = camera; controlsInstance = controls; initialCameraPosition = camera.position.clone(); initialCameraRotation = camera.rotation.clone(); canvas.style.background = "#FEFEFD"; controls.maxZoom = maxZoom; controls.minZoom = minZoom; controls.minAngle = minAngle; controls.maxAngle = maxAngle; controls.update(); try { await SPLAT.PLYLoader.LoadAsync( plyUrl, scene, (progress) => { progressIndicator.value = progress * 100; } ); progressDialog.style.display = 'none'; } catch (error) { console.error("Error loading PLY file:", error); progressDialog.innerHTML = `

Error loading model: ${error.message}

`; } const frame = () => { controls.update(); renderer.render(scene, camera); requestAnimationFrame(frame); }; const handleResize = () => { renderer.setSize(canvas.clientWidth, canvas.clientHeight); }; handleResize(); window.addEventListener("resize", handleResize); requestAnimationFrame(frame); } })();