Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/components/painter-config-panel/painter-config-panel.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   linkedSignal,
0008   untracked,
0009 } from '@angular/core';
0010 import { form, FormField } from '@angular/forms/signals';
0011 import { Listbox, Option } from '@angular/aria/listbox';
0012 
0013 import {
0014   EventPiece,
0015   PainterConfigDescriptor,
0016   PiecePainterConstructor,
0017   painterIdOf,
0018   painterMetaOf,
0019 } from '@dexvis/firebird-core';
0020 import { DataModelService } from '../../services/data-model.service';
0021 import { EventDisplayService } from '../../services/event-display.service';
0022 import { PainterConfigService } from '../../services/painter-config.service';
0023 import { SelectionService } from '../../services/selection.service';
0024 
0025 /**
0026  * The painter panel of the right pane: for the piece selected in the model
0027  * tree (or via a 3D pick) it shows WHICH painter draws the piece and the
0028  * selected painter's knobs — both auto-rendered from the painter's static
0029  * meta, no per-painter UI code.
0030  *
0031  * Everything routes through config keys (`painters.byPiece.<piece>` and
0032  * `painters.byPiece.<piece>.<knob>`), so the same state is reachable from
0033  * yaml/server config, URL deep links and batch scripts; edits here apply
0034  * live through the painter's `onConfigChanged`.
0035  *
0036  * Form plumbing is Signal Forms (`form` + `[formField]`) over a model built
0037  * from the knob declarations; pickers are Angular Aria listboxes.
0038  */
0039 @Component({
0040   selector: 'app-painter-config-panel',
0041   imports: [FormField, Listbox, Option],
0042   templateUrl: './painter-config-panel.component.html',
0043   changeDetection: ChangeDetectionStrategy.OnPush,
0044   styleUrls: ['./painter-config-panel.component.scss'],
0045 })
0046 export class PainterConfigPanelComponent {
0047   private dataService = inject(DataModelService);
0048   private eventDisplay = inject(EventDisplayService);
0049   private painterConfig = inject(PainterConfigService);
0050   public selection = inject(SelectionService);
0051 
0052   /** The piece the panel configures (from the shared selection). */
0053   piece = computed<EventPiece | null>(() => {
0054     const pieceName = this.selection.selectedPiece();
0055     if (!pieceName) return null;
0056     return this.dataService.currentEntry()?.pieces.find(piece => piece.name === pieceName) ?? null;
0057   });
0058 
0059   /** Painter classes registered for the piece's type. */
0060   candidates = computed<readonly PiecePainterConstructor[]>(() => {
0061     const piece = this.piece();
0062     return piece ? this.eventDisplay.paintersForType(piece.type) : [];
0063   });
0064 
0065   painterOptions = computed(() => this.candidates().map(candidate => ({
0066     id: painterIdOf(candidate),
0067     label: painterMetaOf(candidate)?.label ?? painterIdOf(candidate),
0068   })));
0069 
0070   /** The selected painter id (reactive through the selection config key). */
0071   selectedPainterId = computed<string | null>(() => {
0072     const piece = this.piece();
0073     const candidates = this.candidates();
0074     if (!piece || candidates.length === 0) return null;
0075     return this.painterConfig.selectionProperty(piece.name, candidates).valueSignal();
0076   });
0077 
0078   /** Aria listbox holds selections as arrays; resets when the piece changes. */
0079   painterListboxValue = linkedSignal<string[]>(() => {
0080     const id = this.selectedPainterId();
0081     return id ? [id] : [];
0082   });
0083 
0084   private selectedPainterClass = computed<PiecePainterConstructor | null>(() => {
0085     const id = this.selectedPainterId();
0086     return this.candidates().find(candidate => painterIdOf(candidate) === id)
0087       ?? this.candidates()[0]
0088       ?? null;
0089   });
0090 
0091   /** Knob declarations of the selected painter. */
0092   descriptors = computed<readonly PainterConfigDescriptor[]>(() =>
0093     painterMetaOf(this.selectedPainterClass() ?? undefined as never)?.configs ?? []);
0094 
0095   /** The declared config properties behind the knobs. */
0096   private knobProperties = computed(() => {
0097     const piece = this.piece();
0098     const painterClass = this.selectedPainterClass();
0099     if (!piece || !painterClass) return new Map<string, import('@dexvis/app-features').ConfigProperty<unknown>>();
0100     return this.painterConfig.knobProperties(piece, painterClass);
0101   });
0102 
0103   /**
0104    * Signal Forms model over the knob values. linkedSignal: recomputes from
0105    * the config properties (so URL/server/other-writer changes reach the
0106    * form), stays writable (so form edits land here first).
0107    */
0108   formModel = linkedSignal<Record<string, unknown>>(() => {
0109     const model: Record<string, unknown> = {};
0110     for (const [key, property] of this.knobProperties()) {
0111       model[key] = property.valueSignal();
0112     }
0113     return model;
0114   });
0115 
0116   /** The Signal Forms field tree the inputs bind to via [formField]. */
0117   panelForm = form(this.formModel);
0118 
0119   constructor() {
0120     // Form edits -> config writes. The write triggers the painter's live
0121     // restyle (EventDisplayService watches the knob keys). Values coming
0122     // back through the linkedSignal recompute equal the written ones, so
0123     // this settles without loops.
0124     effect(() => {
0125       const values = this.formModel();
0126       const properties = untracked(() => this.knobProperties());
0127       for (const [key, property] of properties) {
0128         if (!(key in values)) continue;
0129         const value = this.coerceLike(values[key], property.value);
0130         if (value !== property.value) {
0131           property.setValue(value);
0132         }
0133       }
0134     });
0135   }
0136 
0137   /** Field of one knob for [formField] binding. */
0138   field(key: string) {
0139     return (this.panelForm as unknown as Record<string, unknown>)[key] as never;
0140   }
0141 
0142   onPainterPicked(ids: string[]): void {
0143     const piece = this.piece();
0144     const candidates = this.candidates();
0145     if (!piece || ids.length === 0) return;
0146     this.painterConfig.selectionProperty(piece.name, candidates).setValue(ids[0]);
0147   }
0148 
0149   /** Knob listboxes: single-select over the descriptor options. */
0150   knobListboxValue(key: string): unknown[] {
0151     const value = this.formModel()[key];
0152     return value === undefined ? [] : [value];
0153   }
0154 
0155   onKnobOptionPicked(key: string, values: unknown[]): void {
0156     if (values.length === 0) return;
0157     this.formModel.update(model => ({ ...model, [key]: values[0] }));
0158   }
0159 
0160   onRangeInput(key: string, event: Event): void {
0161     const value = Number((event.target as HTMLInputElement).value);
0162     this.formModel.update(model => ({ ...model, [key]: value }));
0163   }
0164 
0165   /** Which control a descriptor renders as. */
0166   controlKind(descriptor: PainterConfigDescriptor): 'options' | 'range' | 'color' | 'checkbox' | 'text' {
0167     if (descriptor.options?.length) return 'options';
0168     if (typeof descriptor.default === 'number') return 'range';
0169     if (typeof descriptor.default === 'boolean') return 'checkbox';
0170     if (typeof descriptor.default === 'string' && descriptor.default.startsWith('#')) return 'color';
0171     return 'text';
0172   }
0173 
0174   knobValue(key: string): unknown {
0175     return this.formModel()[key];
0176   }
0177 
0178   optionLabel(option: unknown): string {
0179     return String(option);
0180   }
0181 
0182   /** Coerces form strings back to the property's type (number inputs). */
0183   private coerceLike(value: unknown, sample: unknown): unknown {
0184     if (typeof sample === 'number' && typeof value === 'string') {
0185       const parsed = Number(value);
0186       return isNaN(parsed) ? value : parsed;
0187     }
0188     return value;
0189   }
0190 }