Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 96 additions & 9 deletions packages/phoenix-event-display/src/loaders/event-data-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,123 @@ 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,
ui: UIManager,
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 };
}
26 changes: 26 additions & 0 deletions packages/phoenix-event-display/src/loaders/phoenix-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand All @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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] },
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ export class DatGUIMenuUI implements PhoenixUI<GUI> {
// 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'];
}
});
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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')
Expand All @@ -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'];
Expand Down Expand Up @@ -120,6 +124,10 @@ describe('PhoenixLoader', () => {
label: 'Run / Event',
value: '1 / 1',
},
{
label: 'Data recorded',
value: '500',
},
]);
});

Expand All @@ -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();
});
});
Loading