(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 viewer state --- let SPLAT = null; let viewerActive = false; // 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 = `
Error loading model: ${error.message}
`; return false; } // Start rendering this.startRenderLoop(); return true; }, // Handle resize handleResize() { if (this.renderer) { this.renderer.setSize(canvas.clientWidth, canvas.clientHeight); } }, // Start the render loop startRenderLoop() { // Animation frame loop const renderFrame = () => { if (this.controls && this.scene && this.camera && this.renderer) { this.controls.update(); this.renderer.render(this.scene, this.camera); this.animFrameId = requestAnimationFrame(renderFrame); } }; this.animFrameId = requestAnimationFrame(renderFrame); }, // Reset camera to initial position resetCamera() { if (!this.camera || !this.initialCameraPosition || !this.initialCameraRotation) { console.log("Cannot reset camera - missing camera instance or initial positions"); return; } try { // Dispose previous controls if they exist if (this.controls && typeof this.controls.dispose === 'function') { this.controls.dispose(); } // Set camera back to initial position this.camera.position = this.initialCameraPosition.clone(); this.camera.rotation = this.initialCameraRotation.clone(); // Create new controls this.controls = new SPLAT.OrbitControls( this.camera, canvas, 0.5, 0.5, 5, true, new SPLAT.Vector3(), chosenInitAlpha, chosenInitBeta, chosenInitRadius ); // Set control constraints this.controls.maxZoom = maxZoom; this.controls.minZoom = minZoom; this.controls.minAngle = minAngle; this.controls.maxAngle = maxAngle; this.controls.minAzimuth = minAzimuth; this.controls.maxAzimuth = maxAzimuth; this.controls.panSpeed = isMobile ? 0.5 : 1.2; // Update controls this.controls.update(); console.log("Camera reset successful"); } catch (error) { console.error("Error resetting camera:", error); } }, // Clean up resources dispose() { // Cancel animation frame if (this.animFrameId) { cancelAnimationFrame(this.animFrameId); this.animFrameId = null; } // Remove resize observer if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } // Dispose controls if (this.controls && typeof this.controls.dispose === 'function') { try { this.controls.dispose(); } catch (e) { console.warn("Error disposing controls:", e); } this.controls = null; } // Dispose renderer if (this.renderer && typeof this.renderer.dispose === 'function') { try { this.renderer.dispose(); } catch (e) { console.warn("Error disposing renderer:", e); } this.renderer = null; } // Clear scene if (this.scene) { this.scene = null; } // Clear camera this.camera = null; this.initialCameraPosition = null; this.initialCameraRotation = null; console.log("Viewer resources disposed"); } }; // Initialize the viewer const success = await window.viewer.init(); if (success) { viewerActive = true; console.log("PLY viewer started successfully"); } else { viewerActive = false; console.error("Failed to initialize PLY viewer"); } } catch (error) { console.error("Error in loadAndStartViewer:", error); progressDialog.innerHTML = `Error initializing viewer: ${error.message}
`; viewerActive = false; } } // Stop the viewer and clean up resources function stopViewer() { if (!viewerActive) { console.log("Viewer not active, nothing to stop"); return; } console.log("Stopping viewer..."); // Dispose viewer resources if (window.viewer && typeof window.viewer.dispose === 'function') { window.viewer.dispose(); } // Reset WebGL context if possible if (canvas) { const ctx = canvas.getContext('webgl') || canvas.getContext('webgl2'); if (ctx && ctx.getExtension('WEBGL_lose_context')) { try { ctx.getExtension('WEBGL_lose_context').loseContext(); } catch (e) { console.warn("Error releasing WebGL context:", e); } } } // Mark viewer as not active viewerActive = false; console.log("Viewer stopped"); } })();