Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/pages/split-window/split-window.component.ts is written in an unsupported language. File is not indexed.

0001 import {
0002   ChangeDetectionStrategy,
0003   Component,
0004   ElementRef,
0005   OnDestroy,
0006   ViewChild,
0007   inject,
0008 } from '@angular/core';
0009 
0010 import { MatIcon } from '@angular/material/icon';
0011 import { MatIconButton } from '@angular/material/button';
0012 import { MatTooltip } from '@angular/material/tooltip';
0013 import { MatProgressSpinner } from '@angular/material/progress-spinner';
0014 
0015 import GUI from 'lil-gui';
0016 import { Subscription } from 'rxjs';
0017 
0018 import { EventDisplayService } from '../../services/event-display.service';
0019 import { ThreeService } from '../../services/three.service';
0020 import { CommandBusService, ConfigService } from '@dexvis/app-features';
0021 import { injectToolbarActions, ToolbarActionRegistration, type RenderView } from '@dexvis/firebird-ng/api';
0022 import { FirebirdShellComponent } from '../../components/firebird-shell/firebird-shell.component';
0023 import { EventSelectorComponent } from '../../components/event-selector/event-selector.component';
0024 import { GeometryClippingComponent } from '../../components/geometry-clipping/geometry-clipping.component';
0025 import { EventTimeControlComponent } from '../../components/event-time-control/event-time-control.component';
0026 import { AnimationSettingsComponent } from '../../components/animation-settings/animation-settings.component';
0027 import { PerfStatsComponent } from '../../components/perf-stats/perf-stats.component';
0028 import { RecordingMenuComponent } from '../../components/recording-menu/recording-menu.component';
0029 import { FirebirdDisplayComponent } from '../../components/firebird-display/firebird-display.component';
0030 
0031 /**
0032  * Quad projection view: four views of the one shared scene, each with its
0033  * own geometry cut.
0034  *
0035  * Layout (fixed): top-left TOP (looking down -Y, cut by the XZ plane),
0036  * top-right SIDE (looking along +X, cut by the YZ plane), bottom-left
0037  * FRONT/XY (looking along -Z, cut by an adjustable XY plane — the
0038  * transverse view), bottom-right the MAIN 3D view with orbit controls and
0039  * the wedge/Z clipping of the display page (the navigation cube follows the
0040  * main view here).
0041  *
0042  * The three projection views are orthographic — tracks drawn over geometry
0043  * only line up with the cross-section without perspective distortion — and
0044  * share one independently clipped geometry copy (see ClippedGeometrySlice);
0045  * each writes its own plane into it right before rendering. Event data is
0046  * never clipped and is visible in every view.
0047  *
0048  * The toolbar carries the controls that make sense here (event selection,
0049  * the 3D view's clipping, time animation); the side panes stay closed. The
0050  * shared canvas fills the central pane behind a CSS grid of view cells;
0051  * each view renders its scissored rectangle. This page is the reference
0052  * consumer of the multi-view API: addView + geometry slice + per-view clip
0053  * planes.
0054  */
0055 @Component({
0056   selector: 'app-split-window',
0057   imports: [
0058     MatIcon,
0059     MatIconButton,
0060     MatTooltip,
0061     MatProgressSpinner,
0062     FirebirdShellComponent,
0063     EventSelectorComponent,
0064     GeometryClippingComponent,
0065     EventTimeControlComponent,
0066     AnimationSettingsComponent,
0067     PerfStatsComponent,
0068     RecordingMenuComponent,
0069     FirebirdDisplayComponent,
0070   ],
0071   templateUrl: './split-window.component.html',
0072   changeDetection: ChangeDetectionStrategy.OnPush,
0073   styleUrl: './split-window.component.scss',
0074 })
0075 export class SplitWindowComponent implements OnDestroy {
0076   /** The `<firebird-display>` element: the shared canvas fills it, behind the view cells. */
0077   @ViewChild(FirebirdDisplayComponent, { read: ElementRef }) host!: ElementRef<HTMLElement>;
0078   @ViewChild('cellTop') cellTop!: ElementRef<HTMLElement>;
0079   @ViewChild('cellSide') cellSide!: ElementRef<HTMLElement>;
0080   @ViewChild('cellFront') cellFront!: ElementRef<HTMLElement>;
0081   @ViewChild('cellMain') cellMain!: ElementRef<HTMLElement>;
0082 
0083   /** Views added by this page (the main view is not in this list — it stays). */
0084   private addedViews: RenderView[] = [];
0085 
0086   /** Demonstrator control panel (cut positions, tracks-on-top). */
0087   private gui?: GUI;
0088   private configSubscriptions: Subscription[] = [];
0089 
0090   private readonly commandBus = inject(CommandBusService);
0091   /** Buttons packs add to the time controls (withToolbarAction). */
0092   readonly toolbarActions = injectToolbarActions();
0093 
0094   constructor(
0095     public eventDisplay: EventDisplayService,
0096     private three: ThreeService,
0097     private config: ConfigService,
0098   ) {}
0099 
0100   /** Dispatches a toolbar action's command; a failure reaches the user's error channel. */
0101   runToolbarAction(action: ToolbarActionRegistration): void {
0102     this.commandBus.dispatch({ ...action.command, source: 'ui' })
0103       .catch(error => this.eventDisplay.reportError(
0104         `Command '${action.command.type}' failed: ${error instanceof Error ? error.message : error}`));
0105   }
0106 
0107   /** Loading indicators shared with the display page (service-owned). */
0108   get loadingDex() { return this.eventDisplay.loadingDex; }
0109   get loadingEdm() { return this.eventDisplay.loadingEdm; }
0110   get loadingGeometry() { return this.eventDisplay.loadingGeometry; }
0111 
0112   /**
0113    * The display is attached to `<firebird-display>`, before the configured
0114    * sources load: move the main view into its cell and add the projection
0115    * views. Revisits re-attach the existing scene the same way.
0116    */
0117   onDisplayAttached(): void {
0118     this.three.setMainViewContainer(this.cellMain.nativeElement);
0119 
0120     // The projection views' geometry copy. Empty until geometry loads:
0121     // EventDisplayService rebuilds it after every geometry load.
0122     const slice = this.three.createGeometrySlice();
0123 
0124     // Orthographic projections in the standard frame: beam along Z pointing
0125     // right on screen. Each clip plane removes the geometry half between the
0126     // camera and the cut, so the view shows the cross-section (visible side
0127     // of a plane is normal·p + constant >= 0). Initial visible height fits
0128     // the whole detector. A view created with `geometrySlice` renders the
0129     // slice copy instead of the original geometry.
0130     const orthoHeight = 15000;
0131     this.addedViews = [
0132       this.three.addView({
0133         name: 'top',
0134         container: this.cellTop.nativeElement,
0135         orthographic: true,
0136         orthoWorldHeight: orthoHeight,
0137         fixedDirection: { direction: [0, 1, 0], up: [1, 0, 0] },
0138         geometrySlice: slice,
0139         clipPlane: { normal: [0, -1, 0], constant: 0 }, // keep y <= 0
0140         tracksOnTop: true,
0141       }),
0142       this.three.addView({
0143         name: 'side',
0144         container: this.cellSide.nativeElement,
0145         orthographic: true,
0146         orthoWorldHeight: orthoHeight,
0147         fixedDirection: { direction: [-1, 0, 0], up: [0, 1, 0] },
0148         geometrySlice: slice,
0149         clipPlane: { normal: [1, 0, 0], constant: 0 }, // keep x >= 0
0150         tracksOnTop: true,
0151       }),
0152       this.three.addView({
0153         name: 'front',
0154         container: this.cellFront.nativeElement,
0155         orthographic: true,
0156         orthoWorldHeight: orthoHeight,
0157         fixedDirection: { direction: [0, 0, 1], up: [0, 1, 0] },
0158         geometrySlice: slice,
0159         clipPlane: { normal: [0, 0, -1], constant: 0 }, // keep z <= cut position
0160         tracksOnTop: true,
0161       }),
0162     ];
0163 
0164     this.setupDemoControls();
0165   }
0166 
0167   ngOnDestroy(): void {
0168     // <firebird-display> detached the display already (child before parent)
0169     for (const subscription of this.configSubscriptions) {
0170       subscription.unsubscribe();
0171     }
0172     this.configSubscriptions = [];
0173     this.gui?.destroy();
0174     this.gui = undefined;
0175     for (const view of this.addedViews) {
0176       this.three.removeView(view);
0177     }
0178     this.addedViews = [];
0179     // Restores single-copy layer routing for the display page.
0180     this.three.removeGeometrySlice();
0181   }
0182 
0183   /**
0184    * The demonstrator control panel (lil-gui): per-view cut positions and
0185    * tracks-on-top toggles. Every control is backed by a declared config key,
0186    * so the same state is deep-linkable and scriptable like any other setting
0187    * (`?config.quadView.front.clipPos=1500`) and survives reloads; the GUI is
0188    * one writer among many, not the source of truth.
0189    */
0190   private setupDemoControls(): void {
0191     const viewByName = (name: string) => name === 'main'
0192       ? this.three.mainView
0193       : this.addedViews.find(view => view.name === name);
0194 
0195     this.gui = new GUI({ container: this.host.nativeElement, title: 'Quad view' });
0196     const guiRoot = this.gui.domElement;
0197     guiRoot.style.position = 'absolute';
0198     guiRoot.style.top = '8px';
0199     guiRoot.style.right = '8px';
0200     guiRoot.style.zIndex = '3';
0201 
0202     const state: Record<string, number | boolean> = {};
0203 
0204     // Cut positions [mm]. Each view's plane keeps its fixed normal; the
0205     // position moves the cut along that normal's axis (see the addView
0206     // calls for the keep-side conventions).
0207     const cuts = this.gui.addFolder('Cut position [mm]');
0208     const cutDefs: Array<{ key: string; view: string; label: string; toConstant: (pos: number) => number }> = [
0209       { key: 'quadView.top.clipPos',   view: 'top',   label: 'Top cut (y)',   toConstant: pos => pos },
0210       { key: 'quadView.side.clipPos',  view: 'side',  label: 'Side cut (x)',  toConstant: pos => -pos },
0211       { key: 'quadView.front.clipPos', view: 'front', label: 'Front cut (z)', toConstant: pos => pos },
0212     ];
0213     for (const def of cutDefs) {
0214       const property = this.config.getConfigOrCreate<number>(def.key, 0);
0215       state[def.key] = property.value;
0216       const controller = cuts.add(state, def.key, -5000, 5000, 10).name(def.label)
0217         .onChange((value: number) => { property.value = value; });
0218       this.configSubscriptions.push(property.subject.subscribe(value => {
0219         const view = viewByName(def.view);
0220         if (view?.clipPlane) {
0221           view.clipPlane.constant = def.toConstant(value);
0222           view.dirty = true; // render-on-demand: schedule a frame
0223         }
0224         if (state[def.key] !== value) { state[def.key] = value; controller.updateDisplay(); }
0225       }));
0226     }
0227 
0228     // Tracks-on-top: event data drawn over geometry regardless of depth.
0229     // Defaults: ON for the projection views, OFF for the 3D view.
0230     const tracks = this.gui.addFolder('Tracks on top');
0231     const trackDefs: Array<{ key: string; view: string; label: string; initial: boolean }> = [
0232       { key: 'quadView.top.tracksOnTop',   view: 'top',   label: 'Top',   initial: true },
0233       { key: 'quadView.side.tracksOnTop',  view: 'side',  label: 'Side',  initial: true },
0234       { key: 'quadView.front.tracksOnTop', view: 'front', label: 'Front', initial: true },
0235       { key: 'quadView.main.tracksOnTop',  view: 'main',  label: '3D',    initial: false },
0236     ];
0237     for (const def of trackDefs) {
0238       const property = this.config.getConfigOrCreate<boolean>(def.key, def.initial);
0239       state[def.key] = property.value;
0240       const controller = tracks.add(state, def.key).name(def.label)
0241         .onChange((value: boolean) => { property.value = value; });
0242       this.configSubscriptions.push(property.subject.subscribe(value => {
0243         const view = viewByName(def.view);
0244         if (view) {
0245           view.tracksOnTop = value;
0246           view.dirty = true; // render-on-demand: schedule a frame
0247         }
0248         if (state[def.key] !== value) { state[def.key] = value; controller.updateDisplay(); }
0249       }));
0250     }
0251   }
0252 }