-
Couldn't load subscription status.
- Fork 3
Description
We now have a left panel with object trees (and possibly other trees). When we click on the object (or maybe add a special icon) the right panel should open with object configuration such as visualization options. #35 will work with that.
The next is Clauda Brainstormed implementation ideas (so yes, I am aware it is AI generated):
Painter Configuration System Design Summary
Context and Overview
The Firebird project is an Angular-based visualization library where:
- Data objects are rendered by "Painter" components that determine visual appearance
- Users need to configure painter properties (colors, sizes, opacities, etc.)
- Third-party developers can extend the system with custom data objects and painters
- We need a flexible UI system to allow configuring different painter types
Requirements
- Create a configuration UI where users select objects from left panel and edit their painters in right panel
- Different object types need different configuration options (e.g., tracks need line width, hits need size)
- Configuration UI must be extensible for third-party painters
- Property edits should immediately update visualizations
Proposed Architecture
We'll implement a hybrid approach using:
- Component-based UI - Dynamic component loading for different painter types
- Decorator-based metadata - Use TypeScript decorators to define UI controls
Data Model Structure
// Base configuration interface
interface PainterConfig {
// Common properties for all painters
visible: boolean;
// Method to identify config type
getConfigType(): string;
}
// Example specific configurations
class TrackPainterConfig implements PainterConfig {
visible: boolean = true;
coloringMode: 'PID' | 'Momentum' | 'Color' = 'PID';
color: string = '#FF0000';
lineWidth: number = 2;
showSteps: boolean = false;
getConfigType(): string { return 'track'; }
}
class HitPainterConfig implements PainterConfig {
visible: boolean = true;
size: number = 5;
getConfigType(): string { return 'hit'; }
}Decorator System
Create decorators to describe UI controls:
// Decorator factory functions
function ConfigProperty(options: PropertyOptions = {}): PropertyDecorator {
return (target: Object, propertyKey: string | symbol) => {
Reflect.defineMetadata('configProperty', options, target, propertyKey);
};
}
function SelectField(options: SelectOptions): PropertyDecorator {
return ConfigProperty({ ...options, type: 'select' });
}
function NumberField(options: NumberOptions): PropertyDecorator {
return ConfigProperty({ ...options, type: 'number' });
}
function ColorField(options: ColorOptions = {}): PropertyDecorator {
return ConfigProperty({ ...options, type: 'color' });
}
function BooleanField(options: BooleanOptions = {}): PropertyDecorator {
return ConfigProperty({ ...options, type: 'boolean' });
}Apply decorators to config classes:
class TrackPainterConfig implements PainterConfig {
@BooleanField({ label: 'Visible' })
visible: boolean = true;
@SelectField({
label: 'Coloring',
options: [
{ value: 'PID', label: 'By Particle ID' },
{ value: 'Momentum', label: 'By Momentum' },
{ value: 'Color', label: 'Single Color' }
]
})
coloringMode: 'PID' | 'Momentum' | 'Color' = 'PID';
@ColorField({
label: 'Color',
showWhen: (config) => config.coloringMode === 'Color'
})
color: string = '#FF0000';
@NumberField({
label: 'Line Width',
min: 1,
max: 10,
step: 0.5
})
lineWidth: number = 2;
@BooleanField({ label: 'Show Steps' })
showSteps: boolean = false;
getConfigType(): string { return 'track'; }
}Component System
- Create a registry service to map config types to components:
@Injectable({ providedIn: 'root' })
export class ConfiguratorRegistryService {
private registry = new Map<string, Type<ConfiguratorComponent<any>>>();
register<T extends PainterConfig>(
configType: string,
component: Type<ConfiguratorComponent<T>>
): void {
this.registry.set(configType, component);
}
getComponent<T extends PainterConfig>(configType: string): Type<ConfiguratorComponent<T>> | undefined {
return this.registry.get(configType);
}
}- Create a base configurator component:
@Directive()
export abstract class ConfiguratorComponent<T extends PainterConfig> implements OnInit {
@Input() config!: T;
@Output() configChanged = new EventEmitter<T>();
// Utility method to read metadata from config class
protected getPropertyMetadata(propertyName: keyof T): PropertyOptions | undefined {
return Reflect.getMetadata('configProperty', this.config, propertyName as string);
}
// Utility method to check conditional display
protected shouldShowProperty(propertyName: keyof T): boolean {
const metadata = this.getPropertyMetadata(propertyName);
if (!metadata || !metadata.showWhen) return true;
return metadata.showWhen(this.config);
}
// Notify that config has changed
protected notifyChanges(): void {
this.configChanged.emit(this.config);
}
}- Create specific configurator components:
@Component({
selector: 'app-track-configurator',
template: `
<mat-form-field>
<mat-label>Coloring</mat-label>
<mat-select [(ngModel)]="config.coloringMode" (ngModelChange)="notifyChanges()">
<mat-option *ngFor="let opt of coloringOptions" [value]="opt.value">
{{opt.label}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="config.coloringMode === 'Color'">
<mat-label>Color</mat-label>
<input matInput type="color" [(ngModel)]="config.color" (ngModelChange)="notifyChanges()">
</mat-form-field>
<mat-form-field>
<mat-label>Line Width</mat-label>
<input matInput type="number" [(ngModel)]="config.lineWidth"
min="1" max="10" step="0.5" (ngModelChange)="notifyChanges()">
</mat-form-field>
<mat-checkbox [(ngModel)]="config.showSteps" (ngModelChange)="notifyChanges()">
Show Steps
</mat-checkbox>
`,
standalone: true,
imports: [
// Add necessary imports
]
})
export class TrackConfiguratorComponent extends ConfiguratorComponent<TrackPainterConfig> {
coloringOptions = [
{ value: 'PID', label: 'By Particle ID' },
{ value: 'Momentum', label: 'By Momentum' },
{ value: 'Color', label: 'Single Color' }
];
}Generic Decorator-Based Configurator
We can also create a generic component that builds UI from decorators:
@Component({
selector: 'app-generic-configurator',
template: `
<div *ngFor="let prop of configProperties">
<ng-container [ngSwitch]="getPropertyType(prop)">
<!-- Select fields -->
<mat-form-field *ngSwitchCase="'select'" [style.display]="shouldShowProperty(prop) ? 'block' : 'none'">
<mat-label>{{getPropertyLabel(prop)}}</mat-label>
<mat-select [(ngModel)]="config[prop]" (ngModelChange)="notifyChanges()">
<mat-option *ngFor="let opt of getPropertyOptions(prop)" [value]="opt.value">
{{opt.label}}
</mat-option>
</mat-select>
</mat-form-field>
<!-- Other field types... -->
</ng-container>
</div>
`,
standalone: true,
imports: [
// Add necessary imports
]
})
export class GenericConfiguratorComponent<T extends PainterConfig> extends ConfiguratorComponent<T> {
configProperties: (keyof T)[] = [];
ngOnInit() {
// Get all properties with metadata
this.configProperties = Object.getOwnPropertyNames(this.config)
.filter(prop => Reflect.hasMetadata('configProperty', this.config, prop)) as (keyof T)[];
}
getPropertyType(prop: keyof T): string {
return this.getPropertyMetadata(prop)?.type || 'text';
}
getPropertyLabel(prop: keyof T): string {
return this.getPropertyMetadata(prop)?.label || String(prop);
}
getPropertyOptions(prop: keyof T): any[] {
return this.getPropertyMetadata(prop)?.options || [];
}
}Main Configuration Page
Create a page that uses the shell component:
@Component({
selector: 'app-painter-config-page',
template: `
<app-shell>
<div leftPane>
<mat-list>
<mat-list-item *ngFor="let item of configItems"
[class.selected]="selectedItem === item"
(click)="selectItem(item)">
{{item.name}}
</mat-list-item>
</mat-list>
</div>
<div rightPane>
<ng-container #configuratorContainer></ng-container>
</div>
</app-shell>
`,
standalone: true,
imports: [
// Add necessary imports
]
})
export class PainterConfigPageComponent implements OnInit, AfterViewInit {
configItems: {name: string, type: string, config: PainterConfig}[] = [
{ name: 'TracksA', type: 'track', config: new TrackPainterConfig() },
{ name: 'TracksB', type: 'track', config: new TrackPainterConfig() },
{ name: 'Hits', type: 'hit', config: new HitPainterConfig() },
{ name: 'Jets', type: 'jet', config: new JetPainterConfig() }
];
selectedItem: typeof this.configItems[0] | null = null;
@ViewChild('configuratorContainer', { read: ViewContainerRef })
configuratorContainer!: ViewContainerRef;
constructor(
private registry: ConfiguratorRegistryService,
@Inject(DOCUMENT) private document: Document
) {}
ngOnInit() {
// Initialize items
}
ngAfterViewInit() {
// Select first item by default
if (this.configItems.length > 0) {
this.selectItem(this.configItems[0]);
}
}
selectItem(item: typeof this.configItems[0]) {
this.selectedItem = item;
this.loadConfiguratorComponent(item);
}
loadConfiguratorComponent(item: typeof this.configItems[0]) {
// Clear previous component
this.configuratorContainer.clear();
// Get registered component for this config type
const componentType = this.registry.getComponent(item.type);
if (componentType) {
// Create the component
const componentRef = this.configuratorContainer.createComponent(componentType);
// Set input properties
componentRef.instance.config = item.config;
// Listen for changes
componentRef.instance.configChanged.subscribe((updatedConfig: PainterConfig) => {
console.log('Config updated:', updatedConfig);
// Update your painters here
});
}
}
}Registration in App Initialization
Register components during app initialization:
@NgModule({
// ...
})
export class AppModule {
constructor(registry: ConfiguratorRegistryService) {
// Register configurator components
registry.register('track', TrackConfiguratorComponent);
registry.register('hit', HitConfiguratorComponent);
registry.register('jet', JetConfiguratorComponent);
}
}Extension Pattern for Third-Party Developers
Third-party developers would:
- Create their painter config class with decorators:
class CustomPainterConfig implements PainterConfig {
@ColorField({ label: 'Custom Color' })
color: string = '#00FF00';
// Other properties with decorators
getConfigType(): string { return 'custom'; }
}-
Create a configurator component (or use the generic one)
-
Register their component:
// In their module
constructor(registry: ConfiguratorRegistryService) {
registry.register('custom', CustomConfiguratorComponent);
}Implementation Steps
- Create the decorator system (metadata, utility functions)
- Implement the configurator registry service
- Create base configurator component class
- Implement specific configurator components
- Create the generic decorator-based configurator
- Build the main configuration page using Shell component
- Add registration code for built-in types
- Document extension pattern for third-parties
This approach provides a flexible, extensible system for painter configuration with a mix of static typing and runtime metadata. The decorator system reduces boilerplate while allowing specialized components when needed.