Back to home page

EIC code displayed by LXR

 
 

    


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

0001 import {computed, effect, Injectable, linkedSignal, Signal, signal, WritableSignal} from '@angular/core';
0002 import {Group as TweenGroup, Tween} from '@tweenjs/tween.js';
0003 import {ThreeService} from './three.service';
0004 import {GeometryService} from './geometry.service';
0005 import {DataModelService} from './data-model.service';
0006 import {ConfigService} from './config.service';
0007 import {UrlService} from './url.service';
0008 
0009 
0010 import {disposeHierarchy} from '../utils/three.utils';
0011 import {ThreeEventProcessor} from '../data-pipelines/three-event.processor';
0012 import {DataModelPainter, DisplayMode} from '../painters/data-model-painter';
0013 import {AnimationManager} from "../animation/animation-manager";
0014 import {initGroupFactories} from "../model/default-group-init";
0015 import {Mesh, MeshBasicMaterial, SphereGeometry, Vector3} from "three";
0016 import {arrangeEpicDetectors} from "../utils/epic-geometry-arranger";
0017 
0018 
0019 @Injectable({
0020   providedIn: 'root',
0021 })
0022 export class EventDisplayService {
0023 
0024   private eventsByName = new Map<string, any>();
0025   private eventsArray: any[] = [];
0026   private _animationSpeed: number = 1.0;
0027 
0028   selectedEventKey: string | undefined;
0029 
0030   // Time
0031   //private eventDisplayMode: WritableSignal<DisplayMode> = signal(DisplayMode.Timeless);
0032   public eventTime: WritableSignal<number | null> = signal(0);
0033 
0034   // Animation cycling
0035   public animationIsCycling: WritableSignal<boolean> = signal(false);
0036 
0037   // Whether camera moves (zoom/pan) during time animation
0038   public animateCameraMovement: boolean = false;
0039 
0040 
0041   public maxTime = 200;
0042   public minTime = 0;
0043 
0044 
0045   // Time animation
0046   private tweenGroup = new TweenGroup();
0047   private tween: Tween<any> | null = null;
0048   private beamAnimationTime: number = 1000;
0049 
0050   // Geometry
0051   private animateEventAfterLoad: boolean = false;
0052   private trackInfos: any | null = null; // Replace 'any' with the actual type
0053 
0054   // Painter that draws the event
0055   private painter: DataModelPainter = new DataModelPainter();
0056 
0057   // Animation manager
0058   private animationManager: AnimationManager;
0059 
0060   /** The last successfully loaded Firebird DEX JSON url. Switches to null on every new load attempt */
0061   public lastLoadedDexUrl: string | null = "";
0062 
0063   /** The last successfully loaded Geometry url. Switches to null on every new load attempt */
0064   public lastLoadedGeometryUrl: string | null = "";
0065 
0066   /** The last successfully loaded Edm4Eic converted url. Switches to null on every new load attempt */
0067   public lastLoadedRootUrl: string | null = "";
0068   public lastLoadedRootEventRange: string | null = "";
0069 
0070   constructor(
0071     public three: ThreeService,
0072     private geomService: GeometryService,
0073     private config: ConfigService,
0074     private dataService: DataModelService,
0075     private urlService: UrlService
0076   ) {
0077 
0078     // Add event model factories (things that decode json to objects)
0079     initGroupFactories();
0080 
0081     // Connect painter to its scene place
0082     this.painter.setThreeSceneParent(this.three.sceneEvent);
0083 
0084     // Connect animation manager with threejs components
0085     this.animationManager = new AnimationManager(this.three.scene, this.three.camera, this.three.renderer);
0086 
0087     // On time change
0088     effect(() => {
0089       const time = this.eventTime();
0090       this.painter.paint(time);
0091     }, {debugName: "EventDisplayService.OnTimeChange"});
0092 
0093     effect(() => {
0094       //this.processCurrentTimeChange(this.eventTime());
0095       const geometry = this.geomService.geometry();
0096     }, {debugName: "EventDisplayService.OnTimeChange"});
0097 
0098     // On current entry change
0099     effect(() => {
0100       console.log("[eventDisplay] Entry change effect start")
0101       let event = this.dataService.currentEntry();
0102 
0103       // Make sure to clean-up even if event is null
0104       // this.painter.cleanupCurrentEntry();
0105 
0106       if (event === null || this.painter.getEntry() == event) return;
0107       this.painter.setEntry(event);
0108       this.painter.paint(null);
0109 
0110       console.log("[eventDisplay] Entry change effect end")
0111     }, {debugName: "EventDisplayService.OnEventChange"});
0112   }
0113 
0114   // ****************************************************
0115   // *************** THREE SETUP ************************
0116   // ****************************************************
0117 
0118   /**
0119    * Initialize the default three.js scene
0120    * @param container
0121    */
0122   initThree(container: string | HTMLElement) {
0123     this.three.init(container);
0124     this.painter.setThreeSceneParent(this.three.sceneEvent);
0125     this.three.startRendering();
0126 
0127     // We need this to update the animation group
0128     this.three.addFrameCallback(() => {
0129       this.tweenGroup.update();
0130     })
0131   }
0132 
0133 
0134   // ****************************************************
0135   // *************** TIME *******************************
0136   // ****************************************************
0137 
0138   public updateEventTime(time: number) {
0139     this.eventTime.set(time);
0140   }
0141 
0142   getMaxTime(): number {
0143     return this.maxTime;
0144   }
0145 
0146   getMinTime(): number {
0147     return this.minTime;
0148   }
0149 
0150   get animationSpeed(): number {
0151     return this._animationSpeed;
0152   }
0153 
0154   set animationSpeed(value: number) {
0155     this._animationSpeed = Math.max(0.1, value);
0156   }
0157 
0158   private get timeStepSize(): number {
0159     // never allow a zero step
0160     return Math.max(this._animationSpeed, 0.1);
0161   }
0162 
0163 
0164   animateTime() {
0165     let time = this.eventTime() ?? this.minTime;
0166     const timeToTravel = this.maxTime - time;
0167 
0168     // Speed: the higher the animationSpeed, the faster (less duration)
0169     const baseMsPerUnit = 200;
0170     const speed = this.animationSpeed;
0171 
0172     const duration = timeToTravel * (baseMsPerUnit / speed);
0173 
0174     this.animateCurrentTime(this.maxTime, duration);
0175   }
0176 
0177 
0178   stopTimeAnimation(): void {
0179     if (this.tween) {
0180       this.tween.stop(); // Stops the tween if it is running
0181       this.tween = null; // Remove reference
0182     }
0183   }
0184 
0185   rewindTime() {
0186     this.updateEventTime(0);
0187   }
0188 
0189   animateCurrentTime(targetTime: number, duration: number): void {
0190     if (this.tween) {
0191       this.stopTimeAnimation();
0192     }
0193 
0194     this.tween = new Tween({currentTime: this.eventTime() ?? this.minTime}, this.tweenGroup)
0195       .to({currentTime: targetTime}, duration)
0196       .onUpdate((obj) => {
0197         this.eventTime.set(obj.currentTime);
0198         if (this.animateCameraMovement) {
0199           const dz = Math.max(obj.currentTime/10, 25);
0200           if(obj.currentTime <50) {
0201             const direction = new Vector3();
0202             direction.subVectors(this.three.controls.target, this.three.camera.position).normalize();
0203             const zoomAmount = -5;
0204             this.three.camera.position.addScaledVector(direction, zoomAmount);  // positive = zoom in
0205           }
0206           this.three.camera.position.setZ(this.three.camera.position.z + dz);
0207           this.three.controls.target.setZ(this.three.controls.target.z + dz);
0208           this.three.camera.updateMatrix();
0209         }
0210       }).onStop((time)=>{
0211         console.log(`[eventDisplay]: time animation stopped at: ${time}`);
0212       }).onComplete((time)=>{
0213         if(this.animationIsCycling()) {
0214           this.dataService.setNextEntry();
0215           setTimeout(() => { this.animateWithCollision();}, 1);
0216         }
0217       })
0218       // .easing(TWEEN.Easing.Quadratic.In) // This can be changed to other easing functions
0219       .start();
0220   }
0221 
0222   /**
0223    * Animate the collision of two particles.
0224    * @param tweenDuration Duration of the particle collision animation tween.
0225    * @param particleSize Size of the particles.
0226    * @param distanceFromOrigin Distance of the particles (along z-axes) from the origin.
0227    * @param onEnd Callback to call when the particle collision ends.
0228    */
0229   public animateParticlesCollide(
0230     tweenDuration: number,
0231     particleSize: number = 30,
0232     distanceFromOrigin: number = 5000,
0233     onEnd?: () => void,
0234   ) {
0235 
0236     // Make electron
0237     const electronGeometry = new SphereGeometry(particleSize, 32, 32);
0238     const electronMaterial = new MeshBasicMaterial({ color: 0x0000FF, transparent: true, opacity: 0});
0239     const electron = new Mesh(electronGeometry, electronMaterial);
0240 
0241     // Make ion
0242     const ionMaterial = new MeshBasicMaterial({ color: 0xFF0000, transparent: true, opacity: 0});
0243     const ionGeometry = new SphereGeometry(2*particleSize, 32, 32);
0244     const ion = new Mesh(ionGeometry, ionMaterial);
0245 
0246     electron.position.setZ(distanceFromOrigin);
0247     ion.position.setZ(-distanceFromOrigin);
0248 
0249     const particles = [electron, ion];
0250 
0251     this.three.sceneEvent.add(...particles);
0252 
0253     const particleTweens = [];
0254 
0255     for (const particle of particles) {
0256       new Tween(particle.material, this.tweenGroup)
0257         .to({opacity: 1,},300,)
0258         .start();
0259 
0260       const particleToOrigin = new Tween(particle.position, this.tweenGroup)
0261         .to({z: 0,}, tweenDuration,)
0262         .onUpdate((time)=>{// Move camera closer to the target (what you're doing, but toward target)
0263           if (this.animateCameraMovement) {
0264             const direction = new Vector3();
0265             direction.subVectors(this.three.controls.target, this.three.camera.position).normalize();
0266             const zoomAmount = 3;
0267             this.three.camera.position.addScaledVector(direction, zoomAmount);  // positive = zoom in
0268           }
0269         })
0270         .start();
0271 
0272       particleTweens.push(particleToOrigin);
0273     }
0274 
0275     particleTweens[0].onComplete(() => {
0276       this.three.sceneEvent.remove(...particles);
0277       onEnd?.();
0278     });
0279   }
0280 
0281   animateWithCollision() {
0282     this.stopTimeAnimation();
0283     this.rewindTime();
0284     if (this.trackInfos) {
0285       for (let trackInfo of this.trackInfos) {
0286         trackInfo.trackNode.visible = false;
0287       }
0288     }
0289 
0290     const ed_this = this;
0291     this.animateParticlesCollide(1000, undefined, undefined, ()=>{
0292       ed_this.animateTime();
0293     });
0294   }
0295 
0296   timeStepBack(): void {
0297     const t = this.eventTime() ?? this.minTime;
0298     this.updateEventTime(Math.max(t - this.timeStepSize, this.minTime));
0299   }
0300 
0301 
0302   timeStep(): void {
0303     const t = this.eventTime();
0304     if (t == null) return;
0305     this.updateEventTime(Math.min(t + this.timeStepSize, this.maxTime));
0306   }
0307 
0308   exitTimedDisplay() {
0309 
0310     this.stopTimeAnimation();
0311     this.eventTime.set(null);
0312     this.animateEventAfterLoad = false;
0313     if (this.trackInfos) {
0314       for (let trackInfo of this.trackInfos) {
0315         trackInfo.trackNode.visible = true;
0316         trackInfo.newLine.geometry.instanceCount = Infinity;
0317       }
0318     }
0319   }
0320 
0321   // Animation cycling methods
0322   startAnimationCycling() {
0323     this.animationIsCycling.set(true);
0324     // TODO: Implement animation cycling logic
0325   }
0326 
0327   stopAnimationCycling() {
0328     this.animationIsCycling.set(false);
0329     // TODO: Stop animation cycling logic
0330   }
0331 
0332   // Entry navigation
0333   setNextEntry() {
0334     this.dataService.setNextEntry();
0335   }
0336 
0337   // ****************************************************
0338   // *************** DATA LOADING ***********************
0339   // ****************************************************
0340 
0341   /**
0342    * Load geometry
0343    */
0344   async loadGeometry(url: string, scale = 10, clearGeometry = true) {
0345     this.lastLoadedGeometryUrl = null;
0346     let {rootGeometry, threeGeometry} = await this.geomService.loadGeometry(url);
0347     if (!threeGeometry) return;
0348 
0349 
0350 
0351     const sceneGeo = this.three.sceneGeometry;
0352 
0353     // There should be only one geometry if clearGeometry=true
0354     if (clearGeometry && sceneGeo.children.length > 0) {
0355       disposeHierarchy(sceneGeo, /* disposeSelf= */ false);
0356     }
0357 
0358     await this.geomService.postProcessing(threeGeometry, this.three.clipPlanes, {
0359       renderer: this.three.renderer,
0360       sceneGeometry: this.three.sceneGeometry,
0361       scene: this.three.scene,
0362     });
0363 
0364     sceneGeo.add(threeGeometry);
0365 
0366     // Set geometry scale (ROOT uses cm, we want mm, so scale by 10)
0367     if (scale) {
0368       sceneGeo.scale.setScalar(scale);
0369       // Since matrixAutoUpdate is false on worker-loaded geometry,
0370       // we must manually update the matrix after changing scale
0371       sceneGeo.updateMatrix();
0372       sceneGeo.updateMatrixWorld(true);
0373     }
0374 
0375     // Arrange by category
0376     arrangeEpicDetectors(sceneGeo);
0377 
0378     this.lastLoadedGeometryUrl = url;
0379   }
0380 
0381   async loadDexData(url: string) {
0382     this.lastLoadedDexUrl = null;
0383     const data = await this.dataService.loadDexData(url);
0384     if (data == null) {
0385       console.warn(
0386         'DataService.loadDexData() Received data is null or undefined'
0387       );
0388       return;
0389     }
0390 
0391     if (data.events?.length ?? 0 > 0) {
0392       this.painter.setEntry(data.events[0]);
0393       this.eventTime.set(null);
0394       this.painter.paint(this.eventTime());
0395       this.lastLoadedDexUrl = url;
0396 
0397     } else {
0398       console.warn('DataService.loadDexData() Received data had no entries');
0399       console.log(data);
0400     }
0401   }
0402 
0403   async loadRootData(url: string, eventRange: string = "0") {
0404     this.lastLoadedRootUrl = null;
0405     this.lastLoadedRootEventRange = null;
0406     const data = await this.dataService.loadRootData(url, eventRange);
0407     if (data == null) {
0408       console.warn(
0409         'DataService.loadRootData() Received data is null or undefined'
0410       );
0411       return;
0412     }
0413 
0414     if (data.events?.length ?? 0 > 0) {
0415       this.painter.setEntry(data.events[0]);
0416       this.eventTime.set(null);
0417       this.painter.paint(this.eventTime());
0418       this.lastLoadedRootUrl = url;
0419       this.lastLoadedRootEventRange = eventRange;
0420     } else {
0421       console.warn('DataService.loadRootData() Received data had no entries');
0422       console.log(data);
0423     }
0424   }
0425 
0426   // ****************************************************
0427   // *************** EVENTS *****************************
0428   // ****************************************************
0429 
0430   /**
0431    * Process current time change
0432    * @param value
0433    * @private
0434    */
0435   private processCurrentTimeChange(value: number | null) {
0436 
0437   }
0438 
0439   public buildEventDataFromJSON(eventData: any) {
0440     const threeEventProcessor = new ThreeEventProcessor();
0441 
0442     console.time('[buildEventDataFromJSON] BUILD EVENT');
0443 
0444     this.three.sceneEvent.clear();
0445 
0446     // Event data collections by type
0447     for (const collectionType in eventData) {
0448       const collectionsOfType = eventData[collectionType];
0449 
0450       for (const collectionName in collectionsOfType) {
0451         const collection = collectionsOfType[collectionName];
0452       }
0453     }
0454 
0455     // Post-processing for specific event data types
0456     const mcTracksGroup = this.three.sceneEvent.getObjectByName('mc_tracks');
0457     if (mcTracksGroup) {
0458       this.trackInfos = threeEventProcessor.processMcTracks(mcTracksGroup);
0459 
0460       let minTime = Infinity;
0461       let maxTime = 0;
0462       for (const trackInfo of this.trackInfos) {
0463         if (trackInfo.startTime < minTime) minTime = trackInfo.startTime;
0464         if (trackInfo.endTime > maxTime) maxTime = trackInfo.endTime;
0465       }
0466 
0467       this.maxTime = maxTime;
0468       this.minTime = minTime;
0469 
0470       console.log(`Tracks: ${this.trackInfos.length}`);
0471       if (this.trackInfos && this.animateEventAfterLoad) {
0472         for (const trackInfo of this.trackInfos) {
0473           trackInfo.trackNode.visible = false;
0474         }
0475       }
0476       console.timeEnd('Process tracks on event load');
0477     }
0478 
0479     console.timeEnd('[buildEventDataFromJSON] BUILD EVENT');
0480 
0481     if (this.animateEventAfterLoad) {
0482       this.animateWithCollision();
0483     }
0484   }
0485 
0486   /**
0487    * Offline frame-by-frame render. Steps the tween manually,
0488    * captures each frame as PNG, returns array of blobs.
0489    */
0490   async captureFramesOffline(options: {
0491     overrideResolution?: boolean;
0492     width: number;
0493     height: number;
0494     eventTimeStep: number;       // event-time units per frame, e.g. 0.1
0495     includeCollision?: boolean;
0496     onProgress?: (current: number, total: number) => void;
0497     signal?: AbortSignal;
0498   }): Promise<Blob[]> {
0499     const { width, height, eventTimeStep, onProgress } = options;
0500     const renderer = this.three.renderer;
0501 
0502     // ── Save original state ──
0503     const origWidth = renderer.domElement.width;
0504     const origHeight = renderer.domElement.height;
0505     const origPixelRatio = renderer.getPixelRatio();
0506     const origCameraPos = this.three.camera.position.clone();
0507     const origTarget = this.three.controls.target.clone();
0508     const origAspect = this.three.perspectiveCamera.aspect;
0509 
0510     // ── Force render resolution (opt-in) ──
0511     if (options.overrideResolution) {
0512       renderer.setSize(width, height, false);
0513       renderer.setPixelRatio(1);
0514       this.three.perspectiveCamera.aspect = width / height;
0515       this.three.perspectiveCamera.updateProjectionMatrix();
0516     }
0517 
0518     const frames: Blob[] = [];
0519 
0520     const captureFrame = (): Promise<Blob> => {
0521       renderer.render(this.three.scene, this.three.camera);
0522       return new Promise((resolve, reject) => {
0523         renderer.domElement.toBlob(
0524           blob => blob ? resolve(blob) : reject(new Error('toBlob failed')),
0525           'image/png'
0526         );
0527       });
0528     };
0529 
0530     // ── Yield to browser so UI updates (progress bar etc.) ──
0531     const yieldFrame = () => new Promise(resolve => setTimeout(resolve, 0));
0532 
0533     try {
0534       // ── Phase 1: Collision particles (optional) ──
0535       if (options.includeCollision) {
0536         const collisionDuration = 1000; // ms, matches animateParticlesCollide
0537         const collisionFps = 60;
0538         const collisionMsPerFrame = 1000 / collisionFps;
0539         const collisionFrames = Math.ceil(collisionDuration / collisionMsPerFrame);
0540 
0541         // Reset state
0542         this.rewindTime();
0543         this.three.camera.position.copy(origCameraPos);
0544         this.three.controls.target.copy(origTarget);
0545 
0546         // Build offline tween group for collision
0547         const collGroup = new TweenGroup();
0548 
0549         const particleSize = 30;
0550         const dist = 5000;
0551 
0552         const electronGeom = new SphereGeometry(particleSize, 32, 32);
0553         const electronMat = new MeshBasicMaterial({ color: 0x0000FF, transparent: true, opacity: 0 });
0554         const electron = new Mesh(electronGeom, electronMat);
0555         electron.position.setZ(dist);
0556 
0557         const ionGeom = new SphereGeometry(2 * particleSize, 32, 32);
0558         const ionMat = new MeshBasicMaterial({ color: 0xFF0000, transparent: true, opacity: 0 });
0559         const ion = new Mesh(ionGeom, ionMat);
0560         ion.position.setZ(-dist);
0561 
0562         this.three.sceneEvent.add(electron, ion);
0563 
0564         // Opacity fade-in
0565         new Tween(electronMat, collGroup).to({ opacity: 1 }, 300).start(0);
0566         new Tween(ionMat, collGroup).to({ opacity: 1 }, 300).start(0);
0567 
0568         // Move to origin
0569         new Tween(electron.position, collGroup)
0570           .to({ z: 0 }, collisionDuration)
0571           .onUpdate(() => {
0572             if (this.animateCameraMovement) {
0573               const dir = new Vector3().subVectors(this.three.controls.target, this.three.camera.position).normalize();
0574               this.three.camera.position.addScaledVector(dir, 3);
0575             }
0576           })
0577           .start(0);
0578         new Tween(ion.position, collGroup).to({ z: 0 }, collisionDuration).start(0);
0579 
0580         for (let i = 0; i <= collisionFrames; i++) {
0581           if (options.signal?.aborted) break;
0582           collGroup.update(i * collisionMsPerFrame);
0583           frames.push(await captureFrame());
0584           onProgress?.(frames.length, -1); // indeterminate total during collision
0585           await yieldFrame();
0586         }
0587 
0588         this.three.sceneEvent.remove(electron, ion);
0589         electronGeom.dispose(); electronMat.dispose();
0590         ionGeom.dispose(); ionMat.dispose();
0591       }
0592 
0593       // ── Phase 2: Time animation ──
0594       // Speed scales event-time per frame: speed=2 → 2x event-time per frame → half as many frames
0595       const totalEventTime = this.maxTime - this.minTime;
0596       const effectiveStep = eventTimeStep * this.animationSpeed;
0597       const totalFrames = Math.ceil(totalEventTime / effectiveStep);
0598 
0599       for (let i = 0; i <= totalFrames; i++) {
0600         if (options.signal?.aborted) break;
0601 
0602         const currentTime = Math.min(this.minTime + i * effectiveStep, this.maxTime);
0603         this.eventTime.set(currentTime);
0604 
0605         // Camera movement (matches animateCurrentTime tween onUpdate)
0606         if (this.animateCameraMovement) {
0607           const dz = Math.max(currentTime / 10, 25);
0608           if (currentTime < 50) {
0609             const direction = new Vector3()
0610               .subVectors(this.three.controls.target, this.three.camera.position)
0611               .normalize();
0612             this.three.camera.position.addScaledVector(direction, -5);
0613           }
0614           this.three.camera.position.setZ(this.three.camera.position.z + dz);
0615           this.three.controls.target.setZ(this.three.controls.target.z + dz);
0616           this.three.camera.updateMatrix();
0617         }
0618 
0619         frames.push(await captureFrame());
0620         onProgress?.(frames.length, totalFrames);
0621         await yieldFrame();
0622       }
0623 
0624     } finally {
0625       // ── Restore everything ──
0626       if (options.overrideResolution) {
0627         renderer.setSize(origWidth, origHeight, false);
0628         renderer.setPixelRatio(origPixelRatio);
0629         this.three.perspectiveCamera.aspect = origAspect;
0630         this.three.perspectiveCamera.updateProjectionMatrix();
0631       }
0632       this.three.camera.position.copy(origCameraPos);
0633       this.three.controls.target.copy(origTarget);
0634       this.three.camera.updateMatrix();
0635     }
0636 
0637     return frames;
0638   }
0639 }