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
text/typescript
/**
* 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;