eic / eic/firebird

Side panel objects configuration

Open
#46 0 comments 0 reactions 0 assignees View on GitHub
[engine] [UI]
Dominant language
TypeScript
Stars
1
Forks
4
PR merge metrics
No merged PRs in 30d

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:
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

```typescript
// 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:

```typescript
// 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:

```typescript
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:

```typescript
@Injectable({ providedIn: 'root' })
export class ConfiguratorRegistryService {
private registry = new Map>>();

register(
configType: string,
component: Type>
): void {
this.registry.set(configType, component);
}

getComponent(configType: string): Type> | undefined {
return this.registry.get(configType);
}
}
```

2. Create a base configurator component:

```typescript
@Directive()
export abstract class ConfiguratorComponent implements OnInit {
@Input() config!: T;
@Output() configChanged = new EventEmitter();

// 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);
}
}
```

3. Create specific configurator components:

```typescript
@Component({
selector: 'app-track-configurator',
template: `

Coloring


{{opt.label}}





Color




Line Width




Show Steps

`,
standalone: true,
imports: [
// Add necessary imports
]
})
export class TrackConfiguratorComponent extends ConfiguratorComponent {
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:

```typescript
@Component({
selector: 'app-generic-configurator',
template: `





{{getPropertyLabel(prop)}}


{{opt.label}}







`,
standalone: true,
imports: [
// Add necessary imports
]
})
export class GenericConfiguratorComponent extends ConfiguratorComponent {
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:

```typescript
@Component({
selector: 'app-painter-config-page',
template: `




{{item.name}}








`,
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:

```typescript
@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:
```typescript
class CustomPainterConfig implements PainterConfig {
@ColorField({ label: 'Custom Color' })
color: string = '#00FF00';

// Other properties with decorators

getConfigType(): string { return 'custom'; }
}
```

2. Create a configurator component (or use the generic one)

3. Register their component:
```typescript
// 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.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.