CoolFace
Apppublic

bilca/module_de_visualisation

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
index.js796 linesDownload Raw Back to js_scripts
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        //