UNPKG

js-subpcd

Version:

🌟 High-performance JavaScript/TypeScript QuadTree point cloud filtering and processing library. Published on npm as js-subpcd with PCL.js compatible API for spatial filtering, subsampling, and nearest neighbor search.

1,150 lines (968 loc) • 98.6 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>2.5D Point Cloud QuadTree Demo</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&family=Roboto+Mono:wght@300;400;500&display=swap" rel="stylesheet"> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <style> body { margin: 0; padding: 0; background: #1a1a1a; color: white; font-family: 'Roboto', sans-serif; font-weight: normal; overflow: hidden; } *, *::before, *::after { color: white !important; font-weight: normal !important; } canvas { color: initial !important; } .log-area { position: absolute; top: 20px; left: 20px; z-index: 999; width: 300px; height: 200px; font-family: 'Roboto Mono', monospace; font-size: 10px; color: rgba(255, 255, 255, 0.6); overflow: hidden; pointer-events: none; line-height: 1.2; } .progress-bar { display: inline-block; width: 200px; height: 6px; background-color: #2c3e50; border-radius: 3px; overflow: hidden; margin: 0 8px; vertical-align: middle; } .progress-fill { height: 100%; background: linear-gradient(90deg, #27ae60, #2ecc71); border-radius: 3px; transition: width 0.2s ease; } .progress-line { color: rgba(255, 255, 255, 0.8); font-weight: 500; } .minimal-stats { position: absolute; top: 20px; right: 20px; z-index: 1000; font-family: 'Roboto Mono', monospace; font-size: 11px; color: rgba(255, 255, 255, 0.7); line-height: 1.3; pointer-events: none; } .minimal-stats div { margin: 2px 0; } #canvas-container { width: 100vw; height: 100vh; } .timer { font-family: 'Roboto Mono', monospace; font-size: 11px; color: rgba(255, 255, 255, 0.7); margin: 2px 0; pointer-events: none; } .controls-overlay { position: absolute; bottom: 20px; left: 20px; z-index: 1000; background: rgba(0, 0, 0, 0.8); padding: 12px 16px; border-radius: 8px; font-family: 'Roboto Mono', monospace; font-size: 11px; color: rgba(255, 255, 255, 0.8); line-height: 1.4; pointer-events: none; backdrop-filter: blur(4px); } .controls-overlay .key { color: #3498db; font-weight: 500; } </style> </head> <body> <div class="log-area" id="logArea"></div> <div class="minimal-stats"> <div id="datasetDisplay">Mountain Range (500K)</div> <div>Total: <span id="totalPoints">0</span></div> <div>Displayed: <span id="displayedPoints">0</span></div> <div>Grid: <span id="subsampleDisplay">No Sampling</span></div> <div>FPS: <span id="fpsDisplay">60</span></div> <div>Quality: <span id="qualityDisplay">100%</span></div> </div> <!-- Hidden select for dataset management --> <select id="datasetSelect" style="display: none;"> <option value="mountain">Mountain Range (500K)</option> <option value="alpine">Alpine Valley (750K)</option> <option value="canyon">Grand Canyon (1M)</option> <option value="synthetic">Synthetic Terrain (62.5K)</option> <option value="lidar">LiDAR Sample (35K)</option> </select> <div class="controls-overlay"> <div><span class="key">A/D</span> Switch datasets</div> <div><span class="key">W/S</span> More/Less points</div> <div><span class="key">Space</span> Toggle rotation</div> <div><span class="key">Mouse</span> Orbit camera</div> <div><span class="key">Scroll</span> Zoom</div> </div> <div id="canvas-container"></div> <script src="quadtree-wasm-wrapper.js"></script> <script src="js-quadpcd-browser.js"></script> <script> // Global variables let scene, camera, renderer, controls; let currentPointCloud = null; let quadTreeFilter = null; let pointsMesh = null; let originalPoints = []; let filteredPoints = []; let currentSubsampleLevel = 0; // Default to no sampling let gridHelper = null; let startTime = Date.now(); let timerInterval = null; // Enhanced performance optimization variables let lastFrameTime = 0; let targetFrameTime = 16.67; // 60 FPS target (1000ms / 60fps) let performanceMode = false; let adaptiveQuality = true; let renderLOD = 1.0; // Level of detail multiplier (0.1 = 10% of points) let lastFPSCheck = 0; let frameTimeHistory = []; const MAX_FRAME_HISTORY = 60; // Track last 60 frames // Web Worker for QuadTree operations let quadTreeWorker = null; let isWorkerReady = false; // Timer control let timerStarted = false; // Enhanced loading state management let isLoadingDataset = false; let currentDatasetId = null; // Track current dataset to prevent race conditions let pendingDatasetLoad = null; // Track pending dataset change // Performance monitoring and memory management let renderStats = { frameCount: 0, lastTime: performance.now(), fps: 0 }; // Memory management tracking let memoryStats = { lastGC: Date.now(), gcInterval: 30000, // Force GC every 30 seconds geometryPool: [], // Pool for reusing geometries materialPool: [], // Pool for reusing materials maxPoolSize: 5 }; // Log area functionality const logArea = document.getElementById('logArea'); const maxLogLines = 15; let logLines = []; function addLog(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); let logEntry; if (type === 'progress' || type === 'complete') { // For progress bars, don't add timestamp, use HTML directly logEntry = message; // Replace the last progress line if it exists if (logLines.length > 0 && (logLines[logLines.length - 1].includes('progress-bar') || logLines[logLines.length - 1].includes('[â–ˆ'))) { logLines[logLines.length - 1] = logEntry; } else { logLines.push(logEntry); } } else { // Regular log entries with timestamp logEntry = `[${timestamp}] ${message}`; logLines.push(logEntry); } if (logLines.length > maxLogLines) { logLines.shift(); } logArea.innerHTML = logLines.join('<br>'); } // Override console.log to capture messages const originalConsoleLog = console.log; console.log = function(...args) { originalConsoleLog.apply(console, args); addLog(args.join(' ')); }; // Add initial system info addLog('Point Cloud QuadTree Demo initialized'); addLog(`Browser: ${navigator.userAgent.split(' ')[0]}`); addLog(`WebGL: ${renderer ? 'Available' : 'Checking...'}`); // Base subsampling levels - will be updated dynamically let subsampleLevels = [ { value: 2, display: '4x4' }, { value: 3, display: '8x8' }, { value: 4, display: '16x16' }, { value: 5, display: '32x32' }, { value: 6, display: '64x64' }, { value: 7, display: '128x128' }, { value: 8, display: '256x256' } ]; // Update subsampling levels based on point cloud size function updateSubsamplingLevels(pointCount) { console.log(`Updating subsampling levels for ${pointCount} points`); if (pointCount < 10000) { // Small datasets (< 10K points) subsampleLevels = [ { value: 0, display: 'No Sampling' }, { value: 2, display: '4x4' }, { value: 3, display: '8x8' }, { value: 4, display: '16x16' }, { value: 5, display: '32x32' }, { value: 6, display: '64x64' } ]; currentSubsampleLevel = 0; // Default to no sampling for small datasets } else if (pointCount < 100000) { // Medium datasets (10K - 100K points) subsampleLevels = [ { value: 0, display: 'No Sampling' }, { value: 3, display: '8x8' }, { value: 4, display: '16x16' }, { value: 5, display: '32x32' }, { value: 6, display: '64x64' }, { value: 7, display: '128x128' }, { value: 8, display: '256x256' } ]; currentSubsampleLevel = 5; // Default to 32x32 for medium datasets } else if (pointCount < 500000) { // Large datasets (100K - 500K points) subsampleLevels = [ { value: 0, display: 'No Sampling' }, { value: 4, display: '16x16' }, { value: 5, display: '32x32' }, { value: 6, display: '64x64' }, { value: 7, display: '128x128' }, { value: 8, display: '256x256' }, { value: 9, display: '512x512' }, { value: 10, display: '1024x1024' } ]; currentSubsampleLevel = 6; // Default to 64x64 for large datasets } else { // Very large datasets (500K+ points) subsampleLevels = [ { value: 0, display: 'No Sampling' }, { value: 5, display: '32x32' }, { value: 6, display: '64x64' }, { value: 7, display: '128x128' }, { value: 8, display: '256x256' }, { value: 9, display: '512x512' }, { value: 10, display: '1024x1024' }, { value: 11, display: '2048x2048' } ]; currentSubsampleLevel = 6; // Default to 64x64 for good performance } console.log(`Subsampling levels updated:`, subsampleLevels.map(l => l.display)); console.log(`Default level set to: ${subsampleLevels.find(l => l.value === currentSubsampleLevel)?.display || 'N/A'}`); // Update the UI to show the current level updateSubsampleDisplay(); } // Initialize Web Worker for QuadTree operations function initializeWorker() { if (window.Worker) { try { quadTreeWorker = new Worker('quadtree-worker.js'); quadTreeWorker.onmessage = function(e) { const { type, data, error, points, processingTime, depth, gridSize, expectedMax, bounds, pointCount, message, progress } = e.data; switch (type) { case 'DATASET_LOADED': console.log('Dataset loaded in worker thread'); originalPoints = points.map(p => new PointXYZ(p.x, p.y, p.z)); // Now build the tree with the loaded dataset console.log('Building QuadTree in worker...'); showLoading('Building QuadTree in worker...'); // Calculate bounds for tree building const coords = { x: points.map(p => p.x), y: points.map(p => p.y), z: points.map(p => p.z) }; let minX = coords.x[0], maxX = coords.x[0]; let minY = coords.y[0], maxY = coords.y[0]; for (let i = 1; i < coords.x.length; i++) { if (coords.x[i] < minX) minX = coords.x[i]; if (coords.x[i] > maxX) maxX = coords.x[i]; } for (let i = 1; i < coords.y.length; i++) { if (coords.y[i] < minY) minY = coords.y[i]; if (coords.y[i] > maxY) maxY = coords.y[i]; } const bounds = [minX, minY, maxX, maxY]; // Send to worker for tree building quadTreeWorker.postMessage({ type: 'BUILD_TREE', data: { coords, bounds } }); break; case 'TREE_BUILT': console.log(`QuadTree built in worker: ${pointCount} points`); isWorkerReady = true; // Update loading message for final steps loadingMessage = 'Finalizing visualization...'; loadingProgress = 90; updateLoadingProgress(); // Update subsampling levels based on dataset size updateSubsamplingLevels(pointCount); updateSubsampleDisplay(); // Check if this is still the active dataset load if (currentDatasetId !== datasetId) { console.log(`Dataset ${datasetId} cancelled during processing`); return; } // Apply initial subsampling (timer will start after first frame) if (currentSubsampleLevel === 0) { // No sampling - display all points filteredPoints = originalPoints; requestAnimationFrame(() => { // Double-check we're still the active dataset if (currentDatasetId === datasetId) { displayPointCloudAsync(filteredPoints); updateStats(); hideLoading(); // Complete loading startTimerAfterFirstFrame(); isLoadingDataset = false; // Reset loading flag currentDatasetId = null; // Clear current dataset ID } }); } else { applySubsamplingWorker(); } break; case 'FILTER_COMPLETE': console.log(`Worker filtering complete: depth ${depth} (${gridSize}x${gridSize}) -> ${points.length} points (expected max ${expectedMax}) in ${processingTime.toFixed(2)}ms`); // Handle both initial loading and subsampling operations filteredPoints = points.map(p => new PointXYZ(p.x, p.y, p.z)); // Schedule visualization update on next frame requestAnimationFrame(() => { displayPointCloudAsync(filteredPoints); updateStats(); hideLoading(); // Only handle timer and loading state for initial dataset loading if (currentDatasetId && isLoadingDataset) { // Start timer after first visualization frame is rendered startTimerAfterFirstFrame(); // Reset loading flag and clear dataset ID isLoadingDataset = false; currentDatasetId = null; } }); break; case 'PROGRESS': console.log(`Worker progress: ${message} (${progress}%)`); break; case 'ERROR': console.error('Worker error:', error); hideLoading(); // Fallback to main thread console.log('Falling back to main thread processing...'); isWorkerReady = false; // If we have original points, initialize main thread processing if (originalPoints && originalPoints.length > 0) { console.log('Initializing main thread processing with existing data...'); loadDatasetMainThread(originalPoints); } break; } }; quadTreeWorker.onerror = function(error) { console.error('Worker error:', error); isWorkerReady = false; // Fallback to main thread initializeMainThread(); // If we have original points, initialize main thread processing if (originalPoints && originalPoints.length > 0) { console.log('Initializing main thread processing with existing data...'); loadDatasetMainThread(originalPoints); } }; console.log('QuadTree Web Worker initialized'); isWorkerReady = true; } catch (error) { console.warn('Web Worker not supported, using main thread:', error); initializeMainThread(); } } else { console.warn('Web Workers not supported, using main thread'); initializeMainThread(); } } // Fallback to main thread processing function initializeMainThread() { console.log('Using main thread for QuadTree operations'); isWorkerReady = false; } // Start timer after first visualization frame function startTimerAfterFirstFrame() { if (!timerStarted) { if (!document.getElementById('timer')) { startTimer(); } else { resetTimer(); } timerStarted = true; console.log('Timer started after first visualization frame rendered'); } } // Legacy function for main thread fallback function startTimerAfterTreeBuild() { // Timer will start after visualization in main thread mode too console.log('QuadTree construction completed - timer will start after visualization'); } // Simple orbit controls implementation class SimpleOrbitControls { constructor(camera, domElement) { this.camera = camera; this.domElement = domElement; this.target = new THREE.Vector3(0, 0, 0); this.enableDamping = true; this.dampingFactor = 0.05; this.rotateSpeed = 1.0; this.zoomSpeed = 1.2; this.panSpeed = 0.8; this.isMouseDown = false; this.mouseX = 0; this.mouseY = 0; this.phi = Math.PI / 2; // Start with XY plane horizontal this.theta = 0; this.radius = this.camera.position.length(); // Auto-rotation properties this.autoRotate = false; this.autoRotateSpeed = 36; // degrees per second (10 seconds per revolution) this.originalAutoRotateSpeed = 36; // Store original speed for reset this.bindEvents(); this.update(); } bindEvents() { this.domElement.addEventListener('mousedown', (e) => { this.isMouseDown = true; this.mouseX = e.clientX; this.mouseY = e.clientY; }); this.domElement.addEventListener('mousemove', (e) => { if (!this.isMouseDown) return; const deltaX = e.clientX - this.mouseX; const deltaY = e.clientY - this.mouseY; this.theta -= deltaX * 0.01 * this.rotateSpeed; this.phi += deltaY * 0.01 * this.rotateSpeed; this.phi = Math.max(0.1, Math.min(Math.PI - 0.1, this.phi)); this.mouseX = e.clientX; this.mouseY = e.clientY; }); this.domElement.addEventListener('mouseup', () => { this.isMouseDown = false; }); this.domElement.addEventListener('wheel', (e) => { e.preventDefault(); this.radius *= (e.deltaY > 0) ? this.zoomSpeed : (1 / this.zoomSpeed); this.radius = Math.max(1, Math.min(1000, this.radius)); }); } update(deltaTime = 16.67) { // Auto-rotation around vertical axis if (this.autoRotate) { // Convert degrees per second to radians per millisecond, then multiply by deltaTime const radiansPerMs = (this.autoRotateSpeed * Math.PI / 180) / 1000; this.theta += radiansPerMs * deltaTime; } // Ensure camera stays above zero surface (phi constraint) this.phi = Math.max(0.1, Math.min(Math.PI / 2 - 0.1, this.phi)); // Use spherical coordinates with Z as up const x = this.radius * Math.sin(this.phi) * Math.cos(this.theta); const y = this.radius * Math.sin(this.phi) * Math.sin(this.theta); const z = this.radius * Math.cos(this.phi); this.camera.position.set( this.target.x + x, this.target.y + y, this.target.z + z ); // Set up vector to Z this.camera.up.set(0, 0, 1); this.camera.lookAt(this.target); } } // Initialize 3D scene function init3DScene() { // Scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x1a1a1a); // Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000); camera.position.set(50, 30, 50); // Position camera above the XY plane camera.up.set(0, 0, 1); // Set Z as up direction // Renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.getElementById('canvas-container').appendChild(renderer.domElement); // Controls controls = new SimpleOrbitControls(camera, renderer.domElement); controls.target.set(0, 0, 0); // Lights const ambientLight = new THREE.AmbientLight(0x404040, 0.6); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(100, 100, 50); scene.add(directionalLight); // Grid will be created dynamically based on data bounds // Start render loop animate(); } function animate() { const now = performance.now(); const frameTime = now - lastFrameTime; // Enhanced frame rate limiting with adaptive quality if (frameTime < targetFrameTime) { requestAnimationFrame(animate); return; } lastFrameTime = now; requestAnimationFrame(animate); // Track frame time history for adaptive performance frameTimeHistory.push(frameTime); if (frameTimeHistory.length > MAX_FRAME_HISTORY) { frameTimeHistory.shift(); } // Performance monitoring and adaptive quality control renderStats.frameCount++; const deltaTime = now - renderStats.lastTime; if (deltaTime >= 1000) { renderStats.fps = Math.round((renderStats.frameCount * 1000) / deltaTime); renderStats.frameCount = 0; renderStats.lastTime = now; // Adaptive performance optimization with smoother transitions const avgFrameTime = frameTimeHistory.reduce((a, b) => a + b, 0) / frameTimeHistory.length; const wasInPerformanceMode = performanceMode; // More sophisticated performance mode detection if (renderStats.fps < 30 || avgFrameTime > 20) { if (!performanceMode) { performanceMode = true; adaptRenderQuality(0.7); // Reduce to 70% quality console.log('Entered performance mode: reducing render quality'); } } else if (renderStats.fps > 50 && avgFrameTime < 16) { if (performanceMode || renderLOD < 1.0) { performanceMode = false; adaptRenderQuality(1.0); // Full quality console.log('Exited performance mode: full render quality restored'); } } // Unified rotation speed if (controls.autoRotate) { controls.autoRotateSpeed = controls.originalAutoRotateSpeed; } // Memory management: Force garbage collection periodically const currentTime = Date.now(); if (currentTime - memoryStats.lastGC > memoryStats.gcInterval) { forceGarbageCollection(); memoryStats.lastGC = currentTime; } } // Update controls and render controls.update(frameTime); renderer.render(scene, camera); } // Force garbage collection and cleanup function forceGarbageCollection() { try { // Clear geometry pool if it gets too large if (memoryStats.geometryPool.length > memoryStats.maxPoolSize) { memoryStats.geometryPool.forEach(geom => geom.dispose()); memoryStats.geometryPool = []; } // Clear material pool if it gets too large if (memoryStats.materialPool.length > memoryStats.maxPoolSize) { memoryStats.materialPool.forEach(mat => { if (mat.map) mat.map.dispose(); mat.dispose(); }); memoryStats.materialPool = []; } // Force garbage collection if available if (window.gc) { window.gc(); console.log('Forced garbage collection'); } // Log memory usage if available if (performance.memory) { const used = Math.round(performance.memory.usedJSHeapSize / 1048576); const total = Math.round(performance.memory.totalJSHeapSize / 1048576); const limit = Math.round(performance.memory.jsHeapSizeLimit / 1048576); console.log(`Memory: ${used}MB used / ${total}MB total / ${limit}MB limit`); } } catch (error) { console.warn('Garbage collection failed:', error); } } // Adaptive render quality function function adaptRenderQuality(targetLOD) { if (!pointsMesh || !filteredPoints) return; renderLOD = targetLOD; // If quality is reduced significantly, create a simplified version if (renderLOD < 0.9 && filteredPoints.length > 50000) { const step = Math.max(1, Math.floor(1 / renderLOD)); const simplifiedPoints = []; for (let i = 0; i < filteredPoints.length; i += step) { simplifiedPoints.push(filteredPoints[i]); } // Update geometry with simplified points updatePointGeometry(simplifiedPoints); console.log(`Render LOD: ${(renderLOD * 100).toFixed(0)}% (${simplifiedPoints.length}/${filteredPoints.length} points)`); } } // Update point geometry without full recreation function updatePointGeometry(points) { if (!pointsMesh || !points || points.length === 0) return; const positions = new Float32Array(points.length * 3); const colors = new Float32Array(points.length * 3); // Calculate color range once let minZ = points[0].z, maxZ = points[0].z; for (const point of points) { minZ = Math.min(minZ, point.z); maxZ = Math.max(maxZ, point.z); } const rangeZ = maxZ - minZ || 1; // Fill arrays efficiently for (let i = 0; i < points.length; i++) { const point = points[i]; const idx = i * 3; positions[idx] = point.x; positions[idx + 1] = point.y; positions[idx + 2] = point.z; // Fast color calculation const normalizedHeight = (point.z - minZ) / rangeZ; const hue = (1 - normalizedHeight) * 0.67; const color = new THREE.Color().setHSL(hue, 1.0, 0.5); colors[idx] = color.r; colors[idx + 1] = color.g; colors[idx + 2] = color.b; } // Update attributes instead of recreating geometry pointsMesh.geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); pointsMesh.geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); pointsMesh.geometry.attributes.position.needsUpdate = true; pointsMesh.geometry.attributes.color.needsUpdate = true; // Update draw count for LOD pointsMesh.geometry.setDrawRange(0, points.length); } // Handle window resize window.addEventListener('resize', function() { if (camera && renderer) { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } }); // Reset timer function function resetTimer() { startTime = Date.now(); updateTimer(); // Update display immediately } // Reset timer state for new dataset function resetTimerState() { timerStarted = false; if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } // Remove existing timer element const existingTimer = document.getElementById('timer'); if (existingTimer) { existingTimer.remove(); } } // Reset plot window state for new dataset function resetPlotWindow() { // Reset performance mode performanceMode = false; renderLOD = 1.0; // Reset render stats renderStats.fps = 60; renderStats.frameCount = 0; renderStats.lastTime = performance.now(); frameTimeHistory = []; // Reset camera far plane to default if (camera) { camera.far = 10000; camera.updateProjectionMatrix(); } console.log('Plot window state reset for new dataset'); } // Force cleanup of current loading operation async function forceCleanupCurrentLoad() { console.log('Force cleaning up current load operation...'); // Stop worker operations if (quadTreeWorker && isWorkerReady) { try { // Send abort signal to worker quadTreeWorker.postMessage({ type: 'ABORT' }); } catch (error) { console.warn('Could not send abort to worker:', error); } } // Clear loading progress if (loadingProgressInterval) { clearInterval(loadingProgressInterval); loadingProgressInterval = null; } // Clear any build intervals if (window.buildInterval) { clearInterval(window.buildInterval); window.buildInterval = null; } // Clear current point cloud clearCurrentPointCloud(); // Small delay to ensure cleanup completes await new Promise(resolve => setTimeout(resolve, 10)); } // Clear current point cloud with object pooling function clearCurrentPointCloud() { if (pointsMesh) { console.log('Clearing current point cloud from scene...'); scene.remove(pointsMesh); // Pool geometry and material for reuse instead of disposing immediately if (pointsMesh.geometry && memoryStats.geometryPool.length < memoryStats.maxPoolSize) { memoryStats.geometryPool.push(pointsMesh.geometry); } else if (pointsMesh.geometry) { pointsMesh.geometry.dispose(); } if (pointsMesh.material && memoryStats.materialPool.length < memoryStats.maxPoolSize) { // Reset material state for reuse if (pointsMesh.material.map) { pointsMesh.material.map.dispose(); // Circle texture is small, dispose it } memoryStats.materialPool.push(pointsMesh.material); } else if (pointsMesh.material) { if (pointsMesh.material.map) { pointsMesh.material.map.dispose(); } pointsMesh.material.dispose(); } pointsMesh = null; } // Also clear the grid helper if (gridHelper) { scene.remove(gridHelper); gridHelper = null; } } // Get geometry from pool or create new one function getPooledGeometry() { if (memoryStats.geometryPool.length > 0) { const geometry = memoryStats.geometryPool.pop(); // Reset geometry attributes geometry.attributes = {}; geometry.setDrawRange(0, Infinity); console.log('Reusing pooled geometry'); return geometry; } return new THREE.BufferGeometry(); } // Get material from pool or create new one function getPooledMaterial(pointSize) { if (memoryStats.materialPool.length > 0) { const material = memoryStats.materialPool.pop(); // Reset material properties material.size = pointSize; material.map = createCircleTexture(); // Recreate texture console.log('Reusing pooled material'); return material; } return new THREE.PointsMaterial({ size: pointSize, vertexColors: true, sizeAttenuation: false, map: createCircleTexture(), alphaTest: 0.1, transparent: true }); } // Enhanced dataset loading with proper race condition handling async function loadDataset() { const datasetType = document.getElementById('datasetSelect').value; const datasetId = `${datasetType}_${Date.now()}`; // Unique ID for this load operation // Handle concurrent dataset load requests if (isLoadingDataset) { console.log('Dataset load in progress, cancelling and starting new load...'); // Mark current operation for cancellation if (currentDatasetId) { console.log(`Cancelling dataset load: ${currentDatasetId}`); } // Store pending request pendingDatasetLoad = datasetId; // Force cleanup of current operation await forceCleanupCurrentLoad(); } // Start new load operation isLoadingDataset = true; currentDatasetId = datasetId; pendingDatasetLoad = null; console.log(`Starting dataset load: ${datasetId} (${datasetType})`); // Immediately clear existing point cloud to prevent overlap clearCurrentPointCloud(); // Clear arrays to prevent stale data originalPoints = []; filteredPoints = []; // Reset all state for new dataset resetTimerState(); resetPlotWindow(); showLoading('Loading dataset...'); try { // Load dataset points in main thread let datasetPoints = []; switch(datasetType) { case 'mountain': datasetPoints = await generateMountainRange(); break; case 'alpine': datasetPoints = await generateAlpineValley(); break; case 'canyon': datasetPoints = await generateGrandCanyon(); break; case 'synthetic': datasetPoints = await generateSyntheticTerrain(); break; case 'lidar': datasetPoints = await generateLidarSample(); break; } console.log(`Generated ${datasetPoints.length} points for ${datasetType} dataset`); if (isWorkerReady && quadTreeWorker) { // Use worker for QuadTree operations console.log('Using worker for QuadTree construction...'); showLoading('Loading dataset in worker...'); // Store original points originalPoints = datasetPoints; // First load dataset in worker to initialize QuadTreeFilter quadTreeWorker.postMessage({ type: 'LOAD_DATASET', data: { points: datasetPoints.map(p => ({ x: p.x, y: p.y, z: p.z })), datasetType: datasetType } }); // Enable controls immediately updateSubsampleDisplay(); } else { // Fallback to main thread await loadDatasetMainThread(datasetPoints); } } catch (error) { console.error(`Error loading dataset ${currentDatasetId}:`, error); if (currentDatasetId === datasetId) { // Only handle error if this is still the current operation hideLoading(); isLoadingDataset = false; currentDatasetId = null; } } // Check if there's a pending dataset load after completion if (pendingDatasetLoad && pendingDatasetLoad !== datasetId) { console.log(`Processing pending dataset load: ${pendingDatasetLoad}`); setTimeout(() => loadDataset(), 50); // Small delay before loading pending dataset } } // Fallback main thread dataset loading async function loadDatasetMainThread(datasetPoints) { showLoading('Initializing dataset...'); // Initialize dataset await initializeDataset(datasetPoints); // Update loading message for QuadTree building loadingMessage = 'Building QuadTree...'; console.log('Auto-building QuadTree for caching...'); const coords = quadTreeFilter.dataLoader.getCoordinateArrays(); const bounds = quadTreeFilter.dataLoader.getBounds(); // Build tree with non-blocking operation and progress simulation await new Promise(resolve => { let buildProgress = 0; window.buildInterval = setInterval(() => { buildProgress += 10; if (buildProgress <= 90) { loadingProgress = buildProgress; updateLoadingProgress(); } }, 50); setTimeout(() => { if (window.buildInterval) { clearInterval(window.buildInterval); window.buildInterval = null; } quadTreeFilter.quadtreeManager.buildTreeFromArrays(coords.x, coords.y, coords.z, bounds); quadTreeFilter.bounds = bounds; quadTreeFilter._treeBuilt = true; loadingProgress = 100; updateLoadingProgress(); console.log(`QuadTree built and cached with ${coords.x.length} points`); resolve(); }, 500); // Simulate some build time }); startTimerAfterTreeBuild(); // Update subsampling levels based on dataset size updateSubsamplingLevels(coords.x.length); // Update subsampling display updateSubsampleDisplay(); // Apply initial subsampling or show all points if (currentSubsampleLevel === 0) { // No sampling - display all points filteredPoints = originalPoints; } else { await applySampling(); } // Display filtered points and start timer after first frame await displayPointCloud(filteredPoints); updateStats(); // Start timer after main thread visualization requestAnimationFrame(() => { startTimerAfterFirstFrame(); }); hideLoading(); isLoadingDataset = false; } // Generate Mountain Range (500K points) - Optimized async function generateMountainRange() { console.log('Generating Mountain Range dataset (500K points)...'); const points = []; const width = 800; const height = 625; const extent = 2000; // 2km range const totalPoints = width * height; // Pre-calculate common values const invWidth = 1 / width; const invHeight = 1 / height; const halfExtent = extent * 0.5; console.log(`[DEBUG] Mountain Range generation:`); console.log(` - Grid: ${width}x${height} = ${totalPoints} points`); console.log(` - Extent: ${extent} (${-extent/2} to ${extent/2})`); console.log(` - Expected X range: ${(0 * invWidth - 0.5) * extent} to ${((width-1) * invWidth - 0.5) * extent}`); console.log(` - Expected Y range: ${(0 * invHeight - 0.5) * extent} to ${((height-1) * invHeight - 0.5) * extent}`); // Batch processing for better performance const batchSize = 5000; let processedPoints = 0; for (let x = 0; x < width; x++) { const worldXBase = (x * invWidth - 0.5) * extent; const batchPoints = []; for (let y = 0; y < height; y++) { const worldX = worldXBase; const worldY = (y * invHeight - 0.5) * extent;