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.

379 lines (319 loc) • 13.6 kB
/** * WebAssembly QuadTree Wrapper for Node.js * Provides seamless integration between WASM and JavaScript implementations */ import { SimpleQuadTree } from './simple-quadtree'; import { Point3D } from './index'; // WASM Module interface interface WASMModule { ccall: (fname: string, returnType: string, argTypes: string[], args: any[]) => any; getValue: (ptr: number, type: string) => number; default?: () => Promise<WASMModule>; } // Tree info interface export interface TreeInfo { type: 'wasm' | 'javascript'; instance: any; pointCount: number; } // Bounds interface export interface Bounds { minX: number; minY: number; maxX: number; maxY: number; } // Point interface for WASM export interface WASMPoint { x: number; y: number; z: number; } /** * QuadTree WASM Wrapper for Node.js environment */ export class QuadTreeWASMWrapper { private wasmModule: WASMModule | null = null; private wasmInstance: any = null; private isWasmReady: boolean = false; private isLoading: boolean = false; private fallbackQuadTree: SimpleQuadTree | null = null; private useFallback: boolean = false; private wasmTreeData: any = null; // Performance thresholds private readonly WASM_THRESHOLD: number = 1000; // Use WASM for datasets larger than this constructor() { this.loadWasm(); } /** * Load WebAssembly module */ private async loadWasm(): Promise<void> { if (this.isLoading || this.isWasmReady) { return; } this.isLoading = true; try { // Check if WebAssembly is supported if (typeof WebAssembly === 'undefined') { console.warn('WebAssembly not supported, using JavaScript fallback'); this.useFallback = true; this.isLoading = false; return; } // Load the WASM module (Node.js way) // eslint-disable-next-line @typescript-eslint/no-var-requires const QuadTreeModule = require('../quadtree.js'); this.wasmModule = await QuadTreeModule(); this.isWasmReady = true; console.log('WebAssembly QuadTree module loaded successfully'); } catch (error: any) { console.warn('Failed to load WebAssembly module, using JavaScript fallback:', error); this.useFallback = true; } finally { this.isLoading = false; } } /** * Wait for WASM to load with timeout */ private async waitForWasm(): Promise<void> { if (this.isWasmReady || this.useFallback) { return; } // Wait for WASM to load (with timeout) const timeout = 5000; // 5 seconds const startTime = Date.now(); while (!this.isWasmReady && !this.useFallback && (Date.now() - startTime) < timeout) { await new Promise(resolve => setTimeout(resolve, 50)); } if (!this.isWasmReady && !this.useFallback) { console.warn('WASM loading timeout, using JavaScript fallback'); this.useFallback = true; } } /** * Determine if WASM should be used based on point count */ private shouldUseWasm(pointCount: number): boolean { return this.isWasmReady && !this.useFallback && pointCount >= this.WASM_THRESHOLD; } /** * Build tree using either WASM or JavaScript fallback */ async buildTree(points: WASMPoint[], bounds: Bounds): Promise<TreeInfo> { await this.waitForWasm(); const useWasm = this.shouldUseWasm(points.length); if (useWasm) { console.log(`Using WebAssembly for ${points.length} points`); return this.buildTreeWasm(points, bounds); } else { console.log(`Using JavaScript fallback for ${points.length} points`); return this.buildTreeJS(points, bounds); } } /** * Build tree using WebAssembly */ private buildTreeWasm(points: WASMPoint[], bounds: Bounds): TreeInfo { try { console.log('Attempting to use WASM QuadTree implementation...'); // Check if WASM module has the expected functions if (!this.wasmModule || typeof this.wasmModule.ccall !== 'function') { console.warn('WASM module does not have ccall interface, falling back to JavaScript'); return this.buildTreeJS(points, bounds); } // Convert points to format expected by WASM (flatten arrays) const xCoords = new Float32Array(points.length); const yCoords = new Float32Array(points.length); const zCoords = new Float32Array(points.length); for (let i = 0; i < points.length; i++) { xCoords[i] = points[i].x; yCoords[i] = points[i].y; zCoords[i] = points[i].z || 0; } // Try to call WASM function for tree building const result = this.wasmModule.ccall('buildQuadTree', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number'], [points.length, xCoords, yCoords, zCoords, bounds.minX, bounds.minY, bounds.maxX, bounds.maxY] ); if (result !== 0) { console.log(`WASM QuadTree built successfully for ${points.length} points`); // Create a wrapper that mimics the expected interface this.wasmTreeData = { points: points, bounds: bounds, wasmHandle: result }; return { type: 'wasm', instance: this.wasmTreeData, pointCount: points.length }; } else { throw new Error('WASM tree building returned null/failed'); } } catch (error: any) { console.warn('WASM tree building failed, falling back to JavaScript:', error.message); return this.buildTreeJS(points, bounds); } } /** * Build tree using JavaScript fallback */ private buildTreeJS(points: WASMPoint[], bounds: Bounds): TreeInfo { // Use the existing SimpleQuadTree implementation const boundary = { x: bounds.minX, y: bounds.minY, width: bounds.maxX - bounds.minX, height: bounds.maxY - bounds.minY }; this.fallbackQuadTree = new SimpleQuadTree(boundary); for (let i = 0; i < points.length; i++) { this.fallbackQuadTree.insert({ x: points[i].x, y: points[i].y, z: points[i].z || 0, index: i }); } return { type: 'javascript', instance: this.fallbackQuadTree, pointCount: points.length }; } /** * Filter points by depth using grid-based subsampling */ filterByDepth(treeInfo: TreeInfo, targetDepth: number): WASMPoint[] { if (treeInfo.type === 'wasm' && treeInfo.instance) { try { console.log(`WASM filtering at depth ${targetDepth}...`); // Call WASM function for grid-based subsampling const gridSize = Math.pow(2, targetDepth); if (!this.wasmModule || typeof this.wasmModule.ccall !== 'function') { console.warn('WASM module not available for filtering'); return []; } // Allocate memory for result points const resultPtr = this.wasmModule.ccall('filterByDepth', 'number', ['number', 'number'], [treeInfo.instance.wasmHandle, targetDepth] ); if (resultPtr === 0) { console.warn('WASM filtering returned null'); return []; } // Read results from WASM memory const resultCount = this.wasmModule.getValue(resultPtr, 'i32'); const pointsPtr = this.wasmModule.getValue(resultPtr + 4, 'i32'); const filteredPoints: WASMPoint[] = []; for (let i = 0; i < resultCount; i++) { const pointPtr = pointsPtr + (i * 12); // 3 floats * 4 bytes const x = this.wasmModule.getValue(pointPtr, 'float'); const y = this.wasmModule.getValue(pointPtr + 4, 'float'); const z = this.wasmModule.getValue(pointPtr + 8, 'float'); filteredPoints.push({ x, y, z }); } // Free WASM memory this.wasmModule.ccall('freeFilterResult', 'void', ['number'], [resultPtr]); console.log(`WASM filtered to ${filteredPoints.length} points (${gridSize}x${gridSize} grid)`); return filteredPoints; } catch (error: any) { // WASM filtering failed, using fallback // Fallback to JavaScript implementation using the original points return this.fallbackGridSubsampling(treeInfo.instance.points, treeInfo.instance.bounds, targetDepth); } } else if (treeInfo.type === 'javascript' && treeInfo.instance) { // JavaScript QuadTree filtering (already implemented in main code) const allPoints = treeInfo.instance.getAllPoints(); const gridSize = Math.pow(2, targetDepth); // Simple grid-based subsampling return this.fallbackGridSubsampling(allPoints, treeInfo.instance.boundary, targetDepth); } return []; } /** * Fallback JavaScript grid-based subsampling */ private fallbackGridSubsampling(points: WASMPoint[], bounds: any, targetDepth: number): WASMPoint[] { const gridSize = Math.pow(2, targetDepth); const cellWidth = (bounds.maxX - bounds.minX) / gridSize; const cellHeight = (bounds.maxY - bounds.minY) / gridSize; const gridCells = new Map<string, WASMPoint>(); for (const point of points) { const cellX = Math.floor((point.x - bounds.minX) / cellWidth); const cellY = Math.floor((point.y - bounds.minY) / cellHeight); const cellKey = `${cellX},${cellY}`; // Keep only first point in each cell if (!gridCells.has(cellKey)) { gridCells.set(cellKey, point); } } return Array.from(gridCells.values()); } /** * Query range using appropriate implementation */ queryRange(treeInfo: TreeInfo, x: number, y: number, width: number, height: number): WASMPoint[] { if (treeInfo.type === 'wasm' && treeInfo.instance) { try { // WASM range query would be implemented here return this.convertWasmArrayToJS([]); } catch (error: any) { // WASM range query failed, using fallback return []; } } else if (treeInfo.type === 'javascript' && treeInfo.instance) { const range = { x, y, width, height }; return treeInfo.instance.query(range); } return []; } /** * Convert WASM array to JavaScript array */ private convertWasmArrayToJS(wasmArray: any[]): WASMPoint[] { const result: WASMPoint[] = []; const length = wasmArray.length || 0; for (let i = 0; i < length; i++) { const point = wasmArray[i]; result.push({ x: point.x, y: point.y, z: point.z }); } return result; } /** * Clear tree data */ clear(treeInfo: TreeInfo): void { if (treeInfo.type === 'wasm' && treeInfo.instance) { // WASM cleanup would be implemented here } else if (treeInfo.type === 'javascript' && treeInfo.instance) { treeInfo.instance.clear(); } } /** * Get performance information */ getPerformanceInfo(): { wasmReady: boolean; usingFallback: boolean; wasmThreshold: number; wasmSupported: boolean; } { return { wasmReady: this.isWasmReady, usingFallback: this.useFallback, wasmThreshold: this.WASM_THRESHOLD, wasmSupported: typeof WebAssembly !== 'undefined' }; } } // Export for CommonJS compatibility export default QuadTreeWASMWrapper;