Back to home page

EIC code displayed by LXR

 
 

    


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

0001 import {Injectable, signal, WritableSignal} from '@angular/core';
0002 import {ConfigService} from "./config.service";
0003 import {Subdetector} from "../model/subdetector";
0004 import {Color, DoubleSide, MeshLambertMaterial, NormalBlending, Object3D, ObjectLoader, Plane} from "three";
0005 import {UrlService} from "./url.service";
0006 import {DetectorThreeRuleSet, ThreeGeometryProcessor} from "../data-pipelines/three-geometry.processor";
0007 import * as THREE from "three";
0008 import {getColorOrDefault} from "../utils/three.utils";
0009 
0010 import {cool2ColorRules} from "../theme/cool2-geometry-ruleset";
0011 import {cadColorRules} from "../theme/cad-geometry-ruleset";
0012 import {monoColorRules} from "../theme/mono-geometry-ruleset";
0013 import {cool2NoOutlineColorRules} from "../theme/cool2no-geometry-ruleset";
0014 
0015 import {ConfigProperty} from "../utils/config-property";
0016 import {prettify, PrettifyOptions} from "../utils/eic-geometry-prettifier";
0017 
0018 import type {
0019   WorkerRequest,
0020   WorkerResponse,
0021   GeometryLoadRequest,
0022   GeometryCancelRequest,
0023   SubdetectorInfo
0024 } from "../workers/geometry-loader.worker";
0025 
0026 export const GROUP_CALORIMETRY = "Calorimeters";
0027 export const GROUP_TRACKING = "Tracking";
0028 export const GROUP_PID = "PID";
0029 export const GROUP_MAGNETS = "Magnets";
0030 export const GROUP_SUPPORT = "Beam pipe and support";
0031 export const ALL_GROUPS = [
0032   GROUP_CALORIMETRY,
0033   GROUP_TRACKING,
0034   GROUP_PID,
0035   GROUP_MAGNETS,
0036   GROUP_SUPPORT,
0037 ]
0038 
0039 
0040 
0041 // constants.ts
0042 export const DEFAULT_GEOMETRY = 'builtin://epic-central-optimized';
0043 
0044 /** Result returned by loadGeometry */
0045 export interface GeometryLoadResult {
0046   threeGeometry: Object3D | null;
0047   cancelled: boolean;
0048 }
0049 
0050 /** Progress callback type */
0051 export type GeometryProgressCallback = (stage: string, progress: number) => void;
0052 
0053 @Injectable({
0054   providedIn: 'root'
0055 })
0056 export class GeometryService {
0057 
0058   geometryFastAndUgly = new ConfigProperty('geometry.FastDefaultMaterial', false);
0059   geometryCutListName = new ConfigProperty('geometry.cutListName', "off");
0060   geometryThemeName = new ConfigProperty('geometry.themeName', "cool2");
0061   geometryRootFilterName = new ConfigProperty('geometry.rootFilterName', "default");
0062 
0063   /** Collection of subdetectors */
0064   public subdetectors: Subdetector[] = [];
0065 
0066   /** TGeoManager - no longer available when using worker (kept for API compatibility) */
0067   public rootGeometry: any | null = null;
0068 
0069   public groupsByDetName: Map<string, string>;
0070 
0071   /** for geometry post-processing */
0072   private threeGeometryProcessor = new ThreeGeometryProcessor();
0073 
0074   private defaultColor: Color = new Color(0x68698D);
0075 
0076   public geometry: WritableSignal<Object3D | null> = signal(null);
0077 
0078   /** Loading progress signal (0-100) */
0079   public loadingProgress: WritableSignal<number> = signal(0);
0080 
0081   /** Current loading stage description */
0082   public loadingStage: WritableSignal<string> = signal('');
0083 
0084   /** Worker instance for geometry loading */
0085   private worker: Worker | null = null;
0086 
0087   /** Current active request ID */
0088   private currentRequestId: string | null = null;
0089 
0090   /** Pending promise resolvers for geometry loading */
0091   private pendingResolvers: Map<string, {
0092     resolve: (result: GeometryLoadResult) => void;
0093     reject: (error: Error) => void;
0094     onProgress?: GeometryProgressCallback;
0095   }> = new Map();
0096 
0097   /** ObjectLoader for deserializing geometry from worker */
0098   private objectLoader = new ObjectLoader();
0099 
0100   constructor(
0101     private urlService: UrlService,
0102     private config: ConfigService,
0103   ) {
0104     this.groupsByDetName = new Map<string, string>([
0105       ["SolenoidBarrel_assembly_0", GROUP_MAGNETS],
0106       ["SolenoidEndcapP_1", GROUP_MAGNETS],
0107       ["SolenoidEndcapN_2", GROUP_MAGNETS],
0108       ["VertexBarrelSubAssembly_3", GROUP_TRACKING],
0109       ["InnerSiTrackerSubAssembly_4", GROUP_TRACKING],
0110       ["MiddleSiTrackerSubAssembly_5", GROUP_TRACKING],
0111       ["OuterSiTrackerSubAssembly_6", GROUP_TRACKING],
0112       ["EndcapMPGDSubAssembly_7", GROUP_TRACKING],
0113       ["InnerMPGDBarrelSubAssembly_8", GROUP_TRACKING],
0114       ["EndcapTOFSubAssembly_9", GROUP_PID],
0115       ["BarrelTOFSubAssembly_10", GROUP_PID],
0116       ["OuterBarrelMPGDSubAssembly_11", GROUP_TRACKING],
0117       ["B0TrackerSubAssembly_12", GROUP_TRACKING],
0118       ["InnerTrackerSupport_assembly_13", GROUP_SUPPORT],
0119       ["DIRC_14", GROUP_PID],
0120       ["RICHEndcapN_Vol_15", GROUP_PID],
0121       ["DRICH_16", GROUP_PID],
0122       ["EcalEndcapP_17", GROUP_CALORIMETRY],
0123       ["EcalEndcapPInsert_18", GROUP_CALORIMETRY],
0124       ["EcalBarrelImaging_19", GROUP_CALORIMETRY],
0125       ["EcalBarrelScFi_20", GROUP_CALORIMETRY],
0126       ["EcalEndcapN_21", GROUP_CALORIMETRY],
0127       ["LFHCAL_env_22", GROUP_CALORIMETRY],
0128       ["HcalEndcapPInsert_23", GROUP_CALORIMETRY],
0129       ["HcalBarrel_24", GROUP_CALORIMETRY],
0130       ["FluxBarrel_env_25", GROUP_SUPPORT],
0131       ["FluxEndcapP_26", GROUP_SUPPORT],
0132       ["HcalEndcapN_27", GROUP_CALORIMETRY],
0133       ["FluxEndcapN_28", GROUP_SUPPORT],
0134       ["BeamPipe_assembly_29", GROUP_SUPPORT],
0135       ["B0PF_BeamlineMagnet_assembly_30", GROUP_MAGNETS],
0136       ["B0APF_BeamlineMagnet_assembly_31", GROUP_MAGNETS],
0137       ["Q1APF_BeamlineMagnet_assembly_32", GROUP_MAGNETS],
0138       ["Q1BPF_BeamlineMagnet_assembly_33", GROUP_MAGNETS],
0139       ["BeamPipeB0_assembly_38", GROUP_SUPPORT],
0140       ["Pipe_cen_to_pos_assembly_39", GROUP_SUPPORT],
0141       ["Q0EF_assembly_40", GROUP_MAGNETS],
0142       ["Q0EF_vac_41", GROUP_MAGNETS],
0143       ["Q1EF_assembly_42", GROUP_MAGNETS],
0144       ["Q1EF_vac_43", GROUP_MAGNETS],
0145       ["B0ECal_44", GROUP_CALORIMETRY],
0146       ["Pipe_Q1eR_to_B2BeR_assembly_54", GROUP_SUPPORT],
0147       ["Magnet_Q1eR_assembly_55", GROUP_MAGNETS],
0148       ["Magnet_Q2eR_assembly_56", GROUP_MAGNETS],
0149       ["Magnet_B2AeR_assembly_57", GROUP_MAGNETS],
0150       ["Magnet_B2BeR_assembly_58", GROUP_MAGNETS],
0151       ["Magnets_Q3eR_assembly_59", GROUP_MAGNETS],
0152     ]);
0153 
0154     this.config.addConfig(this.geometryFastAndUgly);
0155     this.config.addConfig(this.geometryCutListName);
0156     this.config.addConfig(this.geometryThemeName);
0157     this.config.addConfig(this.geometryRootFilterName);
0158 
0159     this.initWorker();
0160   }
0161 
0162   /**
0163    * Initialize the geometry loader worker
0164    */
0165   private initWorker(): void {
0166     if (typeof Worker === 'undefined') {
0167       console.warn('[GeometryService]: Web Workers not supported in this environment');
0168       return;
0169     }
0170 
0171     this.worker = new Worker(new URL('../workers/geometry-loader.worker', import.meta.url), {type: 'module'});
0172 
0173     this.worker.onmessage = ({data}: MessageEvent<WorkerResponse>) => {
0174       this.handleWorkerMessage(data);
0175     };
0176 
0177     this.worker.onerror = (error) => {
0178       console.error('[GeometryService]: Worker error:', error);
0179       // Reject all pending promises
0180       for (const [requestId, resolvers] of this.pendingResolvers) {
0181         resolvers.reject(new Error(`Worker error: ${error.message}`));
0182       }
0183       this.pendingResolvers.clear();
0184       this.currentRequestId = null;
0185     };
0186   }
0187 
0188   /**
0189    * Handle messages from the worker
0190    */
0191   private handleWorkerMessage(data: WorkerResponse): void {
0192     const resolvers = this.pendingResolvers.get(data.requestId);
0193 
0194     // Check if this response is for an old/stale request (not the current one)
0195     const isStaleRequest = data.requestId !== this.currentRequestId;
0196 
0197     if (data.type === 'progress') {
0198       // Only update progress for current request
0199       if (!isStaleRequest) {
0200         this.loadingProgress.set(data.progress);
0201         this.loadingStage.set(data.stage);
0202         if (resolvers?.onProgress) {
0203           resolvers.onProgress(data.stage, data.progress);
0204         }
0205       }
0206       return;
0207     }
0208 
0209     if (!resolvers) {
0210       // This can happen for stale requests that were already resolved
0211       if (isStaleRequest) {
0212         console.log(`[GeometryService]: Ignoring stale response for ${data.requestId} (current: ${this.currentRequestId})`);
0213       } else {
0214         console.warn(`[GeometryService]: No pending request for ${data.requestId}`);
0215       }
0216       return;
0217     }
0218 
0219     this.pendingResolvers.delete(data.requestId);
0220 
0221     // If this is a stale request, resolve as cancelled without processing
0222     if (isStaleRequest && data.type === 'success') {
0223       console.log(`[GeometryService]: Discarding stale geometry for ${data.requestId} (current: ${this.currentRequestId})`);
0224       resolvers.resolve({threeGeometry: null, cancelled: true});
0225       return;
0226     }
0227 
0228     if (data.requestId === this.currentRequestId) {
0229       this.currentRequestId = null;
0230     }
0231 
0232     if (data.type === 'success') {
0233       try {
0234         // Deserialize the geometry using ObjectLoader
0235         console.time('[GeometryService]: Parse geometry from JSON');
0236         const geometry = this.objectLoader.parse(data.geometryJson) as Object3D;
0237         console.timeEnd('[GeometryService]: Parse geometry from JSON');
0238 
0239         // jsroot creates objects with matrixAutoUpdate=false and sets matrices directly.
0240         // After deserialization, we need to ensure all matrices are properly applied.
0241         console.time('[GeometryService]: Update matrix world');
0242         this.restoreMatrixState(geometry);
0243         console.timeEnd('[GeometryService]: Update matrix world');
0244 
0245         // Build subdetectors from the worker's metadata
0246         this.buildSubdetectors(geometry, data.subdetectorInfos);
0247 
0248         this.geometry.set(geometry);
0249         this.loadingProgress.set(100);
0250         this.loadingStage.set('Complete');
0251 
0252         resolvers.resolve({threeGeometry: geometry, cancelled: false});
0253       } catch (error: any) {
0254         resolvers.reject(new Error(`Failed to parse geometry: ${error.message}`));
0255       }
0256     } else if (data.type === 'cancelled') {
0257       console.log(`[GeometryService]: Load cancelled for ${data.requestId}`);
0258       // Only reset progress if this is the current request
0259       if (!isStaleRequest) {
0260         this.loadingProgress.set(0);
0261         this.loadingStage.set('Cancelled');
0262       }
0263       resolvers.resolve({threeGeometry: null, cancelled: true});
0264     } else if (data.type === 'error') {
0265       // Only reset progress if this is the current request
0266       if (!isStaleRequest) {
0267         this.loadingProgress.set(0);
0268         this.loadingStage.set('Error');
0269       }
0270       resolvers.reject(new Error(data.error));
0271     }
0272   }
0273 
0274   /**
0275    * Build subdetector objects from worker metadata and deserialized geometry
0276    */
0277   private buildSubdetectors(geometry: Object3D, infos: SubdetectorInfo[]): void {
0278     this.subdetectors = [];
0279 
0280     if (!geometry.children.length || !geometry.children[0].children.length) {
0281       return;
0282     }
0283 
0284     const topDetectorNodes = geometry.children[0].children;
0285 
0286     for (let i = 0; i < topDetectorNodes.length && i < infos.length; i++) {
0287       const topNode = topDetectorNodes[i];
0288       const info = infos[i];
0289 
0290       const subdetector: Subdetector = {
0291         sourceGeometry: null,  // Not available when using worker
0292         sourceGeometryName: info.originalName,
0293         geometry: topNode,
0294         name: info.name,
0295         groupName: info.groupName
0296       };
0297 
0298       this.subdetectors.push(subdetector);
0299     }
0300   }
0301 
0302   /**
0303    * Generate a unique request ID
0304    */
0305   private generateRequestId(): string {
0306     return `geo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
0307   }
0308 
0309   /**
0310    * Restore matrix state after deserialization.
0311    * jsroot creates objects with matrixAutoUpdate=false and sets matrices directly.
0312    * Three.js ObjectLoader restores the matrix, but we need to ensure it's properly applied.
0313    */
0314   private restoreMatrixState(object: Object3D): void {
0315     // Traverse all objects and ensure matrices are properly set up
0316     object.traverse((child) => {
0317       // jsroot geometry uses matrixAutoUpdate = false
0318       child.matrixAutoUpdate = false;
0319       // Decompose the matrix to position/rotation/scale for proper rendering
0320       child.matrix.decompose(child.position, child.quaternion, child.scale);
0321     });
0322 
0323     // Update the world matrices for the entire hierarchy
0324     object.updateMatrixWorld(true);
0325   }
0326 
0327   /**
0328    * Cancel the current geometry loading operation.
0329    * The loading promise will resolve with cancelled: true.
0330    */
0331   cancelLoading(): void {
0332     if (!this.currentRequestId || !this.worker) {
0333       return;
0334     }
0335 
0336     console.log(`[GeometryService]: Cancelling load request ${this.currentRequestId}`);
0337 
0338     const cancelRequest: GeometryCancelRequest = {
0339       type: 'cancel',
0340       requestId: this.currentRequestId
0341     };
0342 
0343     this.worker.postMessage(cancelRequest);
0344   }
0345 
0346   /**
0347    * Check if geometry is currently being loaded
0348    */
0349   isLoading(): boolean {
0350     return this.currentRequestId !== null;
0351   }
0352 
0353   /**
0354    * Load geometry from a URL using a background worker.
0355    * This method keeps the UI responsive during loading.
0356    *
0357    * @param url The URL to load geometry from
0358    * @param onProgress Optional callback for progress updates
0359    * @returns Promise resolving to the loaded geometry or null if cancelled
0360    */
0361   async loadGeometry(
0362     url: string,
0363     onProgress?: GeometryProgressCallback
0364   ): Promise<{rootGeometry: any | null, threeGeometry: Object3D | null}> {
0365 
0366     this.subdetectors = [];
0367     this.rootGeometry = null;
0368 
0369     // Handle the default geometry alias
0370     if (url === DEFAULT_GEOMETRY) {
0371       url = 'https://eic.github.io/epic/artifacts/tgeo/epic_full.root';
0372     }
0373 
0374     const finalUrl = this.urlService.resolveDownloadUrl(url);
0375 
0376     console.log(`[GeometryService]: Loading geometry from ${finalUrl}`);
0377     console.time('[GeometryService]: Total load geometry time');
0378 
0379     // Cancel any existing load operation and immediately resolve old promise
0380     if (this.currentRequestId) {
0381       const oldRequestId = this.currentRequestId;
0382       const oldResolvers = this.pendingResolvers.get(oldRequestId);
0383       if (oldResolvers) {
0384         console.log(`[GeometryService]: Immediately resolving old request ${oldRequestId} as cancelled`);
0385         this.pendingResolvers.delete(oldRequestId);
0386         oldResolvers.resolve({threeGeometry: null, cancelled: true});
0387       }
0388       this.cancelLoading();
0389     }
0390 
0391     // Check if worker is available
0392     if (!this.worker) {
0393       throw new Error('Geometry loader worker is not available');
0394     }
0395 
0396     const requestId = this.generateRequestId();
0397     this.currentRequestId = requestId;
0398 
0399     this.loadingProgress.set(0);
0400     this.loadingStage.set('Starting');
0401 
0402     // Create the load request
0403     const request: GeometryLoadRequest = {
0404       type: 'load',
0405       requestId,
0406       url: finalUrl,
0407       options: {
0408         cutListName: this.geometryCutListName.value,
0409         rootFilterName: this.geometryRootFilterName.value
0410       }
0411     };
0412 
0413     // Create a promise that will be resolved by the worker message handler
0414     const result = await new Promise<GeometryLoadResult>((resolve, reject) => {
0415       this.pendingResolvers.set(requestId, {resolve, reject, onProgress});
0416       this.worker!.postMessage(request);
0417     });
0418 
0419     console.timeEnd('[GeometryService]: Total load geometry time');
0420 
0421     if (result.cancelled) {
0422       return {rootGeometry: null, threeGeometry: null};
0423     }
0424 
0425     return {rootGeometry: null, threeGeometry: result.threeGeometry};
0426   }
0427 
0428   public async postProcessing(geometry: Object3D, clippingPlanes: Plane[], prettifyOptions?: Omit<PrettifyOptions, 'clippingPlanes'>): Promise<void> {
0429     let threeGeometry = this.geometry();
0430     if (!threeGeometry) return;
0431 
0432     // Now we want to set default materials
0433     threeGeometry.traverse((child: any) => {
0434       if (child.type !== 'Mesh' || !child?.material?.isMaterial) {
0435         return;
0436       }
0437 
0438       // Handle the material of the child
0439       const color = getColorOrDefault(child.material, this.defaultColor);
0440 
0441       if(this.geometryFastAndUgly.value) {
0442         child.material = new MeshLambertMaterial({
0443           color: color,
0444           side: DoubleSide,
0445           transparent: false,
0446           opacity: 1,
0447           blending: THREE.NoBlending,
0448           depthTest: true,
0449           depthWrite: true,
0450           clippingPlanes,
0451           clipIntersection: true,
0452           clipShadows: false,
0453           fog: false,
0454           vertexColors: false,
0455           flatShading: true,
0456           toneMapped: false
0457         });
0458       } else {
0459         child.material = new MeshLambertMaterial({
0460           color: color,
0461           side: DoubleSide,
0462           transparent: true,
0463           opacity: 0.7,
0464           blending: NormalBlending,
0465           depthTest: true,
0466           depthWrite: true,
0467           clippingPlanes: clippingPlanes,
0468           clipIntersection: true,
0469           clipShadows: false,
0470         });
0471       }
0472     });
0473 
0474     // HERE WE DO POSTPROCESSING STEP
0475     let geoTheme = this.geometryThemeName.value;
0476     console.log(`[GeometryService]: Geometry theme name is set to '${geoTheme}'`);
0477 
0478     if(geoTheme === "cool2") {
0479       this.threeGeometryProcessor.processRuleSets(cool2ColorRules, this.subdetectors);
0480     } else if(geoTheme === "cool2no") {
0481       this.threeGeometryProcessor.processRuleSets(cool2NoOutlineColorRules, this.subdetectors);
0482     } else if(geoTheme === "cad") {
0483       this.threeGeometryProcessor.processRuleSets(cadColorRules, this.subdetectors);
0484     } else if(geoTheme === "grey") {
0485       this.threeGeometryProcessor.processRuleSets(monoColorRules, this.subdetectors);
0486     }
0487 
0488     // Apply prettification (reflective materials, environment maps) if not in fast mode
0489     if (!this.geometryFastAndUgly.value && prettifyOptions) {
0490       await prettify(threeGeometry, {
0491         ...prettifyOptions,
0492         clippingPlanes: clippingPlanes,
0493       });
0494     }
0495 
0496     threeGeometry.traverse((child: any) => {
0497       if (!child?.material?.isMaterial) {
0498         return;
0499       }
0500 
0501       if (child.material.type === 'LineMaterial' ||
0502           child.material.isLineMaterial ||
0503           child.type === 'Line2' ||
0504           child.type === 'LineSegments2') {
0505         return;
0506       }
0507 
0508       if (child.material?.clippingPlanes !== undefined) {
0509         child.material.clippingPlanes = clippingPlanes;
0510       }
0511       if (child.material?.clipIntersection !== undefined) {
0512         child.material.clipIntersection = true;
0513       }
0514       if (child.material?.clipShadows !== undefined) {
0515         child.material.clipShadows = false;
0516       }
0517     });
0518   }
0519 
0520   private stripIdFromName(name: string) {
0521       return name.replace(/_\d+$/, '');
0522   }
0523 
0524   toggleVisibility(object: Object3D) {
0525     if (object) {
0526       object.visible = !object.visible;
0527       console.log(`Visibility toggled for object: ${object.name}. Now visible: ${object.visible}`);
0528     }
0529   }
0530 }
0531