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.
277 lines (242 loc) • 8.06 kB
text/typescript
/**
* QuadTree Worker for Node.js using worker_threads
* Handles heavy QuadTree operations in a separate thread
*/
import { parentPort, workerData } from 'worker_threads';
import { QuadTreeFilter, PointXYZ, Point3D } from './index';
// Worker message types
interface WorkerMessage {
type: 'BUILD_TREE' | 'FILTER_BY_DEPTH' | 'FILTER_BY_GRID_SPACING' | 'ABORT';
data?: any;
}
interface WorkerResponse {
type: 'TREE_BUILT' | 'POINTS_FILTERED' | 'ERROR' | 'PROGRESS';
data?: any;
error?: string;
points?: Point3D[];
processingTime?: number;
depth?: number;
gridSize?: number;
expectedMax?: number;
bounds?: number[];
pointCount?: number;
message?: string;
progress?: number;
}
// Worker state
let quadTreeFilter: QuadTreeFilter | null = null;
let isBuilt: boolean = false;
let isAborted: boolean = false;
/**
* Handle incoming messages from main thread
*/
function handleMessage(message: WorkerMessage): void {
if (!parentPort) return;
try {
switch (message.type) {
case 'BUILD_TREE':
handleBuildTree(message.data);
break;
case 'FILTER_BY_DEPTH':
handleFilterByDepth(message.data);
break;
case 'FILTER_BY_GRID_SPACING':
handleFilterByGridSpacing(message.data);
break;
case 'ABORT':
handleAbort();
break;
default:
throw new Error(`Unknown message type: ${message.type}`);
}
} catch (error: any) {
const response: WorkerResponse = {
type: 'ERROR',
error: error.message || 'Unknown worker error'
};
parentPort.postMessage(response);
}
}
/**
* Handle tree building
*/
function handleBuildTree(data: {
points: Array<{x: number, y: number, z: number}>;
bounds: number[];
minCellSize: number;
}): void {
if (!parentPort) return;
try {
console.log(`Worker: Building QuadTree for ${data.points.length} points`);
const startTime = performance.now();
// Reset abort flag
isAborted = false;
// Create QuadTree filter
quadTreeFilter = new QuadTreeFilter(data.minCellSize);
// Convert points to PointXYZ objects
const points: PointXYZ[] = data.points.map(p => new PointXYZ(p.x, p.y, p.z));
// Set points directly
quadTreeFilter.dataLoader.points = points;
// Build the tree
const coords = quadTreeFilter.dataLoader.getCoordinateArrays();
quadTreeFilter.quadtreeManager.buildTreeFromArrays(
coords.x, coords.y, coords.z, data.bounds
);
const buildTime = performance.now() - startTime;
isBuilt = true;
if (!isAborted) {
const response: WorkerResponse = {
type: 'TREE_BUILT',
data: {
pointCount: data.points.length,
bounds: data.bounds,
buildTime: buildTime
},
processingTime: buildTime,
pointCount: data.points.length,
bounds: data.bounds
};
parentPort.postMessage(response);
}
} catch (error: any) {
const response: WorkerResponse = {
type: 'ERROR',
error: `Failed to build tree: ${error.message}`
};
parentPort.postMessage(response);
}
}
/**
* Handle filter by depth
*/
async function handleFilterByDepth(data: {
targetDepth: number;
}): Promise<void> {
if (!parentPort) return;
try {
if (!quadTreeFilter || !isBuilt) {
throw new Error('QuadTree not built');
}
console.log(`Worker: Filtering by depth ${data.targetDepth}`);
const startTime = performance.now();
// Reset abort flag
isAborted = false;
// Perform filtering
const filteredPoints = await quadTreeFilter.filterByDepth(data.targetDepth);
const filterTime = performance.now() - startTime;
if (!isAborted) {
const gridSize = Math.pow(2, data.targetDepth);
const response: WorkerResponse = {
type: 'POINTS_FILTERED',
points: filteredPoints,
processingTime: filterTime,
depth: data.targetDepth,
gridSize: gridSize,
expectedMax: gridSize * gridSize,
message: `Filtered to ${filteredPoints.length} points using ${gridSize}x${gridSize} grid`
};
parentPort.postMessage(response);
}
} catch (error: any) {
const response: WorkerResponse = {
type: 'ERROR',
error: `Failed to filter by depth: ${error.message}`
};
parentPort.postMessage(response);
}
}
/**
* Handle filter by grid spacing
*/
async function handleFilterByGridSpacing(data: {
xCoords: number[];
yCoords: number[];
zCoords: number[];
minLateralSpacing: number;
}): Promise<void> {
if (!parentPort) return;
try {
if (!quadTreeFilter) {
throw new Error('QuadTree filter not initialized');
}
console.log(`Worker: Filtering by grid spacing ${data.minLateralSpacing}`);
const startTime = performance.now();
// Reset abort flag
isAborted = false;
// Perform filtering
const filteredPoints = quadTreeFilter.filterPointsByGridSpacing(
data.xCoords,
data.yCoords,
data.zCoords,
data.minLateralSpacing
);
const filterTime = performance.now() - startTime;
if (!isAborted) {
const response: WorkerResponse = {
type: 'POINTS_FILTERED',
points: filteredPoints,
processingTime: filterTime,
message: `Filtered to ${filteredPoints.length} points with ${data.minLateralSpacing}m spacing`
};
parentPort.postMessage(response);
}
} catch (error: any) {
const response: WorkerResponse = {
type: 'ERROR',
error: `Failed to filter by grid spacing: ${error.message}`
};
parentPort.postMessage(response);
}
}
/**
* Handle abort signal
*/
function handleAbort(): void {
console.log('Worker: Received abort signal');
isAborted = true;
quadTreeFilter = null;
isBuilt = false;
}
/**
* Send progress updates
*/
function sendProgress(progress: number, message: string): void {
if (!parentPort || isAborted) return;
const response: WorkerResponse = {
type: 'PROGRESS',
progress: progress,
message: message
};
parentPort.postMessage(response);
}
// Set up message handler
if (parentPort) {
parentPort.on('message', handleMessage);
// Send ready signal
parentPort.postMessage({
type: 'READY',
message: 'QuadTree worker ready'
});
} else {
console.error('Worker: parentPort not available');
}
// Handle uncaught exceptions
process.on('uncaughtException', (error: Error) => {
if (parentPort) {
parentPort.postMessage({
type: 'ERROR',
error: `Uncaught exception: ${error.message}`
});
}
process.exit(1);
});
// Handle unhandled rejections
process.on('unhandledRejection', (reason: any) => {
if (parentPort) {
parentPort.postMessage({
type: 'ERROR',
error: `Unhandled rejection: ${reason}`
});
}
process.exit(1);
});