From c63d9f1ac92ab42d63d886d50dc423d220867410 Mon Sep 17 00:00:00 2001 From: Dashrat Date: Sat, 25 Jul 2026 22:46:06 +0530 Subject: [PATCH] fix(event-display): sync eta-phi view with 3d view visibility and cuts --- .../src/event-display.ts | 100 ++++++++++++++++++ .../src/managers/ui-manager/dat-gui-ui.ts | 7 +- .../src/managers/ui-manager/index.ts | 13 ++- .../phoenix-menu/phoenix-menu-ui.ts | 15 ++- .../eta-phi-panel-overlay.component.test.ts | 91 ++++++++++++++++ .../eta-phi-panel-overlay.component.ts | 45 ++++++++ 6 files changed, 263 insertions(+), 8 deletions(-) create mode 100644 packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/eta-phi-panel/eta-phi-panel-overlay/eta-phi-panel-overlay.component.test.ts diff --git a/packages/phoenix-event-display/src/event-display.ts b/packages/phoenix-event-display/src/event-display.ts index 069545c4a..e9d4e4962 100644 --- a/packages/phoenix-event-display/src/event-display.ts +++ b/packages/phoenix-event-display/src/event-display.ts @@ -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'; @@ -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 void>> = new Map(); /** Wildcard subscribers fired on every emit (recorders, external bridges). */ @@ -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); } @@ -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. 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 cc331a6a1..9f0430fca 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 @@ -34,6 +34,8 @@ export class DatGUIMenuUI implements PhoenixUI { 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; @@ -292,9 +294,10 @@ export class DatGUIMenuUI implements PhoenixUI { 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 @@ -345,6 +348,7 @@ export class DatGUIMenuUI implements PhoenixUI { minCut.onChange((value) => { cut.minValue = value; this.sceneManager.collectionFilter(collectionName, cuts); + this.onStateChange?.(); }); const maxCut = cutsFolder .add( @@ -357,6 +361,7 @@ export class DatGUIMenuUI implements PhoenixUI { maxCut.onChange((value) => { cut.maxValue = value; this.sceneManager.collectionFilter(collectionName, cuts); + this.onStateChange?.(); }); } } diff --git a/packages/phoenix-event-display/src/managers/ui-manager/index.ts b/packages/phoenix-event-display/src/managers/ui-manager/index.ts index 4f25a1f43..f9aac8d06 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/index.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/index.ts @@ -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. @@ -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 diff --git a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts index 9c792d761..a2d852b32 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts @@ -34,6 +34,9 @@ export class PhoenixMenuUI implements PhoenixUI { /** 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. @@ -246,9 +249,10 @@ export class PhoenixMenuUI implements PhoenixUI { (value: boolean) => { const collectionObject = this.sceneManager .getObjectByName(SceneManager.EVENT_DATA_ID) - .getObjectByName(collectionName); + ?.getObjectByName(collectionName); if (collectionObject) this.sceneManager.objectVisibility(collectionObject, value); + this.onStateChange?.(); }, ); @@ -314,15 +318,17 @@ export class PhoenixMenuUI implements PhoenixUI { 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?.(); + }), ); } } @@ -546,6 +552,7 @@ export class PhoenixMenuUI implements PhoenixUI { for (const [collectionName, cuts] of Object.entries(this.collectionCuts)) { this.sceneManager.collectionFilter(collectionName, cuts); } + this.onStateChange?.(); } /** diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/eta-phi-panel/eta-phi-panel-overlay/eta-phi-panel-overlay.component.test.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/eta-phi-panel/eta-phi-panel-overlay/eta-phi-panel-overlay.component.test.ts new file mode 100644 index 000000000..ca178669e --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/eta-phi-panel/eta-phi-panel-overlay/eta-phi-panel-overlay.component.test.ts @@ -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; + + 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(); + }); +}); diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/eta-phi-panel/eta-phi-panel-overlay/eta-phi-panel-overlay.component.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/eta-phi-panel/eta-phi-panel-overlay/eta-phi-panel-overlay.component.ts index 141071c74..6e381b22d 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/eta-phi-panel/eta-phi-panel-overlay/eta-phi-panel-overlay.component.ts +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/eta-phi-panel/eta-phi-panel-overlay/eta-phi-panel-overlay.component.ts @@ -78,6 +78,9 @@ export class EtaPhiPanelOverlayComponent this.eventDisplay.listenToDisplayedEventChange(() => { this.rebuildData(); }), + this.eventDisplay.listenToStateChange?.(() => { + this.rebuildData(); + }), ); } @@ -215,6 +218,9 @@ export class EtaPhiPanelOverlayComponent for (const caloType of caloTypes) { if (!collections[caloType]) continue; for (const collName of collections[caloType]) { + if (this.eventDisplay.isCollectionVisible?.(collName) === false) { + continue; + } const items = this.eventDisplay.getCollection(collName); if (!items) continue; for (const cell of items) { @@ -222,6 +228,9 @@ export class EtaPhiPanelOverlayComponent continue; } if (cell.energy <= energyThreshold) continue; + if (this.eventDisplay.isItemVisible?.(collName, cell) === false) { + continue; + } const ei = Math.floor( ((cell.eta - etaRange[0]) / (etaRange[1] - etaRange[0])) * etaBins, @@ -255,10 +264,16 @@ export class EtaPhiPanelOverlayComponent // Jets if (collections['Jets']) { for (const collName of collections['Jets']) { + if (this.eventDisplay.isCollectionVisible?.(collName) === false) { + continue; + } const jets = this.eventDisplay.getCollection(collName); if (!jets) continue; jets.forEach((jet: any, i: number) => { if (jet.eta == null || jet.phi == null) return; + if (this.eventDisplay.isItemVisible?.(collName, jet) === false) { + return; + } this.markers.push({ eta: jet.eta, phi: jet.phi, @@ -275,10 +290,16 @@ export class EtaPhiPanelOverlayComponent // Muons if (collections['Muons']) { for (const collName of collections['Muons']) { + if (this.eventDisplay.isCollectionVisible?.(collName) === false) { + continue; + } const muons = this.eventDisplay.getCollection(collName); if (!muons) continue; muons.forEach((mu: any, i: number) => { if (mu.eta == null || mu.phi == null) return; + if (this.eventDisplay.isItemVisible?.(collName, mu) === false) { + return; + } this.markers.push({ eta: mu.eta, phi: mu.phi, @@ -294,10 +315,16 @@ export class EtaPhiPanelOverlayComponent // Electrons if (collections['Electrons']) { for (const collName of collections['Electrons']) { + if (this.eventDisplay.isCollectionVisible?.(collName) === false) { + continue; + } const electrons = this.eventDisplay.getCollection(collName); if (!electrons) continue; electrons.forEach((el: any, i: number) => { if (el.eta == null || el.phi == null) return; + if (this.eventDisplay.isItemVisible?.(collName, el) === false) { + return; + } this.markers.push({ eta: el.eta, phi: el.phi, @@ -313,10 +340,16 @@ export class EtaPhiPanelOverlayComponent // Photons if (collections['Photons']) { for (const collName of collections['Photons']) { + if (this.eventDisplay.isCollectionVisible?.(collName) === false) { + continue; + } const photons = this.eventDisplay.getCollection(collName); if (!photons) continue; photons.forEach((ph: any, i: number) => { if (ph.eta == null || ph.phi == null) return; + if (this.eventDisplay.isItemVisible?.(collName, ph) === false) { + return; + } this.markers.push({ eta: ph.eta, phi: ph.phi, @@ -332,6 +365,9 @@ export class EtaPhiPanelOverlayComponent // Tracks (derive eta/phi from dparams if needed) if (collections['Tracks']) { for (const collName of collections['Tracks']) { + if (this.eventDisplay.isCollectionVisible?.(collName) === false) { + continue; + } const tracks = this.eventDisplay.getCollection(collName); if (!tracks) continue; tracks.forEach((trk: any, i: number) => { @@ -343,6 +379,9 @@ export class EtaPhiPanelOverlayComponent phi = trk.dparams[2]; } if (eta == null || phi == null) return; + if (this.eventDisplay.isItemVisible?.(collName, trk) === false) { + return; + } this.markers.push({ eta, phi, @@ -358,10 +397,16 @@ export class EtaPhiPanelOverlayComponent // MET if (collections['MissingEnergy']) { for (const collName of collections['MissingEnergy']) { + if (this.eventDisplay.isCollectionVisible?.(collName) === false) { + continue; + } const metItems = this.eventDisplay.getCollection(collName); if (!metItems) continue; for (const met of metItems) { if (met.etx == null || met.ety == null) continue; + if (this.eventDisplay.isItemVisible?.(collName, met) === false) { + continue; + } const metPhi = Math.atan2(met.ety, met.etx); const metMag = Math.sqrt(met.etx * met.etx + met.ety * met.ety); this.markers.push({