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 }