bilca/module_de_visualisation
0
1// Initialize the orbit controller2 setTimeout(() => {3 if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {4 // Calculate distance from camera to model5 const modelPos = modelEntity.getPosition();6 const camPos = cameraEntity.getPosition();7 const distanceVec = new pc.Vec3();8 distanceVec.sub2(camPos, modelPos);9 const distance = distanceVec.length();10 11 // Set up the orbit controller12 cameraEntity.script.orbitCamera.pivotPoint.copy(modelPos);13 cameraEntity.script.orbitCamera.distance = distance;14 cameraEntity.script.orbitCamera._removeInertia();15 16 // Log camera setup for debugging17 console.log(`Camera initialized with pivot: (${modelPos.x}, ${modelPos.y}, ${modelPos.z}), distance: ${distance}`);18 }19 }, 100);20 21 app.root.addChild(cameraEntity);22 23 // Initial resize to match container24 resize();25 26 // Set up wheel handlers27 setupWheelHandlers();28 29 // Hide progress dialog when everything is set up30 progressDialog.style.display = 'none';31 32 // Mark viewer as initialized33 viewerInitialized = true;34 35 console.log("PLY viewer initialization complete");36 });37 38 } catch (error) {39 console.error("Error initializing PlayCanvas viewer:", error);40 progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;41 viewerInitialized = false;42 app = null;43 }44 }45})(); Store the current script reference before the async function runs46const currentScriptTag = document.currentScript;47 48(async function() {49 // Import PlayCanvas50 const pc = await import("https://cdn.skypack.dev/playcanvas@v1.68.0");51 window.pc = pc;52 53 // Find the script tag using a more reliable method54 let scriptTag = currentScriptTag;55 56 // Fallback method if currentScriptTag is null57 if (!scriptTag) {58 const scripts = document.getElementsByTagName('script');59 for (let i = 0; i < scripts.length; i++) {60 if (scripts[i].src.includes('index.js') && scripts[i].hasAttribute('data-config')) {61 scriptTag = scripts[i];62 break;63 }64 }65 66 // If still not found, try the last script on the page67 if (!scriptTag && scripts.length > 0) {68 scriptTag = scripts[scripts.length - 1];69 }70 }71 72 // Check if we found a script tag73 if (!scriptTag) {74 console.error("Could not find the script tag with data-config attribute.");75 return;76 }77 78 const configUrl = scriptTag.getAttribute("data-config");79 let config = {};80 if (configUrl) {81 try {82 const response = await fetch(configUrl);83 config = await response.json();84 } catch (error) {85 console.error("Error loading config file:", error);86 return;87 }88 } else {89 console.error("No config file provided. Please set a data-config attribute on the script tag.");90 return;91 }92 93 // Load the external CSS file if provided in the config.94 if (config.css_url) {95 const linkEl = document.createElement("link");96 linkEl.rel = "stylesheet";97 linkEl.href = config.css_url;98 document.head.appendChild(linkEl);99 }100 101 // --- Outer scope variables ---102 let cameraEntity = null;103 let app = null;104 let modelEntity = null;105 let viewerInitialized = false;106 let wheelHandlers = [];107 let resizeHandler = null;108 let progressChecker = null;109 110 // Generate a unique identifier for this widget instance.111 const instanceId = Math.random().toString(36).substr(2, 8);112 113 // Read configuration values from the JSON file.114 const gifUrl = config.gif_url;115 const plyUrl = config.ply_url;116 117 // Camera constraint parameters118 const minZoom = parseFloat(config.minZoom || "1");119 const maxZoom = parseFloat(config.maxZoom || "20");120 const minAngle = parseFloat(config.minAngle || "-45");121 const maxAngle = parseFloat(config.maxAngle || "90");122 const minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -360;123 const maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : 360;124 125 // Model position, scale, and rotation parameters126 const modelX = config.modelX !== undefined ? parseFloat(config.modelX) : 0;127 const modelY = config.modelY !== undefined ? parseFloat(config.modelY) : 0;128 const modelZ = config.modelZ !== undefined ? parseFloat(config.modelZ) : 0;129 130 const modelScale = config.modelScale !== undefined ? parseFloat(config.modelScale) : 1;131 132 const modelRotationX = config.modelRotationX !== undefined ? parseFloat(config.modelRotationX) : 0;133 const modelRotationY = config.modelRotationY !== undefined ? parseFloat(config.modelRotationY) : 0;134 const modelRotationZ = config.modelRotationZ !== undefined ? parseFloat(config.modelRotationZ) : 0;135 136 // Direct camera coordinates137 const cameraX = config.cameraX !== undefined ? parseFloat(config.cameraX) : 0;138 const cameraY = config.cameraY !== undefined ? parseFloat(config.cameraY) : 2;139 const cameraZ = config.cameraZ !== undefined ? parseFloat(config.cameraZ) : 5;140 141 // Camera coordinates for mobile devices142 const cameraXPhone = config.cameraXPhone !== undefined ? parseFloat(config.cameraXPhone) : cameraX;143 const cameraYPhone = config.cameraYPhone !== undefined ? parseFloat(config.cameraYPhone) : cameraY;144 const cameraZPhone = config.cameraZPhone !== undefined ? parseFloat(config.cameraZPhone) : cameraZ * 1.5;145 146 // Detect if the device is iOS.147 const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;148 // Also detect Android devices.149 const isMobile = isIOS || /Android/i.test(navigator.userAgent);150 151 // Choose the appropriate coordinates based on device type152 const chosenCameraX = isMobile ? cameraXPhone : cameraX;153 const chosenCameraY = isMobile ? cameraYPhone : cameraY;154 const chosenCameraZ = isMobile ? cameraZPhone : cameraZ;155 156 // Log device detection and camera coordinates for debugging157 console.log(`Device detected: ${isMobile ? 'Mobile' : 'Desktop'}`);158 console.log(`Camera coordinates: Desktop (${cameraX}, ${cameraY}, ${cameraZ}), Mobile (${cameraXPhone}, ${cameraYPhone}, ${cameraZPhone})`);159 console.log(`Chosen camera coordinates: (${chosenCameraX}, ${chosenCameraY}, ${chosenCameraZ})`);160 161 // Determine the aspect ratio.162 let aspectPercent = "100%";163 if (config.aspect) {164 if (config.aspect.indexOf(":") !== -1) {165 const parts = config.aspect.split(":");166 const w = parseFloat(parts[0]);167 const h = parseFloat(parts[1]);168 if (!isNaN(w) && !isNaN(h) && w > 0) {169 aspectPercent = (h / w * 100) + "%";170 }171 } else {172 const aspectValue = parseFloat(config.aspect);173 if (!isNaN(aspectValue) && aspectValue > 0) {174 aspectPercent = (100 / aspectValue) + "%";175 }176 }177 } else {178 const parentContainer = scriptTag.parentNode;179 const containerWidth = parentContainer.offsetWidth;180 const containerHeight = parentContainer.offsetHeight;181 if (containerWidth > 0 && containerHeight > 0) {182 aspectPercent = (containerHeight / containerWidth * 100) + "%";183 }184 }185 186 // Create the widget container.187 const widgetContainer = document.createElement('div');188 widgetContainer.id = 'ply-widget-container-' + instanceId;189 widgetContainer.classList.add('ply-widget-container');190 // Add a mobile class if on a phone.191 if (isMobile) {192 widgetContainer.classList.add('mobile');193 }194 // Set inline style for aspect ratio.195 widgetContainer.style.height = "0";196 widgetContainer.style.paddingBottom = aspectPercent;197 198 widgetContainer.innerHTML = `199 <!-- GIF Preview Container -->200 <div id="gif-preview-container-${instanceId}" class="gif-preview-container">201 <img id="preview-image-${instanceId}" alt="Preview" crossorigin="anonymous">202 </div>203 <!-- Viewer Container -->204 <div id="viewer-container-${instanceId}" class="viewer-container" style="display: none;">205 <canvas id="canvas-${instanceId}" class="ply-canvas"></canvas>206 <div id="progress-dialog-${instanceId}" class="progress-dialog">207 <progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>208 </div>209 <button id="close-btn-${instanceId}" class="widget-button close-btn">X</button>210 <button id="fullscreen-toggle-${instanceId}" class="widget-button fullscreen-toggle">⇱</button>211 <button id="help-toggle-${instanceId}" class="widget-button help-toggle">?</button>212 <button id="reset-camera-btn-${instanceId}" class="widget-button reset-camera-btn">213 <span class="reset-icon">⟲</span>214 </button>215 <div id="menu-content-${instanceId}" class="menu-content"></div>216 </div>217 `;218 scriptTag.parentNode.appendChild(widgetContainer);219 220 // Grab element references.221 const gifPreview = document.getElementById('gif-preview-container-' + instanceId);222 const viewerContainer = document.getElementById('viewer-container-' + instanceId);223 let previewImage = document.getElementById('preview-image-' + instanceId);224 const closeBtn = document.getElementById('close-btn-' + instanceId);225 const fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId);226 const helpToggle = document.getElementById('help-toggle-' + instanceId);227 const resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId);228 const menuContent = document.getElementById('menu-content-' + instanceId);229 let canvas = document.getElementById('canvas-' + instanceId);230 let progressDialog = document.getElementById('progress-dialog-' + instanceId);231 let progressIndicator = document.getElementById('progress-indicator-' + instanceId);232 233 // Flag to track if mouse is over the viewer234 let isMouseOverViewer = false;235 236 // Add mouse hover tracking for the viewer container237 viewerContainer.addEventListener('mouseenter', function() {238 isMouseOverViewer = true;239 });240 241 viewerContainer.addEventListener('mouseleave', function() {242 isMouseOverViewer = false;243 });244 245 // Set help instructions based on device type.246 if (isMobile) {247 menuContent.innerHTML = `248 - Pour vous déplacer, glissez deux doigts sur l'écran.<br>249 - Pour orbiter, utilisez un doigt.<br>250 - Pour zoomer, pincez avec deux doigts.251 `;252 } else {253 menuContent.innerHTML = `254 - orbitez avec le clic droit<br>255 - zoomez avec la molette<br>256 - déplacez vous avec le clic gauche257 `;258 }259 260 // Function to recreate the canvas - helps with WebGL context issues261 function recreateCanvas() {262 if (canvas) {263 // Remove the old canvas264 const oldCanvas = canvas;265 const parent = oldCanvas.parentNode;266 267 // Create a new canvas with the same attributes268 const newCanvas = document.createElement('canvas');269 newCanvas.id = 'canvas-' + instanceId;270 newCanvas.className = 'ply-canvas';271 272 // Replace the old canvas with the new one273 parent.replaceChild(newCanvas, oldCanvas);274 275 // Update the canvas reference276 canvas = newCanvas;277 278 console.log("Canvas recreated for fresh WebGL context");279 }280 }281 282 // Handle GIF configuration283 if (gifUrl) {284 previewImage.src = gifUrl;285 gifPreview.style.display = 'block';286 viewerContainer.style.display = 'none';287 } else {288 gifPreview.style.display = 'none';289 viewerContainer.style.display = 'block';290 closeBtn.style.display = 'none';291 // Initialize the viewer only when needed292 setTimeout(() => {293 initializeViewer();294 }, 100);295 }296 297 // --- Button Event Handlers ---298 if (gifUrl) {299 // Add click event to the GIF container300 gifPreview.addEventListener('click', function() {301 console.log("GIF preview clicked, showing 3D viewer");302 gifPreview.style.display = 'none';303 viewerContainer.style.display = 'block';304 305 // Recreate canvas for a fresh WebGL context306 recreateCanvas();307 308 // Small delay to ensure DOM updates before initialization309 setTimeout(() => {310 // Always initialize the viewer when the GIF is clicked311 initializeViewer();312 }, 100);313 });314 }315 316 // Function to clean up the viewer completely317 function cleanupViewer() {318 console.log("Starting viewer cleanup...");319 320 // Clear any running intervals321 if (progressChecker) {322 clearInterval(progressChecker);323 progressChecker = null;324 }325 326 // Remove wheel event listeners327 for (const handler of wheelHandlers) {328 const [element, func] = handler;329 element.removeEventListener('wheel', func, { passive: false });330 }331 wheelHandlers = [];332 333 // Remove resize handler if it exists334 if (resizeHandler) {335 window.removeEventListener('resize', resizeHandler);336 resizeHandler = null;337 }338 339 // Reset the canvas context340 if (canvas) {341 const ctx = canvas.getContext('webgl2') || canvas.getContext('webgl');342 if (ctx && ctx.getExtension('WEBGL_lose_context')) {343 try {344 ctx.getExtension('WEBGL_lose_context').loseContext();345 } catch (e) {346 console.error("Error releasing WebGL context:", e);347 }348 }349 }350 351 // Destroy PlayCanvas app if it exists352 if (app) {353 try {354 app.destroy();355 } catch (e) {356 console.error("Error destroying PlayCanvas app:", e);357 }358 app = null;359 }360 361 // Reset entities362 cameraEntity = null;363 modelEntity = null;364 365 // Mark the viewer as not initialized - this is crucial for proper reinitialization366 viewerInitialized = false;367 368 console.log("Viewer cleanup complete");369 }370 371 // Close button event handler372 closeBtn.addEventListener('click', function() {373 console.log("Close button clicked");374 375 // Handle fullscreen exit376 if (document.fullscreenElement === widgetContainer) {377 if (document.exitFullscreen) {378 document.exitFullscreen();379 }380 }381 if (widgetContainer.classList.contains('fake-fullscreen')) {382 widgetContainer.classList.remove('fake-fullscreen');383 fullscreenToggle.textContent = '⇱';384 }385 386 // Clean up the viewer387 cleanupViewer();388 389 // Hide viewer and show GIF390 viewerContainer.style.display = 'none';391 gifPreview.style.display = 'block';392 });393 394 // Fullscreen toggle handler395 fullscreenToggle.addEventListener('click', function() {396 if (isIOS) {397 if (!widgetContainer.classList.contains('fake-fullscreen')) {398 widgetContainer.classList.add('fake-fullscreen');399 } else {400 widgetContainer.classList.remove('fake-fullscreen');401 resetCamera();402 }403 fullscreenToggle.textContent = widgetContainer.classList.contains('fake-fullscreen') ? '⇲' : '⇱';404 } else {405 if (!document.fullscreenElement) {406 if (widgetContainer.requestFullscreen) {407 widgetContainer.requestFullscreen();408 } else if (widgetContainer.webkitRequestFullscreen) {409 widgetContainer.webkitRequestFullscreen();410 } else if (widgetContainer.mozRequestFullScreen) {411 widgetContainer.mozRequestFullScreen();412 } else if (widgetContainer.msRequestFullscreen) {413 widgetContainer.msRequestFullscreen();414 }415 } else {416 if (document.exitFullscreen) {417 document.exitFullscreen();418 }419 }420 }421 });422 423 // Listen for native fullscreen changes.424 document.addEventListener('fullscreenchange', function() {425 if (document.fullscreenElement === widgetContainer) {426 fullscreenToggle.textContent = '⇲';427 widgetContainer.style.height = '100%';428 widgetContainer.style.paddingBottom = '0';429 resetCamera();430 } else {431 fullscreenToggle.textContent = '⇱';432 widgetContainer.style.height = '0';433 widgetContainer.style.paddingBottom = aspectPercent;434 resetCamera();435 }436 });437 438 // Help toggle button439 helpToggle.addEventListener('click', function(e) {440 e.stopPropagation();441 menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block';442 });443 444 // --- Camera Reset Function ---445 function resetCamera() {446 if (!cameraEntity || !modelEntity || !app) {447 console.log("Cannot reset camera - missing entities or app");448 return;449 }450 451 try {452 // Get the orbit camera script453 const orbitCam = cameraEntity.script.orbitCamera;454 if (!orbitCam) {455 console.log("Cannot reset camera - missing orbit camera script");456 return;457 }458 459 console.log(`Resetting camera to: (${chosenCameraX}, ${chosenCameraY}, ${chosenCameraZ}) for ${isMobile ? 'mobile' : 'desktop'}`);460 461 // Store model position462 const modelPos = modelEntity.getPosition();463 464 // 1. Create a temporary entity to help calculate new values465 const tempEntity = new pc.Entity();466 tempEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);467 tempEntity.lookAt(modelPos);468 469 // 2. Calculate the distance between camera and model470 const distance = new pc.Vec3().sub2(471 new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),472 modelPos473 ).length();474 475 // 3. Set camera position476 cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);477 cameraEntity.lookAt(modelPos);478 479 // 4. Update the orbit camera's pivot point480 orbitCam.pivotPoint = new pc.Vec3(modelPos.x, modelPos.y, modelPos.z);481 482 // 5. Set the distance483 orbitCam._targetDistance = distance;484 orbitCam._distance = distance;485 486 // 6. Calculate and set yaw and pitch from the camera's rotation487 const rotation = tempEntity.getRotation();488 const tempForward = new pc.Vec3();489 rotation.transformVector(pc.Vec3.FORWARD, tempForward);490 491 const yaw = Math.atan2(-tempForward.x, -tempForward.z) * pc.math.RAD_TO_DEG;492 493 const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);494 const rotWithoutYaw = new pc.Quat().mul2(yawQuat, rotation);495 const forwardWithoutYaw = new pc.Vec3();496 rotWithoutYaw.transformVector(pc.Vec3.FORWARD, forwardWithoutYaw);497 const pitch = Math.atan2(forwardWithoutYaw.y, -forwardWithoutYaw.z) * pc.math.RAD_TO_DEG;498 499 // Set yaw and pitch directly on internal variables500 orbitCam._targetYaw = yaw;501 orbitCam._yaw = yaw;502 orbitCam._targetPitch = pitch;503 orbitCam._pitch = pitch;504 505 // Force update506 if (typeof orbitCam._updatePosition === 'function') {507 orbitCam._updatePosition();508 }509 510 // Clean up511 tempEntity.destroy();512 513 console.log("Camera reset complete");514 515 } catch (error) {516 console.error("Error resetting camera:", error);517 }518 }519 520 // Reset camera button event handler521 resetCameraBtn.addEventListener('click', function() {522 console.log("Reset camera button clicked");523 resetCamera();524 });525 526 // Escape key handler for fullscreen exit527 document.addEventListener('keydown', function(e) {528 if (e.key === 'Escape' || e.key === 'Esc') {529 let wasFullscreen = false;530 if (document.fullscreenElement === widgetContainer) {531 wasFullscreen = true;532 if (document.exitFullscreen) {533 document.exitFullscreen();534 }535 }536 if (widgetContainer.classList.contains('fake-fullscreen')) {537 wasFullscreen = true;538 widgetContainer.classList.remove('fake-fullscreen');539 fullscreenToggle.textContent = '⇱';540 }541 if (wasFullscreen) {542 resetCamera();543 }544 }545 });546 547 // --- Prevent app from hijacking all wheel events ---548 function setupWheelHandlers() {549 // First remove any existing handlers550 for (const handler of wheelHandlers) {551 const [element, func] = handler;552 element.removeEventListener('wheel', func, { passive: false });553 }554 wheelHandlers = [];555 556 // Create new wheel handler557 const handleWheel = function(event) {558 // Check if mouse is over the viewer559 if (!isMouseOverViewer) {560 // Allow normal page scrolling561 return true;562 }563 564 // Otherwise apply zooming, but prevent default only for viewer area565 event.stopPropagation();566 567 if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {568 const camera = cameraEntity.camera;569 const orbitCamera = cameraEntity.script.orbitCamera;570 const sensitivity = cameraEntity.script.orbitCameraInputMouse ? 571 cameraEntity.script.orbitCameraInputMouse.distanceSensitivity || 0.4 : 0.4;572 573 if (camera.projection === pc.PROJECTION_PERSPECTIVE) {574 orbitCamera.distance -= event.deltaY * 0.01 * sensitivity * (orbitCamera.distance * 0.1);575 } else {576 orbitCamera.orthoHeight -= event.deltaY * 0.01 * sensitivity * (orbitCamera.orthoHeight * 0.1);577 }578 579 event.preventDefault();580 }581 };582 583 // Add wheel handlers and store references for cleanup584 viewerContainer.addEventListener('wheel', handleWheel, { passive: false });585 canvas.addEventListener('wheel', handleWheel, { passive: false });586 587 // Store handlers for later cleanup588 wheelHandlers.push([viewerContainer, handleWheel]);589 wheelHandlers.push([canvas, handleWheel]);590 591 console.log("Wheel handlers set up");592 }593 594 // --- Initialize the 3D PLY Viewer using PlayCanvas ---595 async function initializeViewer() {596 // Skip initialization if already initialized597 if (viewerInitialized && app) {598 console.log("Viewer already initialized and app exists, skipping initialization");599 return;600 }601 602 console.log("Initializing PLY viewer...");603 progressDialog.style.display = 'block';604 progressIndicator.value = 0;605 606 // Initialize PlayCanvas607 const deviceType = "webgl2";608 const gfxOptions = {609 deviceTypes: [deviceType],610 glslangUrl: `https://playcanvas.vercel.app/static/lib/glslang/glslang.js`,611 twgslUrl: `https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js`,612 antialias: false613 };614 615 try {616 // Create graphics device617 const device = await pc.createGraphicsDevice(canvas, gfxOptions);618 device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);619 620 // Create app621 const createOptions = new pc.AppOptions();622 createOptions.graphicsDevice = device;623 createOptions.mouse = new pc.Mouse(canvas);624 createOptions.touch = new pc.TouchDevice(canvas);625 createOptions.componentSystems = [626 pc.RenderComponentSystem,627 pc.CameraComponentSystem,628 pc.LightComponentSystem,629 pc.ScriptComponentSystem,630 pc.GSplatComponentSystem631 ];632 createOptions.resourceHandlers = [633 pc.TextureHandler,634 pc.ContainerHandler,635 pc.ScriptHandler,636 pc.GSplatHandler637 ];638 639 app = new pc.AppBase(canvas);640 app.init(createOptions);641 642 // Set canvas fill mode to match the container643 app.setCanvasFillMode(pc.FILLMODE_NONE);644 app.setCanvasResolution(pc.RESOLUTION_AUTO);645 646 // Set scene options647 app.scene.exposure = 0.8;648 app.scene.toneMapping = pc.TONEMAP_ACES;649 650 // Handle window resizing651 const resize = () => {652 if (app) {653 app.resizeCanvas(canvas.clientWidth, canvas.clientHeight);654 }655 };656 657 // Store resize handler for cleanup658 resizeHandler = resize;659 window.addEventListener('resize', resizeHandler);660 661 // Add cleanup when app is destroyed662 app.on('destroy', () => {663 if (resizeHandler) {664 window.removeEventListener('resize', resizeHandler);665 resizeHandler = null;666 }667 if (progressChecker) {668 clearInterval(progressChecker);669 progressChecker = null;670 }671 });672 673 // Load required assets674 const assets = {675 model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),676 orbit: new pc.Asset('script', 'script', { url: `https://bilca-visionneur-play-canva-2.static.hf.space/orbit-camera.js` })677 };678 679 // Create asset loader with progress tracking680 const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);681 682 // Handle asset loading progress683 let lastProgress = 0;684 assets.model.on('load', (asset) => {685 progressDialog.style.display = 'none';686 console.log("Model loaded successfully");687 });688 689 assets.model.on('error', (err) => {690 console.error("Error loading PLY file:", err);691 progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;692 viewerInitialized = false;693 });694 695 // Set up progress monitoring696 const checkProgress = () => {697 if (!app) {698 clearInterval(progressChecker);699 progressChecker = null;700 return;701 }702 703 if (app && assets.model.resource) {704 progressIndicator.value = 100;705 clearInterval(progressChecker);706 progressChecker = null;707 progressDialog.style.display = 'none';708 } else if (assets.model.loading) {709 // Increment progress for visual feedback710 lastProgress += 2;711 if (lastProgress > 90) lastProgress = 90; // Cap at 90% until fully loaded712 progressIndicator.value = lastProgress;713 }714 };715 716 progressChecker = setInterval(checkProgress, 100);717 718 // Load assets and set up scene719 assetListLoader.load(() => {720 if (!app) {721 console.log("App was destroyed during asset loading");722 return;723 }724 725 app.start();726 727 // Create model entity728 modelEntity = new pc.Entity('model');729 modelEntity.addComponent('gsplat', {730 asset: assets.model731 });732 733 // Position the model using JSON parameters734 modelEntity.setLocalPosition(modelX, modelY, modelZ);735 modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);736 modelEntity.setLocalScale(modelScale, modelScale, modelScale);737 738 app.root.addChild(modelEntity);739 740 // Create camera entity741 cameraEntity = new pc.Entity('camera');742 cameraEntity.addComponent('camera', {743 clearColor: new pc.Color(744 config.canvas_background ? parseInt(config.canvas_background.substr(1, 2), 16) / 255 : 0,745 config.canvas_background ? parseInt(config.canvas_background.substr(3, 2), 16) / 255 : 0,746 config.canvas_background ? parseInt(config.canvas_background.substr(5, 2), 16) / 255 : 0747 ),748 toneMapping: pc.TONEMAP_ACES749 });750 751 // Set camera position directly using X, Y, Z coordinates from config752 // Log the chosen camera position for debugging753 console.log(`Setting camera position for ${isMobile ? 'mobile' : 'desktop'}: (${chosenCameraX}, ${chosenCameraY}, ${chosenCameraZ})`);754 755 cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);756 cameraEntity.lookAt(modelEntity.getPosition());757 758 // Add orbit camera script for interactive navigation759 cameraEntity.addComponent('script');760 cameraEntity.script.create('orbitCamera', {761 attributes: {762 inertiaFactor: 0.2,763 focusEntity: modelEntity,764 distanceMax: maxZoom,765 distanceMin: minZoom,766 pitchAngleMax: maxAngle,767 pitchAngleMin: minAngle,768 yawAngleMax: maxAzimuth,769 yawAngleMin: minAzimuth,770 frameOnStart: false // Don't auto-frame since we're setting position directly771 }772 });773 774 // Create input controllers but don't add mouse wheel handling - we handle that separately775 cameraEntity.script.create('orbitCameraInputMouse', {776 attributes: {777 orbitSensitivity: isMobile ? 0.6 : 0.3,778 distanceSensitivity: isMobile ? 0.5 : 0.4779 }780 });781 782 // Disable wheel event in the orbit camera input783 if (cameraEntity.script.orbitCameraInputMouse) {784 // Override mouse wheel to do nothing - we handle wheel events separately785 cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};786 }787 788 // Add touch input controller789 cameraEntity.script.create('orbitCameraInputTouch', {790 attributes: {791 orbitSensitivity: 0.6,792 distanceSensitivity: 0.5793 }794 });795 796 //