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