Side panel objects configuration
- 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.