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
100 changes: 100 additions & 0 deletions packages/phoenix-event-display/src/event-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { LoadingManager } from './managers/loading-manager';
import { StateManager } from './managers/state-manager';
import type { AnimationPreset } from './managers/three-manager/animations-manager';
import { ThreeManager } from './managers/three-manager/index';
import { SceneManager } from './managers/three-manager/scene-manager';
import { XRSessionType } from './managers/three-manager/xr/xr-manager';
import { UIManager } from './managers/ui-manager/index';
import { URLOptionsManager } from './managers/url-options-manager';
Expand Down Expand Up @@ -49,6 +50,8 @@ export class EventDisplay {
private onEventsChange: ((events: any) => void)[] = [];
/** Array containing callbacks to be called when the displayed event changes. */
private onDisplayedEventChange: ((nowDisplayingEvent: any) => void)[] = [];
/** Callbacks to be called on scene state / visibility / cut changes. */
private onStateChange: (() => void)[] = [];
/** Generic event bus for integration with external frameworks. */
private eventBus: Map<string, Set<(data: any) => void>> = new Map();
/** Wildcard subscribers fired on every emit (recorders, external bridges). */
Expand Down Expand Up @@ -85,6 +88,7 @@ export class EventDisplay {
this.infoLogger = new InfoLogger();
this.graphicsLibrary = new ThreeManager(this.infoLogger);
this.ui = new UIManager(this.graphicsLibrary);
this.ui.onStateChange = () => this.triggerStateChange();
if (configuration) {
this.init(configuration);
}
Expand Down Expand Up @@ -822,6 +826,102 @@ export class EventDisplay {
};
}

/**
* Add a callback to onStateChange array to call
* when visibility or cuts change.
* @param callback Callback to be added to the onStateChange array.
* @returns Unsubscribe function to remove the callback.
*/
public listenToStateChange(callback: () => void): () => void {
this.onStateChange.push(callback);
return () => {
const index = this.onStateChange.indexOf(callback);
if (index > -1) {
this.onStateChange.splice(index, 1);
}
};
}

/**
* Trigger state change callbacks (e.g. on visibility or cut change).
*/
public triggerStateChange(): void {
this.onStateChange.forEach((callback) => callback());
}

/**
* Check if a collection group is visible in the 3D scene.
* @param collectionName Name of the collection.
* @returns Whether the collection group is visible.
*/
public isCollectionVisible(collectionName: string): boolean {
const sceneManager = this.getThreeManager()?.getSceneManager();
if (!sceneManager) return true;
const eventDataGroup = sceneManager
.getScene()
.getObjectByName(SceneManager.EVENT_DATA_ID);
if (!eventDataGroup || !eventDataGroup.visible) return false;
const collectionObject = eventDataGroup.getObjectByName(collectionName);
if (!collectionObject) return false;
return collectionObject.visible;
}

/**
* Check if a specific event data object item is visible in the 3D scene (and passes cuts).
* @param collectionName Name of the collection.
* @param item The event data item.
* @returns Whether the item is visible.
*/
public isItemVisible(collectionName: string, item: any): boolean {
if (!item) return false;
if (!this.isCollectionVisible(collectionName)) {
return false;
}
// Check 3D object visibility if object with item.uuid exists in scene
const sceneManager = this.getThreeManager()?.getSceneManager();
if (sceneManager && item.uuid) {
const eventDataGroup = sceneManager
.getScene()
.getObjectByName(SceneManager.EVENT_DATA_ID);
const collectionObject =
eventDataGroup?.getObjectByName(collectionName);
if (collectionObject) {
const obj =
collectionObject.getObjectByName(item.uuid) ||
collectionObject.children.find(
(child: any) =>
child.userData?.uuid === item.uuid || child.uuid === item.uuid,
);
if (obj && obj.visible === false) {
return false;
}
}
}

// Check active cuts for this collection
const cutsMap = this.getUIManager()
?.getPhoenixMenuUI()
?.getCollectionCuts();
const cuts = cutsMap?.[collectionName];
if (cuts && cuts.length > 0) {
for (const cut of cuts) {
let val = item[cut.field];
if (val === undefined && cut.field === 'pT') {
if (item.dparams && item.dparams.length >= 5) {
val = Math.abs(1 / item.dparams[4]) * Math.sin(item.dparams[3]);
}
}
if (val !== undefined && val !== null) {
if (!cut.cutPassed(val)) {
return false;
}
}
}
}

return true;
}

/**
* Get metadata associated to the displayed event (experiment info, time, run, event...).
* @returns Metadata of the displayed event.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export class DatGUIMenuUI implements PhoenixUI<GUI> {
private eventFolder: GUI;
/** dat.GUI menu folder containing labels. */
private labelsFolder: GUI;
/** Callback fired when UI state / visibility / cuts change. */
public onStateChange?: () => void;

/** Max changeable position of an object along the x-axis. */
private maxPositionX = 4000;
Expand Down Expand Up @@ -292,9 +294,10 @@ export class DatGUIMenuUI implements PhoenixUI<GUI> {
showMenu.onChange((value) => {
const collectionObject = this.sceneManager
.getObjectByName(SceneManager.EVENT_DATA_ID)
.getObjectByName(collectionName);
?.getObjectByName(collectionName);
if (collectionObject)
this.sceneManager.objectVisibility(collectionObject, value);
this.onStateChange?.();
});

// A color picker is added to the collection's folder
Expand Down Expand Up @@ -345,6 +348,7 @@ export class DatGUIMenuUI implements PhoenixUI<GUI> {
minCut.onChange((value) => {
cut.minValue = value;
this.sceneManager.collectionFilter(collectionName, cuts);
this.onStateChange?.();
});
const maxCut = cutsFolder
.add(
Expand All @@ -357,6 +361,7 @@ export class DatGUIMenuUI implements PhoenixUI<GUI> {
maxCut.onChange((value) => {
cut.maxValue = value;
this.sceneManager.collectionFilter(collectionName, cuts);
this.onStateChange?.();
});
}
}
Expand Down
13 changes: 10 additions & 3 deletions packages/phoenix-event-display/src/managers/ui-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export class UIManager {
private stateManager: StateManager;
/** Stored keydown handler for cleanup. */
private keydownHandler: ((e: KeyboardEvent) => void) | null = null;
/** Callback fired on UI state / visibility / cut changes. */
public onStateChange?: () => void;

/**
* Constructor for the UI manager.
Expand All @@ -111,12 +113,17 @@ export class UIManager {
// UI Menus
this.uiMenus = [];
if (configuration.enableDatGUIMenu) {
this.uiMenus.push(new DatGUIMenuUI(configuration.elementId, this.three));
const datGui = new DatGUIMenuUI(configuration.elementId, this.three);
datGui.onStateChange = () => this.onStateChange?.();
this.uiMenus.push(datGui);
}
if (configuration.phoenixMenuRoot) {
this.uiMenus.push(
new PhoenixMenuUI(configuration.phoenixMenuRoot, this.three),
const phoenixMenu = new PhoenixMenuUI(
configuration.phoenixMenuRoot,
this.three,
);
phoenixMenu.onStateChange = () => this.onStateChange?.();
this.uiMenus.push(phoenixMenu);
}
if (!configuration.forceColourTheme) {
// Detect UI color scheme
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export class PhoenixMenuUI implements PhoenixUI<PhoenixMenuNode> {
/** Registry of active cuts per collection name for re-application on event switch. */
private collectionCuts: { [collectionName: string]: Cut[] } = {};

/** Callback fired when UI state / visibility / cuts change. */
public onStateChange?: () => void;

/**
* Create Phoenix menu UI with different controls related to detector geometry and event data.
* @param phoenixMenuRoot Root node of the Phoenix menu.
Expand Down Expand Up @@ -246,9 +249,10 @@ export class PhoenixMenuUI implements PhoenixUI<PhoenixMenuNode> {
(value: boolean) => {
const collectionObject = this.sceneManager
.getObjectByName(SceneManager.EVENT_DATA_ID)
.getObjectByName(collectionName);
?.getObjectByName(collectionName);
if (collectionObject)
this.sceneManager.objectVisibility(collectionObject, value);
this.onStateChange?.();
},
);

Expand Down Expand Up @@ -314,15 +318,17 @@ export class PhoenixMenuUI implements PhoenixUI<PhoenixMenuNode> {
for (const cut of cuts) {
cut.reset();
}
this.onStateChange?.();
},
});

// Add range sliders for cuts
for (const cut of cuts) {
cutsOptionsNode.addConfig(
cut.getConfigRangeSlider(() =>
this.sceneManager.collectionFilter(collectionName, cuts),
),
cut.getConfigRangeSlider(() => {
this.sceneManager.collectionFilter(collectionName, cuts);
this.onStateChange?.();
}),
);
}
}
Expand Down Expand Up @@ -546,6 +552,7 @@ export class PhoenixMenuUI implements PhoenixUI<PhoenixMenuNode> {
for (const [collectionName, cuts] of Object.entries(this.collectionCuts)) {
this.sceneManager.collectionFilter(collectionName, cuts);
}
this.onStateChange?.();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EtaPhiPanelOverlayComponent } from './eta-phi-panel-overlay.component';
import { EventDisplayService } from '../../../../services/event-display.service';
import { PhoenixUIModule } from '../../../phoenix-ui.module';

describe('EtaPhiPanelOverlayComponent', () => {
let component: EtaPhiPanelOverlayComponent;
let fixture: ComponentFixture<EtaPhiPanelOverlayComponent>;

const mockEventDisplay = {
listenToDisplayedEventChange: jest.fn((callback) => {
callback();
return jest.fn();
}),
listenToStateChange: jest.fn((callback) => {
callback();
return jest.fn();
}),
getCollections: jest.fn().mockReturnValue({
CaloCells: ['CaloCellsCollection'],
Tracks: ['TracksCollection'],
Jets: ['JetsCollection'],
}),
getCollection: jest.fn().mockImplementation((name: string) => {
if (name === 'CaloCellsCollection') {
return [{ eta: 0.1, phi: 0.2, energy: 5000 }];
}
if (name === 'TracksCollection') {
return [{ eta: 0.5, phi: 0.5, pT: 2000, uuid: 'trk1' }];
}
if (name === 'JetsCollection') {
return [{ eta: -0.2, phi: 1.0, energy: 10000, uuid: 'jet1' }];
}
return [];
}),
isCollectionVisible: jest.fn().mockReturnValue(true),
isItemVisible: jest.fn().mockReturnValue(true),
highlightObject: jest.fn(),
lookAtObject: jest.fn(),
};

beforeEach(() => {
TestBed.configureTestingModule({
imports: [PhoenixUIModule],
providers: [
{
provide: EventDisplayService,
useValue: mockEventDisplay,
},
],
declarations: [EtaPhiPanelOverlayComponent],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(EtaPhiPanelOverlayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should listen to state changes on init', () => {
expect(mockEventDisplay.listenToStateChange).toHaveBeenCalled();
});

it('should filter markers when a collection is not visible', () => {
mockEventDisplay.isCollectionVisible.mockImplementation(
(collName: string) => collName !== 'TracksCollection',
);
(component as any).rebuildData();
const trackMarker = (component as any).markers.find(
(m: any) => m.type === 'track',
);
expect(trackMarker).toBeUndefined();
});

it('should filter markers when item is hidden by cuts', () => {
mockEventDisplay.isCollectionVisible.mockReturnValue(true);
mockEventDisplay.isItemVisible.mockImplementation(
(_collName: string, item: any) => item.uuid !== 'trk1',
);
(component as any).rebuildData();
const trackMarker = (component as any).markers.find(
(m: any) => m.type === 'track',
);
expect(trackMarker).toBeUndefined();
});
});
Loading
Loading