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.

275 lines (243 loc) • 8.38 kB
/** * Simple QuadTree implementation for Node.js fallback * Used when WebAssembly is not available or for smaller datasets */ export interface QuadTreeBoundary { x: number; y: number; width: number; height: number; } export interface QuadTreePoint { x: number; y: number; z?: number; index?: number; [key: string]: any; } /** * Simple QuadTree implementation */ export class SimpleQuadTree { public boundary: QuadTreeBoundary; public capacity: number; public maxDepth: number; public depth: number; public points: QuadTreePoint[]; public divided: boolean; public northeast: SimpleQuadTree | null; public northwest: SimpleQuadTree | null; public southeast: SimpleQuadTree | null; public southwest: SimpleQuadTree | null; constructor(boundary: QuadTreeBoundary, capacity: number = 50, maxDepth: number = 12, depth: number = 0) { this.boundary = boundary; this.capacity = capacity; this.maxDepth = maxDepth; this.depth = depth; this.points = []; this.divided = false; this.northeast = null; this.northwest = null; this.southeast = null; this.southwest = null; } /** * Insert a point into the quadtree */ insert(point: QuadTreePoint): boolean { if (!this.contains(point)) { return false; } // Force insertion at max depth to prevent infinite recursion if (this.depth >= this.maxDepth) { this.points.push(point); return true; } if (this.points.length < this.capacity) { this.points.push(point); return true; } if (!this.divided) { this.subdivide(); } return (this.northeast!.insert(point) || this.northwest!.insert(point) || this.southeast!.insert(point) || this.southwest!.insert(point)); } /** * Check if a point is within the boundary */ private contains(point: QuadTreePoint): boolean { return (point.x >= this.boundary.x && point.x <= this.boundary.x + this.boundary.width && point.y >= this.boundary.y && point.y <= this.boundary.y + this.boundary.height); } /** * Subdivide the quadtree into four quadrants */ private subdivide(): void { // Prevent subdivision if area becomes too small (prevents infinite recursion) const minSize = 0.001; if (this.boundary.width <= minSize || this.boundary.height <= minSize) { return; } const x = this.boundary.x; const y = this.boundary.y; const w = this.boundary.width / 2; const h = this.boundary.height / 2; const neRect: QuadTreeBoundary = { x: x + w, y: y, width: w, height: h }; const nwRect: QuadTreeBoundary = { x: x, y: y, width: w, height: h }; const seRect: QuadTreeBoundary = { x: x + w, y: y + h, width: w, height: h }; const swRect: QuadTreeBoundary = { x: x, y: y + h, width: w, height: h }; this.northeast = new SimpleQuadTree(neRect, this.capacity, this.maxDepth, this.depth + 1); this.northwest = new SimpleQuadTree(nwRect, this.capacity, this.maxDepth, this.depth + 1); this.southeast = new SimpleQuadTree(seRect, this.capacity, this.maxDepth, this.depth + 1); this.southwest = new SimpleQuadTree(swRect, this.capacity, this.maxDepth, this.depth + 1); this.divided = true; // Redistribute existing points const pointsToRedistribute = [...this.points]; this.points = []; for (const point of pointsToRedistribute) { // Try to insert in child nodes, keep in parent if failed if (!(this.northeast.insert(point) || this.northwest.insert(point) || this.southeast.insert(point) || this.southwest.insert(point))) { this.points.push(point); } } } /** * Query points within a rectangular range */ query(range: QuadTreeBoundary, found: QuadTreePoint[] = []): QuadTreePoint[] { if (!this.intersects(range)) { return found; } for (const point of this.points) { if (this.inRange(point, range)) { found.push(point); } } if (this.divided) { this.northeast!.query(range, found); this.northwest!.query(range, found); this.southeast!.query(range, found); this.southwest!.query(range, found); } return found; } /** * Check if the range intersects with the boundary */ private intersects(range: QuadTreeBoundary): boolean { return !(range.x >= this.boundary.x + this.boundary.width || range.x + range.width <= this.boundary.x || range.y >= this.boundary.y + this.boundary.height || range.y + range.height <= this.boundary.y); } /** * Check if a point is within the range */ private inRange(point: QuadTreePoint, range: QuadTreeBoundary): boolean { return (point.x >= range.x && point.x <= range.x + range.width && point.y >= range.y && point.y <= range.y + range.height); } /** * Get all points in the quadtree */ getAllPoints(found: QuadTreePoint[] = []): QuadTreePoint[] { found.push(...this.points); if (this.divided) { this.northeast!.getAllPoints(found); this.northwest!.getAllPoints(found); this.southeast!.getAllPoints(found); this.southwest!.getAllPoints(found); } return found; } /** * Clear all points from the quadtree */ clear(): void { this.points = []; this.divided = false; this.northeast = null; this.northwest = null; this.southeast = null; this.southwest = null; } /** * Get the total number of points */ size(): number { let count = this.points.length; if (this.divided) { count += this.northeast!.size(); count += this.northwest!.size(); count += this.southeast!.size(); count += this.southwest!.size(); } return count; } /** * Get statistics about the quadtree */ getStatistics(): { totalPoints: number; depth: number; nodes: number; leafNodes: number; } { const stats = { totalPoints: this.size(), depth: this.getMaxDepth(), nodes: this.getNodeCount(), leafNodes: this.getLeafNodeCount() }; return stats; } /** * Get the maximum depth of the quadtree */ private getMaxDepth(): number { if (!this.divided) { return this.depth; } return Math.max( this.northeast!.getMaxDepth(), this.northwest!.getMaxDepth(), this.southeast!.getMaxDepth(), this.southwest!.getMaxDepth() ); } /** * Get the total number of nodes */ private getNodeCount(): number { let count = 1; // This node if (this.divided) { count += this.northeast!.getNodeCount(); count += this.northwest!.getNodeCount(); count += this.southeast!.getNodeCount(); count += this.southwest!.getNodeCount(); } return count; } /** * Get the number of leaf nodes */ private getLeafNodeCount(): number { if (!this.divided) { return 1; } return (this.northeast!.getLeafNodeCount() + this.northwest!.getLeafNodeCount() + this.southeast!.getLeafNodeCount() + this.southwest!.getLeafNodeCount()); } }