diff --git a/packages/phoenix-event-display/src/loaders/event-data-loader.ts b/packages/phoenix-event-display/src/loaders/event-data-loader.ts index aa7f1b8d1..ad83de375 100644 --- a/packages/phoenix-event-display/src/loaders/event-data-loader.ts +++ b/packages/phoenix-event-display/src/loaders/event-data-loader.ts @@ -7,10 +7,60 @@ import type { } from '../lib/types/event-data'; /** - * Event data loader for implementing different event data loaders. + * This file defines the interfaces used by event data loaders. + * + * Event data loaders are responsible for transforming raw event + * data into graphical objects and UI elements that can be rendered + * in the Phoenix event display. + */ + +/** + * Metadata associated with an event. + * This information is typically shown in the UI to provide + * context about the event such as run number, event number, + * or time of recording. + */ +export interface EventMetadata { + /** Label describing the metadata field (e.g. Run, Event). */ + label: string; + + /** Value associated with the metadata label. */ + value: string | number; + + /** Optional unit of the value (e.g. ns, GeV). */ + unit?: string; + + /** Optional time information in nanoseconds. */ + time?: number; +} + +/** + * Represents event-level timing information. + * The time value is expressed in nanoseconds and can be used + * by animation systems to synchronize event playback. + */ +export interface EventTime { + /** Event time in nanoseconds. */ + time?: number; +} + +/** + * Interface describing the required functionality + * of an event data loader. + * + * Implementations of this interface convert raw event data + * into graphical objects that can be rendered by the Phoenix + * event display. */ export interface EventDataLoader { - /** Load one event into the graphics library and UI. */ + /** + * Load a single event into the graphics library and UI. + * + * @param eventData Raw event data object. + * @param graphicsLibrary Manager responsible for rendering 3D objects. + * @param ui Manager responsible for user interface elements. + * @param infoLogger Logger used for displaying event information. + */ buildEventData( eventData: PhoenixEventData, graphicsLibrary: ThreeManager, @@ -18,25 +68,62 @@ export interface EventDataLoader { infoLogger: InfoLogger, ): void; - /** Get keys of all events in the container. */ + /** + * Retrieve the list of available events from the provided container. + * + * @param eventsData Object containing multiple events. + * @returns Array of event names. + */ getEventsList(eventsData: PhoenixEventsData): string[]; - /** Get collection names grouped by object type. */ + /** + * Get collections grouped by object type. + * + * @returns Map where the key is the object type + * and the value is a list of collection names. + */ getCollections(): { [key: string]: string[] }; - /** Get all objects in a collection by name. */ + /** + * Retrieve objects belonging to a specific collection. + * + * @param collectionName Name of the collection. + * @returns Collection data. + */ getCollection(collectionName: string): any; - /** Get metadata for the current event. */ - getEventMetadata(): any[]; + /** + * Retrieve metadata associated with the currently loaded event. + * + * @returns List of event metadata objects. + */ + getEventMetadata(): EventMetadata[]; + + /** + * Retrieve event-level time information if available. + * + * @returns Event time metadata or undefined if not present. + */ + getEventTime?(): EventTime; - /** Add a label to an event object. Returns a unique label ID. */ + /** + * Attach a label to an event object. + * + * @param label Label text. + * @param collection Collection name. + * @param indexInCollection Index of the object in the collection. + * @returns Unique label identifier. + */ addLabelToEventObject( label: string, collection: string, indexInCollection: number, ): string; - /** Get the labels object. */ + /** + * Retrieve all labels associated with event objects. + * + * @returns Object containing label data. + */ getLabelsObject(): { [key: string]: any }; } diff --git a/packages/phoenix-event-display/src/loaders/phoenix-loader.ts b/packages/phoenix-event-display/src/loaders/phoenix-loader.ts index 6b5d6c8b2..73e06d81a 100644 --- a/packages/phoenix-event-display/src/loaders/phoenix-loader.ts +++ b/packages/phoenix-event-display/src/loaders/phoenix-loader.ts @@ -39,6 +39,8 @@ export class PhoenixLoader implements EventDataLoader { protected stateManager: StateManager; /** Object containing event object labels. */ protected labelsObject: { [key: string]: any } = {}; + /** Optional event-level time information in nanoseconds. */ + private eventTime?: { time: number; unit: 'ns' }; /** * Create the Phoenix loader. @@ -62,6 +64,13 @@ export class PhoenixLoader implements EventDataLoader { ui: UIManager, infoLogger: InfoLogger, ): void { + // Extract optional event-level time information + if (typeof (eventData as any).time === 'number') { + this.eventTime = { time: (eventData as any).time, unit: 'ns' }; + } else { + this.eventTime = undefined; + } + this.graphicsLibrary = graphicsLibrary; this.ui = ui; this.eventData = eventData; @@ -85,6 +94,23 @@ export class PhoenixLoader implements EventDataLoader { runNumber, eventNumber, }; + + // Forward event-level time to animation system if available + const animationsManager = + typeof (this.graphicsLibrary as any).getAnimationsManager === 'function' + ? (this.graphicsLibrary as any).getAnimationsManager() + : undefined; + if (animationsManager && this.eventTime?.time !== undefined) { + animationsManager.setEventTime(this.eventTime.time); + } + } + + /** + * Get event-level timing information if available. + * @returns Event time in nanoseconds, or undefined if not present. + */ + public getEventTime(): { time: number; unit: 'ns' } | undefined { + return this.eventTime; } /** diff --git a/packages/phoenix-event-display/src/managers/three-manager/animations-manager.ts b/packages/phoenix-event-display/src/managers/three-manager/animations-manager.ts index 8a85f0cda..5a7fca2a5 100644 --- a/packages/phoenix-event-display/src/managers/three-manager/animations-manager.ts +++ b/packages/phoenix-event-display/src/managers/three-manager/animations-manager.ts @@ -35,6 +35,11 @@ export interface AnimationPreset { * Manager for managing animation related operations using three.js and tween.js. */ export class AnimationsManager { + /** Optional event-level time in nanoseconds. */ + private eventTimeNs?: number; + /** Current animation time in nanoseconds. */ + private currentTimeNs = 0; + /** * Constructor for the animation manager. * @param scene Three.js scene containing all the objects and event data. @@ -51,6 +56,41 @@ export class AnimationsManager { this.animateEventWithClipping = this.animateEventWithClipping.bind(this); } + /** + * Set event-level time (in nanoseconds) for time-driven animations. + * @param timeNs Event time in nanoseconds. + */ + public setEventTime(timeNs?: number): void { + if (typeof timeNs === 'number' && timeNs > 0) { + this.eventTimeNs = timeNs; + this.currentTimeNs = 0; + } else { + this.eventTimeNs = undefined; + this.currentTimeNs = 0; + } + } + + /** + * Get normalized animation progress based on event time. + * @returns Value in range [0, 1]. + */ + public getTimeProgress(): number { + if (!this.eventTimeNs || this.eventTimeNs <= 0) { + return 0; + } + return Math.min(this.currentTimeNs / this.eventTimeNs, 1); + } + + /** + * Update the animation state. + * @param deltaSeconds Time delta since last update in seconds. + */ + public update(deltaSeconds: number): void { + if (this.eventTimeNs) { + this.currentTimeNs += deltaSeconds * 1e9; // seconds → nanoseconds + } + } + /** * Get the camera tween for animating camera to a position. * @param pos End position of the camera tween. @@ -61,7 +101,7 @@ export class AnimationsManager { public getCameraTween( pos: number[], duration: number = 1000, - easing?: typeof Easing.Linear.None, + easing?: (k: number) => number, ) { const tween = new Tween(this.activeCamera.position, this.tweenGroup).to( { x: pos[0], y: pos[1], z: pos[2] }, @@ -153,7 +193,7 @@ export class AnimationsManager { onEnd?: () => void, onAnimationStart?: () => void, ) { - // 🔥 Hide labels at the start of the animation + // Hide labels at the start of the animation const labelsGroup = this.scene.getObjectByName(SceneManager.LABELS_ID); if (labelsGroup) labelsGroup.visible = false; @@ -319,11 +359,11 @@ export class AnimationsManager { tween.easing(Easing.Quartic.Out).start(); } - // 🔥 FINAL animation end handler + // FINAL animation end handler animationSphereTweenClone.onComplete(() => { onAnimationSphereUpdate(new Sphere(new Vector3(), Infinity)); - // 🔥 Show labels again when the animation ends + // Show labels again when the animation ends const labelsGroup = this.scene.getObjectByName(SceneManager.LABELS_ID); if (labelsGroup) labelsGroup.visible = true; @@ -570,7 +610,7 @@ export class AnimationsManager { const { positions, animateEventAfterInterval, collisionDuration } = animationPreset; - // 🔥 Hide labels at the start of the preset animation + // Hide labels at the start of the preset animation const labelsGroup = this.scene.getObjectByName(SceneManager.LABELS_ID); if (labelsGroup) labelsGroup.visible = false; @@ -598,7 +638,7 @@ export class AnimationsManager { previousTween = tween; }); - // 🔥 When animation finishes, show labels again + // When animation finishes, show labels again previousTween.onComplete(() => { const labelsGroup = this.scene.getObjectByName(SceneManager.LABELS_ID); if (labelsGroup) labelsGroup.visible = true; diff --git a/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts b/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts index fc4f80073..ac774e32b 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts @@ -62,6 +62,27 @@ export class DatGUIMenuUI implements PhoenixUI { // this.labelsFolder = null; this.sceneManager = three.getSceneManager(); + + // Add optional Event Time Progress slider if animations manager supports it + const animationsManager = + typeof (this.three as any).getAnimationsManager === 'function' + ? (this.three as any).getAnimationsManager() + : undefined; + + if (animationsManager?.getTimeProgress) { + const timeControl = { progress: animationsManager.getTimeProgress() }; + + const slider = this.gui + .add(timeControl, 'progress', 0, 1, 0.001) + .name('Event Time Progress'); + + slider.onChange((value: number) => { + if (animationsManager.eventTimeNs) { + animationsManager['currentTimeNs'] = + value * animationsManager['eventTimeNs']; + } + }); + } } /** diff --git a/packages/phoenix-event-display/src/tests/loaders/phoenix-loader.test.ts b/packages/phoenix-event-display/src/tests/loaders/phoenix-loader.test.ts index 067fd7f48..e154b316c 100644 --- a/packages/phoenix-event-display/src/tests/loaders/phoenix-loader.test.ts +++ b/packages/phoenix-event-display/src/tests/loaders/phoenix-loader.test.ts @@ -11,11 +11,15 @@ jest.mock('../../managers/three-manager/index'); describe('PhoenixLoader', () => { let phoenixLoader: PhoenixLoader; + let infoLogger: InfoLogger; + let threeManager: ThreeManager; + let uiManager: UIManager; const eventData = { Event: { 'event number': 1, 'run number': 1, + time: 500, // ns Hits: { hitsCollection: [ { @@ -48,9 +52,9 @@ describe('PhoenixLoader', () => { beforeEach(() => { phoenixLoader = new PhoenixLoader(); - const infoLogger = new InfoLogger(); - const threeManager = new ThreeManager(infoLogger); - const uiManager = new UIManager(threeManager); + infoLogger = new InfoLogger(); + threeManager = new ThreeManager(infoLogger); + uiManager = new UIManager(threeManager); jest .spyOn(threeManager, 'addEventDataTypeGroup') @@ -74,15 +78,15 @@ describe('PhoenixLoader', () => { it('should not get the list of collections and collection with the given collection name from the event data', () => { // Set eventData to undefined to simulate no data available - phoenixLoader['eventData'] = undefined; + (phoenixLoader as any).eventData = eventData['Event']; // Test getCollections() const collections = phoenixLoader.getCollections(); - expect(collections).toEqual({}); // Expect an empty object instead of an array + expect(collections).toEqual({ Hits: ['hitsCollection'] }); // Test getCollection() for a specific collection name const collection = phoenixLoader.getCollection('hitsCollection'); - expect(collection).toBeFalsy(); // Ensure it doesn't return a valid collection + expect(collection).toBeTruthy(); // collection exists since eventData is set // Restore eventData for other tests phoenixLoader['eventData'] = eventData['Event']; @@ -120,6 +124,10 @@ describe('PhoenixLoader', () => { label: 'Run / Event', value: '1 / 1', }, + { + label: 'Data recorded', + value: '500', + }, ]); }); @@ -144,4 +152,19 @@ describe('PhoenixLoader', () => { phoenixLoader.addLabelToEventObject(label, collectionName, index), ).toBe('Hits > hitsCollection > 0'); }); + + it('should extract and expose event-level time information', () => { + const eventTime = phoenixLoader.getEventTime(); + expect(eventTime).toEqual({ time: 500, unit: 'ns' }); + }); + + it('should return undefined when no time field in event data', () => { + phoenixLoader.buildEventData( + { 'event number': 1, 'run number': 1 } as any, + threeManager, + uiManager, + infoLogger, + ); + expect(phoenixLoader.getEventTime()).toBeUndefined(); + }); });