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 }