Warning, /firebird/packages/firebird-ng/workers/geometry/src/lib/geometry-worker.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * The ROOT geometry worker: `runGeometryWorker(self)` in a worker entry
0003 * module of the application (see `withWorkers()`).
0004 *
0005 * This worker performs heavy geometry loading operations off the main thread:
0006 * - Fetches ROOT files via jsroot
0007 * - Parses TGeoManager
0008 * - Prunes and processes geometry
0009 * - Builds Three.js geometry
0010 * - Serializes the result for transfer to main thread
0011 *
0012 * Supports cancellation via requestId tracking.
0013 */
0014
0015 // jsroot declares its subpath modules (jsroot/io, jsroot/geom) in the types
0016 // of its main entry; the reference loads them without importing the main
0017 // entry, whose module graph is several times the size of the io entry.
0018 /// <reference types="jsroot" />
0019 import {openFile} from 'jsroot/io';
0020 import {build} from 'jsroot/geom';
0021 import {
0022 analyzeGeoNodes,
0023 findGeoManager,
0024 } from '@dexvis/root-geo-tree-editor';
0025 import {applyDetectorEditRules, pruneTopLevelDetectors} from './root-geometry.processor';
0026 import type {RootGeometryLoadRules} from '@dexvis/firebird-ng/api';
0027 import type {WorkerScope} from '@dexvis/firebird-ng/api';
0028
0029 // Message types for communication with main thread
0030 export interface GeometryLoadRequest {
0031 type: 'load';
0032 requestId: string;
0033 /**
0034 * Where to read the geometry from: a URL, or a file the user picked or
0035 * dropped. A File is structured-cloneable, so it crosses to the worker
0036 * without being read - jsroot slices it the same way it byte-ranges a URL.
0037 */
0038 url: string | File;
0039 /**
0040 * What to prune and edit before the build, as plain data: this worker is
0041 * a prebuilt module and cannot import the packs that define the rules.
0042 */
0043 rules: RootGeometryLoadRules;
0044 }
0045
0046 export interface GeometryCancelRequest {
0047 type: 'cancel';
0048 requestId: string;
0049 }
0050
0051 export type WorkerRequest = GeometryLoadRequest | GeometryCancelRequest;
0052
0053 export interface GeometryLoadSuccess {
0054 type: 'success';
0055 requestId: string;
0056 geometryJson: any; // Serialized Object3D via toJSON()
0057 subdetectorInfos: SubdetectorInfo[]; // Metadata about subdetectors
0058 }
0059
0060 export interface SubdetectorInfo {
0061 name: string;
0062 originalName: string;
0063 }
0064
0065 export interface GeometryLoadError {
0066 type: 'error';
0067 requestId: string;
0068 error: string;
0069 }
0070
0071 export interface GeometryLoadCancelled {
0072 type: 'cancelled';
0073 requestId: string;
0074 }
0075
0076 export interface GeometryLoadProgress {
0077 type: 'progress';
0078 requestId: string;
0079 stage: string;
0080 progress: number; // 0-100
0081 }
0082
0083 export type WorkerResponse = GeometryLoadSuccess | GeometryLoadError | GeometryLoadCancelled | GeometryLoadProgress;
0084
0085 // Track active requests for cancellation
0086 let activeRequestId: string | null = null;
0087 let cancellationRequested = false;
0088 let isProcessing = false; // Prevents concurrent processing
0089
0090 function stripIdFromName(name: string): string {
0091 return name.replace(/_\d+$/, '');
0092 }
0093
0094 function sendProgress(post: (message: WorkerResponse) => void, requestId: string, stage: string, progress: number) {
0095 const response: GeometryLoadProgress = {
0096 type: 'progress',
0097 requestId,
0098 stage,
0099 progress
0100 };
0101 post(response);
0102 }
0103
0104 async function loadGeometry(request: GeometryLoadRequest, post: (message: WorkerResponse) => void): Promise<void> {
0105 const {requestId, url, rules} = request;
0106
0107 // If already processing, mark for cancellation and wait for it to finish
0108 if (isProcessing) {
0109 console.log(`[GeometryWorker]: Already processing ${activeRequestId}, marking for cancellation`);
0110 cancellationRequested = true;
0111
0112 // Wait for current processing to finish before starting new one
0113 while (isProcessing) {
0114 await new Promise(resolve => setTimeout(resolve, 50));
0115 }
0116 }
0117
0118 isProcessing = true;
0119 activeRequestId = requestId;
0120 cancellationRequested = false;
0121
0122 try {
0123 // Check for cancellation at key points
0124 const checkCancellation = () => {
0125 if (cancellationRequested) {
0126 throw new Error('CANCELLED');
0127 }
0128 };
0129
0130 sendProgress(post, requestId, 'Opening ROOT file', 10);
0131
0132 console.time('[GeometryWorker]: Open root file');
0133 const file = await openFile(url);
0134 console.timeEnd('[GeometryWorker]: Open root file');
0135
0136 checkCancellation();
0137 sendProgress(post, requestId, 'Reading geometry', 20);
0138
0139 console.time('[GeometryWorker]: Reading geometry from file');
0140 const rootGeometry = await findGeoManager(file);
0141 console.timeEnd('[GeometryWorker]: Reading geometry from file');
0142
0143 if (!rootGeometry) {
0144 throw new Error('No TGeoManager found in ROOT file');
0145 }
0146
0147 checkCancellation();
0148 sendProgress(post, requestId, 'Pruning geometry', 30);
0149
0150 // Remove the top-level detectors of the selected cut list
0151 if (rules.cutList.length > 0) {
0152 const result = pruneTopLevelDetectors(rootGeometry, rules.cutList);
0153 console.log(`[GeometryWorker]: Pruned geometry. Nodes left: ${result.nodes.length}, Removed: ${result.removedNodes.length}`);
0154 }
0155
0156 checkCancellation();
0157 sendProgress(post, requestId, 'Pre-processing geometry', 40);
0158
0159 // Apply the selected detector edit rules
0160 if (rules.editRules.length > 0) {
0161 console.time('[GeometryWorker]: Root geometry pre-processing');
0162 applyDetectorEditRules(rootGeometry, rules.editRules);
0163 console.timeEnd('[GeometryWorker]: Root geometry pre-processing');
0164 }
0165
0166 checkCancellation();
0167 sendProgress(post, requestId, 'Analyzing geometry', 50);
0168
0169 console.log("[GeometryWorker]: Number of tree elements analysis (after root geometry prune):");
0170 analyzeGeoNodes(rootGeometry, 1);
0171
0172 checkCancellation();
0173 sendProgress(post, requestId, 'Building 3D geometry', 60);
0174
0175 // Build Three.js geometry - this is the most expensive operation
0176 console.time('[GeometryWorker]: Build geometry');
0177 const geometry = build(rootGeometry, {
0178 numfaces: 5000000000,
0179 numnodes: 5000000000,
0180 instancing: -1,
0181 dflt_colors: false,
0182 vislevel: 200,
0183 doubleside: true,
0184 transparency: true
0185 });
0186 console.timeEnd('[GeometryWorker]: Build geometry');
0187
0188 checkCancellation();
0189
0190 // Validate the geometry
0191 if (!geometry) {
0192 throw new Error("Geometry is null or undefined after build");
0193 }
0194
0195 if (!geometry.children.length) {
0196 throw new Error("Geometry is converted but empty. Expected 'world_volume' but got nothing");
0197 }
0198
0199 if (!geometry.children[0].children.length) {
0200 throw new Error("Geometry is converted but empty. Expected array of top level nodes but got nothing");
0201 }
0202
0203 sendProgress(post, requestId, 'Extracting subdetector info', 80);
0204
0205 // Extract subdetector information
0206 const topDetectorNodes = geometry.children[0].children;
0207
0208 const subdetectorInfos: SubdetectorInfo[] = topDetectorNodes.map((topNode: any) => ({
0209 name: stripIdFromName(topNode.name),
0210 originalName: topNode.name,
0211 }));
0212
0213 checkCancellation();
0214 sendProgress(post, requestId, 'Serializing geometry', 90);
0215
0216 // Serialize the geometry to JSON for transfer
0217 console.time('[GeometryWorker]: Serialize geometry to JSON');
0218 const geometryJson = geometry.toJSON();
0219 console.timeEnd('[GeometryWorker]: Serialize geometry to JSON');
0220
0221 sendProgress(post, requestId, 'Complete', 100);
0222
0223 const response: GeometryLoadSuccess = {
0224 type: 'success',
0225 requestId,
0226 geometryJson,
0227 subdetectorInfos
0228 };
0229
0230 post(response);
0231
0232 } catch (error: any) {
0233 if (error.message === 'CANCELLED') {
0234 const response: GeometryLoadCancelled = {
0235 type: 'cancelled',
0236 requestId
0237 };
0238 post(response);
0239 } else {
0240 console.error('[GeometryWorker]: Error loading geometry:', error);
0241 const response: GeometryLoadError = {
0242 type: 'error',
0243 requestId,
0244 error: error.message || String(error)
0245 };
0246 post(response);
0247 }
0248 } finally {
0249 activeRequestId = null;
0250 cancellationRequested = false;
0251 isProcessing = false;
0252 }
0253 }
0254
0255 /**
0256 * Runs the geometry worker in `scope`, the global scope of a worker entry
0257 * module (`self`): answers the load and cancel requests of GeometryService.
0258 *
0259 * ```ts
0260 * // geometry.worker.ts, the module withWorkers({ geometry }) starts
0261 * import { runGeometryWorker } from '@dexvis/firebird-ng/workers/geometry';
0262 * runGeometryWorker(self);
0263 * ```
0264 */
0265 export function runGeometryWorker(scope: WorkerScope): void {
0266 const post = (message: WorkerResponse) => scope.postMessage(message);
0267 scope.addEventListener('message', ({data}: MessageEvent<WorkerRequest>) => {
0268 if (data.type === 'load') {
0269 void loadGeometry(data, post);
0270 } else if (data.type === 'cancel') {
0271 if (activeRequestId === data.requestId) {
0272 console.log(`[GeometryWorker]: Cancellation requested for ${data.requestId}`);
0273 cancellationRequested = true;
0274 }
0275 }
0276 });
0277 }