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