Skip to content

Side panel objects configuration #46

@DraTeots

Description

@DraTeots

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:

  1. Component-based UI - Dynamic component loading for different painter types
  2. 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

  1. 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);
  }
}
  1. 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);
  }
}
  1. 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:

  1. 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'; }
}
  1. Create a configurator component (or use the generic one)

  2. Register their component:

// In their module
constructor(registry: ConfiguratorRegistryService) {
  registry.register('custom', CustomConfiguratorComponent);
}

Implementation Steps

  1. Create the decorator system (metadata, utility functions)
  2. Implement the configurator registry service
  3. Create base configurator component class
  4. Implement specific configurator components
  5. Create the generic decorator-based configurator
  6. Build the main configuration page using Shell component
  7. Add registration code for built-in types
  8. 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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions