Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/components/model-tree/model-tree.component.ts is written in an unsupported language. File is not indexed.

0001 import {
0002   ChangeDetectionStrategy,
0003   Component,
0004   computed,
0005   effect,
0006   inject,
0007   signal,
0008   untracked,
0009 } from '@angular/core';
0010 import { MatIcon } from '@angular/material/icon';
0011 
0012 import { EntityRefLink, EventPiece } from '@dexvis/firebird-core';
0013 import { DataModelService } from '../../services/data-model.service';
0014 import { PainterConfigService } from '../../services/painter-config.service';
0015 import { SelectionService } from '../../services/selection.service';
0016 
0017 interface EntityNode {
0018   index: number;
0019   label: string;
0020   refs: EntityRefLink[];
0021 }
0022 
0023 /** How many entities a piece shows before the "more" button. */
0024 const ENTITY_PAGE = 200;
0025 
0026 /**
0027  * The physics-oriented tree of the loaded event: Event → pieces → entities,
0028  * built by walking the data model (never the three.js scene — the scene tree
0029  * component stays available as the debug view).
0030  *
0031  * Entity labels and reference links come from the pieces themselves
0032  * (`entityLabel` / `entityRefs`), so extension piece types appear here
0033  * without this component knowing them. Clicking an entity selects it through
0034  * SelectionService (highlighting it in 3D); a 3D click selects here in
0035  * return. Clicking the selected entity again, or the clear button of the
0036  * selection bar, deselects. Reference links navigate to the referenced entity.
0037  */
0038 @Component({
0039   selector: 'app-model-tree',
0040   imports: [MatIcon],
0041   templateUrl: './model-tree.component.html',
0042   changeDetection: ChangeDetectionStrategy.OnPush,
0043   styleUrls: ['./model-tree.component.scss'],
0044 })
0045 export class ModelTreeComponent {
0046   private dataService = inject(DataModelService);
0047   private painterConfig = inject(PainterConfigService);
0048   public selection = inject(SelectionService);
0049 
0050   pieces = computed<EventPiece[]>(() => this.dataService.currentEntry()?.pieces ?? []);
0051 
0052   /** Names of expanded pieces. */
0053   private expanded = signal<ReadonlySet<string>>(new Set<string>());
0054 
0055   /** Per-piece entity display limits (raised by "more" and by reveals). */
0056   private limits = signal<Record<string, number>>({});
0057 
0058   /** Built entity rows per piece, dropped when the event changes. */
0059   private entityCache = new Map<EventPiece, EntityNode[]>();
0060 
0061   constructor() {
0062     effect(() => {
0063       this.dataService.currentEntry();
0064       this.entityCache.clear();
0065       this.expanded.set(new Set());
0066       this.limits.set({});
0067     });
0068 
0069     // Reveal the selection when it arrives from elsewhere (3D click):
0070     // expand the piece, make sure the row is within the display limit,
0071     // then scroll it into view. Only the selection is tracked: the reveal
0072     // reads and writes `expanded` and `limits`, and tracking them would
0073     // re-run it on every expand, collapse and "more", so the selected piece
0074     // could not be collapsed and the list would jump back to the selection.
0075     effect(() => {
0076       const selected = this.selection.selection();
0077       if (!selected) return;
0078       untracked(() => {
0079         this.expandPiece(selected.pieceName);
0080         this.ensureVisible(selected.pieceName, selected.entityIndex);
0081       });
0082       setTimeout(() => {
0083         document.getElementById(this.rowId(selected.pieceName, selected.entityIndex))
0084           ?.scrollIntoView({ block: 'nearest' });
0085       });
0086     });
0087   }
0088 
0089   isExpanded(pieceName: string): boolean {
0090     return this.expanded().has(pieceName);
0091   }
0092 
0093   /**
0094    * The eye toggle reads/writes the piece visibility config key
0095    * (`painters.byPiece.<name>.visible`) — the same key deep links and packs
0096    * set, applied to the 3D scene by EventDisplayService. Reading through the
0097    * property's signal keeps the icon in sync with changes from anywhere.
0098    *
0099    * Property CREATION is untracked: the first declare of a key applies
0100    * pending layer values (signal writes), which template evaluation — a
0101    * reactive context — forbids (NG0600).
0102    */
0103   private visibilityPropertyOf(pieceName: string) {
0104     return untracked(() => this.painterConfig.visibilityProperty(pieceName));
0105   }
0106 
0107   isPieceVisible(pieceName: string): boolean {
0108     return this.visibilityPropertyOf(pieceName).valueSignal() !== false;
0109   }
0110 
0111   togglePieceVisibility(pieceName: string): void {
0112     const property = this.visibilityPropertyOf(pieceName);
0113     property.value = property.value === false;
0114   }
0115 
0116   togglePiece(pieceName: string): void {
0117     const next = new Set(this.expanded());
0118     if (next.has(pieceName)) {
0119       next.delete(pieceName);
0120     } else {
0121       next.add(pieceName);
0122     }
0123     this.expanded.set(next);
0124   }
0125 
0126   private expandPiece(pieceName: string): void {
0127     if (this.expanded().has(pieceName)) return;
0128     const next = new Set(this.expanded());
0129     next.add(pieceName);
0130     this.expanded.set(next);
0131   }
0132 
0133   limitFor(pieceName: string): number {
0134     return this.limits()[pieceName] ?? ENTITY_PAGE;
0135   }
0136 
0137   showMore(pieceName: string): void {
0138     this.limits.update(limits => ({ ...limits, [pieceName]: this.limitFor(pieceName) + ENTITY_PAGE }));
0139   }
0140 
0141   private ensureVisible(pieceName: string, entityIndex: number): void {
0142     if (entityIndex >= this.limitFor(pieceName)) {
0143       this.limits.update(limits => ({ ...limits, [pieceName]: entityIndex + ENTITY_PAGE }));
0144     }
0145   }
0146 
0147   /** The visible entity rows of a piece (built once per piece per event). */
0148   entityNodes(piece: EventPiece): EntityNode[] {
0149     const limit = Math.min(piece.entityCount, this.limitFor(piece.name));
0150     let nodes = this.entityCache.get(piece);
0151     if (!nodes || nodes.length < limit) {
0152       nodes = [];
0153       for (let i = 0; i < limit; i++) {
0154         nodes.push({ index: i, label: piece.entityLabel(i), refs: piece.entityRefs(i) });
0155       }
0156       this.entityCache.set(piece, nodes);
0157     }
0158     return nodes.length > limit ? nodes.slice(0, limit) : nodes;
0159   }
0160 
0161   rowId(pieceName: string, entityIndex: number): string {
0162     return `model-tree-${pieceName}-${entityIndex}`.replace(/\s+/g, '_');
0163   }
0164 
0165   /** Selects the entity; clicking the selected entity again clears the selection. */
0166   selectEntity(pieceName: string, entityIndex: number): void {
0167     if (this.selection.isSelected(pieceName, entityIndex)) {
0168       this.selection.clear();
0169     } else {
0170       this.selection.select({ pieceName, entityIndex });
0171     }
0172   }
0173 
0174   clearSelection(): void {
0175     this.selection.clear();
0176   }
0177 
0178   followRef(ref: EntityRefLink): void {
0179     this.selection.select({ pieceName: ref.targetPiece, entityIndex: ref.targetIndex });
0180   }
0181 }