Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/services/geometry.service.ts is written in an unsupported language. File is not indexed.

0001 import {Injectable, inject, signal, WritableSignal} from '@angular/core';
0002 import {ConfigProperty, ConfigService, resolveRegistry} from '@dexvis/app-features';
0003 import {
0004   FIREBIRD_WORKERS,
0005   GEOMETRY_CUT_LIST_CONFIG,
0006   GEOMETRY_FAST_MATERIAL_CONFIG,
0007   GEOMETRY_ROOT_FILTER_CONFIG,
0008   GEOMETRY_THEME_CONFIG,
0009   GEOMETRY_THEMES,
0010   resolveRootGeometryRules,
0011   ROOT_GEOMETRY_RULES,
0012   RootGeometryLoadRules,
0013   RootGeometryRules,
0014   selectRootGeometryLoadRules,
0015   UrlService,
0016 } from '@dexvis/firebird-ng/api';
0017 import {Subdetector, DetectorThreeRuleSet, ThreeGeometryProcessor, getColorOrDefault} from '@dexvis/threejs-tree-editor';
0018 import {Color, DoubleSide, MeshLambertMaterial, NormalBlending, Object3D, ObjectLoader, Plane} from "three";
0019 import * as THREE from "three";
0020 
0021 
0022 import type {
0023   WorkerResponse,
0024   GeometryLoadRequest,
0025   GeometryCancelRequest,
0026   SubdetectorInfo
0027 } from "@dexvis/firebird-ng/workers/geometry";
0028 
0029 
0030 
0031 /** Result returned by loadGeometry */
0032 interface GeometryLoadResult {
0033   threeGeometry: Object3D | null;
0034   cancelled: boolean;
0035 }
0036 
0037 /** Progress callback type */
0038 export type GeometryProgressCallback = (stage: string, progress: number) => void;
0039 
0040 /** The rejection of a load that a newer load replaced. */
0041 function replacedError(): DOMException {
0042   return new DOMException('A newer geometry load replaced this one', 'AbortError');
0043 }
0044 
0045 @Injectable({
0046   providedIn: 'root'
0047 })
0048 export class GeometryService {
0049 
0050   // Pipeline options from the config page (keys and defaults: config-keys.ts)
0051   geometryFastAndUgly: ConfigProperty<boolean>;
0052   geometryCutListName: ConfigProperty<string>;
0053   geometryThemeName: ConfigProperty<string>;
0054   geometryRootFilterName: ConfigProperty<string>;
0055 
0056   /** Collection of subdetectors */
0057   public subdetectors: Subdetector[] = [];
0058 
0059   /** TGeoManager - no longer available when using worker (kept for API compatibility) */
0060   public rootGeometry: any | null = null;
0061 
0062   /** Pre-build TGeo rules (withRootGeometryRules), merged once on the first load. */
0063   private readonly rootRuleSources = inject(ROOT_GEOMETRY_RULES, {optional: true}) ?? [];
0064   private rootRules: Promise<RootGeometryRules> | null = null;
0065 
0066   /** Geometry themes (withGeometryTheme), one per id: a later registration replaces an earlier one. */
0067   private readonly themes = resolveRegistry(inject(GEOMETRY_THEMES, {optional: true}) ?? [], theme => theme.id);
0068 
0069   /** for geometry post-processing */
0070   private threeGeometryProcessor = new ThreeGeometryProcessor();
0071 
0072   private defaultColor: Color = new Color(0x68698D);
0073 
0074   /** The geometry on screen: EventDisplayService sets it when it shows a load. */
0075   public geometry: WritableSignal<Object3D | null> = signal(null);
0076 
0077   /** Loading progress signal (0-100) */
0078   public loadingProgress: WritableSignal<number> = signal(0);
0079 
0080   /** Current loading stage description */
0081   public loadingStage: WritableSignal<string> = signal('');
0082 
0083   /** The worker factories of the application (withWorkers). */
0084   private readonly workers = inject(FIREBIRD_WORKERS, {optional: true});
0085   /** The geometry worker, created on the first load; dropped when it fails. */
0086   private worker: Worker | null = null;
0087 
0088   /** Current active request ID */
0089   private currentRequestId: string | null = null;
0090 
0091   /** Pending promise resolvers for geometry loading */
0092   private pendingResolvers: Map<string, {
0093     resolve: (result: GeometryLoadResult) => void;
0094     reject: (error: Error) => void;
0095     onProgress?: GeometryProgressCallback;
0096   }> = new Map();
0097 
0098   /** ObjectLoader for deserializing geometry from worker */
0099   private objectLoader = new ObjectLoader();
0100 
0101   constructor(
0102     private urlService: UrlService,
0103     private config: ConfigService,
0104   ) {
0105     // declare returns the canonical instance for the key (the config page or
0106     // the data selector may have declared it first) — keep the returned reference.
0107     this.geometryFastAndUgly = this.config.declare(GEOMETRY_FAST_MATERIAL_CONFIG);
0108     this.geometryCutListName = this.config.declare(GEOMETRY_CUT_LIST_CONFIG);
0109     this.geometryThemeName = this.config.declare(GEOMETRY_THEME_CONFIG);
0110     this.geometryRootFilterName = this.config.declare(GEOMETRY_ROOT_FILTER_CONFIG);
0111   }
0112 
0113   /**
0114    * The geometry worker, started on first use. A worker that fails (its
0115    * script did not load, or it crashed) rejects every pending load and is
0116    * dropped, so the next load starts a fresh one instead of waiting forever
0117    * on a dead worker.
0118    */
0119   private ensureWorker(): Worker {
0120     if (this.worker) return this.worker;
0121     if (typeof Worker === 'undefined') {
0122       throw new Error('Web Workers are not available, cannot load ROOT geometry in this browser');
0123     }
0124     if (!this.workers) {
0125       throw new Error('No geometry worker: add withWorkers({ geometry, rootFile }) to provideFirebird()');
0126     }
0127     const worker = this.workers.geometry();
0128     worker.onmessage = ({data}: MessageEvent<WorkerResponse>) => {
0129       this.handleWorkerMessage(data);
0130     };
0131     worker.onerror = (error) => {
0132       const message = `Geometry worker error: ${error.message ?? 'the worker script failed to load'}`;
0133       console.error(`[GeometryService]: ${message}`);
0134       for (const resolvers of this.pendingResolvers.values()) {
0135         resolvers.reject(new Error(message));
0136       }
0137       this.pendingResolvers.clear();
0138       this.currentRequestId = null;
0139       worker.terminate();
0140       if (this.worker === worker) this.worker = null;
0141     };
0142     this.worker = worker;
0143     return worker;
0144   }
0145 
0146   /**
0147    * Handle messages from the worker
0148    */
0149   private handleWorkerMessage(data: WorkerResponse): void {
0150     const resolvers = this.pendingResolvers.get(data.requestId);
0151 
0152     // Check if this response is for an old/stale request (not the current one)
0153     const isStaleRequest = data.requestId !== this.currentRequestId;
0154 
0155     if (data.type === 'progress') {
0156       // Only update progress for current request
0157       if (!isStaleRequest) {
0158         this.loadingProgress.set(data.progress);
0159         this.loadingStage.set(data.stage);
0160         if (resolvers?.onProgress) {
0161           resolvers.onProgress(data.stage, data.progress);
0162         }
0163       }
0164       return;
0165     }
0166 
0167     if (!resolvers) {
0168       // This can happen for stale requests that were already resolved
0169       if (isStaleRequest) {
0170         console.log(`[GeometryService]: Ignoring stale response for ${data.requestId} (current: ${this.currentRequestId})`);
0171       } else {
0172         console.warn(`[GeometryService]: No pending request for ${data.requestId}`);
0173       }
0174       return;
0175     }
0176 
0177     this.pendingResolvers.delete(data.requestId);
0178 
0179     // If this is a stale request, resolve as cancelled without processing
0180     if (isStaleRequest && data.type === 'success') {
0181       console.log(`[GeometryService]: Discarding stale geometry for ${data.requestId} (current: ${this.currentRequestId})`);
0182       resolvers.resolve({threeGeometry: null, cancelled: true});
0183       return;
0184     }
0185 
0186     if (data.requestId === this.currentRequestId) {
0187       this.currentRequestId = null;
0188     }
0189 
0190     if (data.type === 'success') {
0191       try {
0192         // Deserialize the geometry using ObjectLoader
0193         console.time('[GeometryService]: Parse geometry from JSON');
0194         const geometry = this.objectLoader.parse(data.geometryJson) as Object3D;
0195         console.timeEnd('[GeometryService]: Parse geometry from JSON');
0196 
0197         // jsroot creates objects with matrixAutoUpdate=false and sets matrices directly.
0198         // After deserialization, we need to ensure all matrices are properly applied.
0199         console.time('[GeometryService]: Update matrix world');
0200         this.restoreMatrixState(geometry);
0201         console.timeEnd('[GeometryService]: Update matrix world');
0202 
0203         // Build subdetectors from the worker's metadata
0204         this.buildSubdetectors(geometry, data.subdetectorInfos);
0205 
0206         this.loadingProgress.set(100);
0207         this.loadingStage.set('Complete');
0208 
0209         resolvers.resolve({threeGeometry: geometry, cancelled: false});
0210       } catch (error: any) {
0211         resolvers.reject(new Error(`Failed to parse geometry: ${error.message}`));
0212       }
0213     } else if (data.type === 'cancelled') {
0214       console.log(`[GeometryService]: Load cancelled for ${data.requestId}`);
0215       // Only reset progress if this is the current request
0216       if (!isStaleRequest) {
0217         this.loadingProgress.set(0);
0218         this.loadingStage.set('Cancelled');
0219       }
0220       resolvers.resolve({threeGeometry: null, cancelled: true});
0221     } else if (data.type === 'error') {
0222       // Only reset progress if this is the current request
0223       if (!isStaleRequest) {
0224         this.loadingProgress.set(0);
0225         this.loadingStage.set('Error');
0226       }
0227       resolvers.reject(new Error(data.error));
0228     }
0229   }
0230 
0231   /**
0232    * Build subdetector objects from worker metadata and deserialized geometry
0233    */
0234   private buildSubdetectors(geometry: Object3D, infos: SubdetectorInfo[]): void {
0235     this.subdetectors = [];
0236 
0237     if (!geometry.children.length || !geometry.children[0].children.length) {
0238       return;
0239     }
0240 
0241     const topDetectorNodes = geometry.children[0].children;
0242 
0243     for (let i = 0; i < topDetectorNodes.length && i < infos.length; i++) {
0244       const topNode = topDetectorNodes[i];
0245       const info = infos[i];
0246 
0247       const subdetector: Subdetector = {
0248         sourceGeometry: null,  // Not available when using worker
0249         sourceGeometryName: info.originalName,
0250         geometry: topNode,
0251         name: info.name,
0252         groupName: ''
0253       };
0254 
0255       this.subdetectors.push(subdetector);
0256     }
0257   }
0258 
0259   /**
0260    * Generate a unique request ID
0261    */
0262   private generateRequestId(): string {
0263     return `geo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
0264   }
0265 
0266   /**
0267    * Restore matrix state after deserialization.
0268    * jsroot creates objects with matrixAutoUpdate=false and sets matrices directly.
0269    * Three.js ObjectLoader restores the matrix, but we need to ensure it's properly applied.
0270    */
0271   private restoreMatrixState(object: Object3D): void {
0272     // Traverse all objects and ensure matrices are properly set up
0273     object.traverse((child) => {
0274       // jsroot geometry uses matrixAutoUpdate = false
0275       child.matrixAutoUpdate = false;
0276       // Decompose the matrix to position/rotation/scale for proper rendering
0277       child.matrix.decompose(child.position, child.quaternion, child.scale);
0278     });
0279 
0280     // Update the world matrices for the entire hierarchy
0281     object.updateMatrixWorld(true);
0282   }
0283 
0284   /**
0285    * Cancel the current geometry loading operation.
0286    * The loading promise will resolve with cancelled: true.
0287    */
0288   cancelLoading(): void {
0289     if (!this.currentRequestId || !this.worker) {
0290       return;
0291     }
0292 
0293     console.log(`[GeometryService]: Cancelling load request ${this.currentRequestId}`);
0294 
0295     const cancelRequest: GeometryCancelRequest = {
0296       type: 'cancel',
0297       requestId: this.currentRequestId
0298     };
0299 
0300     this.worker.postMessage(cancelRequest);
0301   }
0302 
0303   /**
0304    * Check if geometry is currently being loaded
0305    */
0306   isLoading(): boolean {
0307     return this.currentRequestId !== null;
0308   }
0309 
0310   /**
0311    * Loads ROOT TGeo geometry in the geometry worker, which keeps the UI
0312    * responsive while jsroot reads and builds it. Lengths stay in centimeters.
0313    * A newer load cancels this one.
0314    *
0315    * @param url The URL or path to load (aliases and server paths are
0316    *   resolved here), or a picked file, read in place.
0317    * @param options.signal Cancels the load: the worker stops and the
0318    *   promise rejects with the signal's reason.
0319    * @returns The geometry root.
0320    * @throws Error with the reason; an AbortError when the signal fired or a
0321    *   newer load replaced this one.
0322    */
0323   async loadGeometry(
0324     url: string | File,
0325     options: { signal?: AbortSignal; onProgress?: GeometryProgressCallback } = {},
0326   ): Promise<Object3D> {
0327     const {signal, onProgress} = options;
0328     signal?.throwIfAborted();
0329     const worker = this.ensureWorker();
0330     this.subdetectors = [];
0331     this.rootGeometry = null;
0332 
0333     // A picked/dropped file is read where it lies - there is no URL to resolve
0334     const finalUrl = typeof url === 'string' ? this.urlService.resolveDownloadUrl(url) : url;
0335     const label = typeof finalUrl === 'string' ? finalUrl : finalUrl.name;
0336 
0337     console.log(`[GeometryService]: Loading geometry from ${label}`);
0338     console.time('[GeometryService]: Total load geometry time');
0339 
0340     // Cancel any existing load operation and immediately resolve old promise
0341     if (this.currentRequestId) {
0342       const oldRequestId = this.currentRequestId;
0343       const oldResolvers = this.pendingResolvers.get(oldRequestId);
0344       if (oldResolvers) {
0345         console.log(`[GeometryService]: Immediately resolving old request ${oldRequestId} as cancelled`);
0346         this.pendingResolvers.delete(oldRequestId);
0347         oldResolvers.resolve({threeGeometry: null, cancelled: true});
0348       }
0349       this.cancelLoading();
0350     }
0351 
0352     const requestId = this.generateRequestId();
0353     this.currentRequestId = requestId;
0354 
0355     this.loadingProgress.set(0);
0356     this.loadingStage.set('Starting');
0357 
0358     // The rules this load applies, chosen by the config names. Merging may
0359     // wait for a rule chunk to load, and a newer load can start meanwhile.
0360     let rules: RootGeometryLoadRules;
0361     try {
0362       this.rootRules ??= resolveRootGeometryRules(this.rootRuleSources);
0363       rules = selectRootGeometryLoadRules(
0364         await this.rootRules, this.geometryRootFilterName.value, this.geometryCutListName.value);
0365     } catch (error) {
0366       // The next load tries the rule chunks again
0367       this.rootRules = null;
0368       if (this.currentRequestId === requestId) this.currentRequestId = null;
0369       console.timeEnd('[GeometryService]: Total load geometry time');
0370       throw new Error(`Geometry rules failed to load: ${error instanceof Error ? error.message : error}`);
0371     }
0372     if (this.currentRequestId !== requestId || signal?.aborted) {
0373       if (this.currentRequestId === requestId) this.currentRequestId = null;
0374       console.timeEnd('[GeometryService]: Total load geometry time');
0375       throw signal?.reason ?? replacedError();
0376     }
0377 
0378     const request: GeometryLoadRequest = {
0379       type: 'load',
0380       requestId,
0381       url: finalUrl,
0382       rules,
0383     };
0384 
0385     // Resolved by the worker message handler; the signal cancels the request
0386     const abort = () => {
0387       if (this.currentRequestId === requestId) this.cancelLoading();
0388     };
0389     signal?.addEventListener('abort', abort, {once: true});
0390     let result: GeometryLoadResult;
0391     try {
0392       result = await new Promise<GeometryLoadResult>((resolve, reject) => {
0393         this.pendingResolvers.set(requestId, {resolve, reject, onProgress});
0394         worker.postMessage(request);
0395       });
0396     } finally {
0397       signal?.removeEventListener('abort', abort);
0398       console.timeEnd('[GeometryService]: Total load geometry time');
0399     }
0400 
0401     if (result.cancelled || !result.threeGeometry) {
0402       throw signal?.reason ?? replacedError();
0403     }
0404     return result.threeGeometry;
0405   }
0406 
0407   /**
0408    * The main-thread material pass of a loaded geometry: default materials,
0409    * then the theme selected by `geometry.themeName`, then the clipping
0410    * planes on every material.
0411    */
0412   public async postProcessing(threeGeometry: Object3D, clippingPlanes: Plane[]): Promise<void> {
0413 
0414     // Now we want to set default materials
0415     threeGeometry.traverse((child: any) => {
0416       if (child.type !== 'Mesh' || !child?.material?.isMaterial) {
0417         return;
0418       }
0419 
0420       // Handle the material of the child
0421       const color = getColorOrDefault(child.material, this.defaultColor);
0422 
0423       if(this.geometryFastAndUgly.value) {
0424         child.material = new MeshLambertMaterial({
0425           color: color,
0426           side: DoubleSide,
0427           transparent: false,
0428           opacity: 1,
0429           blending: THREE.NoBlending,
0430           depthTest: true,
0431           depthWrite: true,
0432           clippingPlanes,
0433           clipIntersection: true,
0434           clipShadows: false,
0435           fog: false,
0436           vertexColors: false,
0437           flatShading: true,
0438           toneMapped: false
0439         });
0440       } else {
0441         child.material = new MeshLambertMaterial({
0442           color: color,
0443           side: DoubleSide,
0444           transparent: true,
0445           opacity: 0.7,
0446           blending: NormalBlending,
0447           depthTest: true,
0448           depthWrite: true,
0449           clippingPlanes: clippingPlanes,
0450           clipIntersection: true,
0451           clipShadows: false,
0452         });
0453       }
0454     });
0455 
0456     const ruleSets = await this.themeRuleSets(this.geometryThemeName.value);
0457     if (ruleSets) {
0458       await this.threeGeometryProcessor.processRuleSets([...ruleSets], this.subdetectors);
0459     }
0460 
0461     threeGeometry.traverse((child: any) => {
0462       if (!child?.material?.isMaterial) {
0463         return;
0464       }
0465 
0466       if (child.material.type === 'LineMaterial' ||
0467           child.material.isLineMaterial ||
0468           child.type === 'Line2' ||
0469           child.type === 'LineSegments2') {
0470         return;
0471       }
0472 
0473       if (child.material?.clippingPlanes !== undefined) {
0474         child.material.clippingPlanes = clippingPlanes;
0475       }
0476       if (child.material?.clipIntersection !== undefined) {
0477         child.material.clipIntersection = true;
0478       }
0479       if (child.material?.clipShadows !== undefined) {
0480         child.material.clipShadows = false;
0481       }
0482     });
0483   }
0484 
0485   /**
0486    * The rule sets of the theme `id`; undefined for 'off', and for an id no
0487    * feature registered (a saved choice from an installation with another
0488    * pack): the geometry then keeps its default materials.
0489    */
0490   private async themeRuleSets(id: string): Promise<readonly DetectorThreeRuleSet[] | undefined> {
0491     console.log(`[GeometryService]: Geometry theme name is set to '${id}'`);
0492     if (id === 'off') return undefined;
0493     const theme = this.themes.find(candidate => candidate.id === id);
0494     if (!theme) {
0495       console.warn(`[GeometryService]: No geometry theme '${id}' is registered; known: ${this.themes.map(known => known.id).join(', ') || 'none'}`);
0496       return undefined;
0497     }
0498     return theme.load();
0499   }
0500 
0501   toggleVisibility(object: Object3D) {
0502     if (object) {
0503       object.visible = !object.visible;
0504       console.log(`Visibility toggled for object: ${object.name}. Now visible: ${object.visible}`);
0505     }
0506   }
0507 }
0508