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 }