diff --git a/README.md b/README.md index 9ce51c2..157afd1 100644 --- a/README.md +++ b/README.md @@ -100,3 +100,117 @@ Hit has - "t": time information [ns] (time, err_time), - "ed": energy deposit with error [GeV] (edep, err_edep) +## TypeScript Event Model + + +The Firebird Data Exchange model provides a structured way to serialize and deserialize +event data. This model is implemented in TypeScript and designed to be extensible, +allowing users to add their own custom components without modifying the core parsing logic. + +The implementation consists of several TypeScript classes and interfaces that mirror the data exchange +format and provide methods for serialization and deserialization. + +- **DataExchange** class - Represents the entire data exchange object. +- **Entry** class - Represents an individual event entry. +- **EntryComponent** abstract class - A base class representing a generic entry component. Described below. + +It Typescript Firebird Data Exchange often referenced as Dex (Data EXchange). E.g. +`toDexObject`, `fromDexObject`. `DexObject` is a JS object, that if serialized to JSON +would correspond a part of DataExchangeFormat. E.g. Dex HitBox will have `pos`, `dim` etc. + +We can put it differently. In general JSON format is very close to object definition in JS => TS. +But despite that, Firebird JSON format is just a data exchange layer and when deserialized from +JSON is not designed to be a 100% a convenient to work with JS data structure. +More over, it lacks some methods and abstractions, that our domain data-model should have, +such as links between event model and three.js rendering tree objects. + +Summarizing: + +- Firebird Data Exchange - is JSON schema shaping data exchange between backend and frontend +- DexObject - JSON parsed to JS object +- TypeScript event model - Frontend set of classes mimicking DexObject structure but designed + to be convenient in terms of the frontend API + + +### EntryComponent machinery + +Different collection of objects such as hits, tracks, vertexes, etc. +that firebird can work with are represented as various event or Entry Component-s. +Each type derive from EntryComponent and registered in a system. +Then corresponding Painters classes know how to render/paint them, there are rules how +to display them in tables, etc. + +- **EntryComponent** An abstract class representing a generic entry component. +- Contains common properties: + - `name`: Unique identifier. + - `type`: Component type (used for determining the appropriate factory). + - `originType`: Optional string indicating the origin type. +- Declares an abstract method `toDexObject()` that must be implemented by subclasses. + +- **EntryComponentFactory Interface**: Defines a factory for creating `EntryComponent` + instances from Dex objects - deserialized data. +- **Component Registry**: A mapping of component types to their corresponding factories. +- Functions: + - `registerComponentFactory(factory: EntryComponentFactory)`: Registers a new factory. + - `getComponentFactory(type: string)`: Retrieves a factory based on the component type. + + +## Extending the Model + +### Adding a New Component Type + +To add a new component type, follow these steps: + +1. **Create a New Component Class**: Extend `EntryComponent` and implement the `toDexObject()` method. + + ```typescript + export class CustomComponent extends EntryComponent { + static type = 'CustomType'; + // Define additional properties + + constructor(name: string, /* additional parameters */, originType?: string) { + super(name, CustomComponent.type, originType); + // Initialize additional properties + } + + toDexObject(): any { + return { + name: this.name, + type: this.type, + originType: this.originType, + // Serialize additional properties + }; + } + } + ``` + +2. **Create a Factory for the Component**: Implement `EntryComponentFactory` for your component. + + ```typescript + export class CustomComponentFactory implements EntryComponentFactory { + type: string = CustomComponent.type; + + fromDexObject(obj: any): EntryComponent { + const name = obj["name"]; + // Extract additional properties + const originType = obj["originType"]; + + // Validate required fields + if (!name /* || missing other required fields */) { + throw new Error("Missing required fields in CustomComponent"); + } + + return new CustomComponent(name, /* additional parameters */, originType); + } + } + ``` + +3. **Register the Factory**: Use the `registerComponentFactory()` function to register your component factory. + + ```typescript + // Register the custom component factory + registerComponentFactory(new CustomComponentFactory()); + ``` + +4. **Update JSON Parsing Logic**: No need to modify existing parsing logic. The registry will dynamically resolve the new component type. + diff --git a/firebird-ng/src/app/model/box-tracker-hit.component.factory.spec.ts b/firebird-ng/src/app/model/box-tracker-hit.component.factory.spec.ts new file mode 100644 index 0000000..bc5c8fa --- /dev/null +++ b/firebird-ng/src/app/model/box-tracker-hit.component.factory.spec.ts @@ -0,0 +1,54 @@ +// box-tracker-hit.component.factory.spec.ts + +import { BoxTrackerHitComponentFactory, BoxTrackerHitComponent } from './box-tracker-hit.component'; +import { EntryComponent } from './entry-component'; + +describe('BoxTrackerHitComponentFactory', () => { + const factory = new BoxTrackerHitComponentFactory(); + + it('should have the correct type', () => { + expect(factory.type).toBe('BoxTrackerHit'); + }); + + it('should create a BoxTrackerHitComponent from DexObject', () => { + const dexObject = { + name: 'TestComponent', + type: 'BoxTrackerHit', + originType: 'TestOriginType', + hits: [ + { + pos: [1, 2, 3], + dim: [10, 10, 1], + t: [4, 1], + ed: [0.001, 0.0001], + }, + { + pos: [4, 5, 6], + dim: [10, 10, 2], + t: [5, 1], + ed: [0.002, 0.0002], + }, + ], + }; + + const component = factory.fromDexObject(dexObject) as BoxTrackerHitComponent; + + expect(component).toBeInstanceOf(BoxTrackerHitComponent); + expect(component.name).toBe('TestComponent'); + expect(component.type).toBe('BoxTrackerHit'); + expect(component.originType).toBe('TestOriginType'); + expect(component.hits.length).toBe(2); + + const hit1 = component.hits[0]; + expect(hit1.position).toEqual([1, 2, 3]); + expect(hit1.dimensions).toEqual([10, 10, 1]); + expect(hit1.time).toEqual([4, 1]); + expect(hit1.energyDeposit).toEqual([0.001, 0.0001]); + + const hit2 = component.hits[1]; + expect(hit2.position).toEqual([4, 5, 6]); + expect(hit2.dimensions).toEqual([10, 10, 2]); + expect(hit2.time).toEqual([5, 1]); + expect(hit2.energyDeposit).toEqual([0.002, 0.0002]); + }); +}); diff --git a/firebird-ng/src/app/model/box-tracker-hit.component.spec.ts b/firebird-ng/src/app/model/box-tracker-hit.component.spec.ts new file mode 100644 index 0000000..78e3582 --- /dev/null +++ b/firebird-ng/src/app/model/box-tracker-hit.component.spec.ts @@ -0,0 +1,30 @@ +// box-tracker-hit.component.spec.ts + +import { BoxTrackerHitComponent, BoxTrackerHit } from './box-tracker-hit.component'; + +describe('BoxTrackerHitComponent', () => { + it('should create an instance with given name', () => { + const component = new BoxTrackerHitComponent('TestComponent'); + + expect(component.name).toBe('TestComponent'); + expect(component.type).toBe(BoxTrackerHitComponent.type); + expect(component.hits.length).toBe(0); + }); + + it('should serialize to DexObject correctly', () => { + const hit1 = new BoxTrackerHit([1, 2, 3], [10, 10, 1], [4, 1], [0.001, 0.0001]); + const hit2 = new BoxTrackerHit([4, 5, 6], [10, 10, 2], [5, 1], [0.002, 0.0002]); + + const component = new BoxTrackerHitComponent('TestComponent', 'TestOriginType'); + component.hits.push(hit1, hit2); + + const dexObject = component.toDexObject(); + + expect(dexObject).toEqual({ + name: 'TestComponent', + type: 'BoxTrackerHit', + originType: 'TestOriginType', + hits: [hit1.toDexObject(), hit2.toDexObject()], + }); + }); +}); diff --git a/firebird-ng/src/app/model/box-tracker-hit.component.ts b/firebird-ng/src/app/model/box-tracker-hit.component.ts new file mode 100644 index 0000000..26e4691 --- /dev/null +++ b/firebird-ng/src/app/model/box-tracker-hit.component.ts @@ -0,0 +1,141 @@ +// box-tracker-hit.component.ts + +import { EntryComponent, EntryComponentFactory, registerComponentFactory } from './entry-component'; + +/** + * Represents an individual tracker hit with position, dimensions, time, and energy deposit. + */ +export class BoxTrackerHit { + + /** The position of the hit [mm] as [x, y, z]. */ + position: [number, number, number]; + + /** The dimensions of the hit box [mm] as [dx, dy, dz]. */ + dimensions: [number, number, number]; + + /** The time information [ns] as [time, err_time]. */ + time: [number, number]; + + /** The energy deposit with error [GeV] as [edep, err_edep]. */ + energyDeposit: [number, number]; + + /** + * Constructs a new BoxTrackerHit instance. + * + * @param position - The position of the hit as [x, y, z]. + * @param dimensions - The dimensions of the hit box as [dx, dy, dz]. + * @param time - The time information as [time, err_time]. + * @param energyDeposit - The energy deposit with error as [edep, err_edep]. + */ + constructor( + position: [number, number, number], + dimensions: [number, number, number], + time: [number, number], + energyDeposit: [number, number] + ) { + this.position = position; + this.dimensions = dimensions; + this.time = time; + this.energyDeposit = energyDeposit; + } + + /** + * Serializes the BoxTrackerHit instance into a JSON-compatible object. + * + * @returns A JSON-compatible object representing the serialized BoxTrackerHit. + */ + toDexObject(): any { + return { + pos: this.position, + dim: this.dimensions, + t: this.time, + ed: this.energyDeposit, + }; + } + + /** + * Creates a BoxTrackerHit instance from a deserialized object. + * + * @param obj - The deserialized object representing a BoxTrackerHit. + * @returns A new instance of BoxTrackerHit populated with data from the object. + */ + static fromDexObject(obj: any): BoxTrackerHit { + return new BoxTrackerHit( + obj["pos"], + obj["dim"], + obj["t"], + obj["ed"] + ); + } +} + +/** + * Represents a component that contains multiple BoxTrackerHits. + */ +export class BoxTrackerHitComponent extends EntryComponent { + /** The static type identifier for the BoxTrackerHitComponent. */ + static type = 'BoxTrackerHit'; + + /** An array of BoxTrackerHits contained in this component. */ + hits: BoxTrackerHit[] = []; + + /** + * Constructs a new BoxTrackerHitComponent instance. + * + * @param name - The name of the component. + * @param originType - Optional origin type of the component. + */ + constructor(name: string, originType?: string) { + super(name, BoxTrackerHitComponent.type, originType); + } + + /** + * Serializes the BoxTrackerHitComponent instance into a JSON-compatible object. + * + * @returns A JSON-compatible object representing the serialized BoxTrackerHitComponent. + */ + toDexObject(): any { + const objHits = []; + for (const hit of this.hits) { + objHits.push(hit.toDexObject()); + } + + return { + name: this.name, + type: this.type, + originType: this.originType, + hits: objHits, + }; + } +} + +/** + * Factory for creating instances of BoxTrackerHitComponent from deserialized data. + */ +export class BoxTrackerHitComponentFactory implements EntryComponentFactory { + /** The type of the component that this factory creates. */ + type: string = BoxTrackerHitComponent.type; + + /** + * Creates an instance of BoxTrackerHitComponent from a deserialized object. + * + * @param obj - The deserialized object representing a BoxTrackerHitComponent. + * @returns An instance of BoxTrackerHitComponent. + */ + fromDexObject(obj: any): EntryComponent { + const result = new BoxTrackerHitComponent(obj["name"]); + + if (obj["originType"]) { + result.originType = obj["originType"]; + } + + for (const objHit of obj["hits"]) { + result.hits.push(BoxTrackerHit.fromDexObject(objHit)); + } + + return result; + } +} + +// Register the component factory +registerComponentFactory(new BoxTrackerHitComponentFactory()); diff --git a/firebird-ng/src/app/model/component-registry.spec.ts b/firebird-ng/src/app/model/component-registry.spec.ts new file mode 100644 index 0000000..cab71da --- /dev/null +++ b/firebird-ng/src/app/model/component-registry.spec.ts @@ -0,0 +1,22 @@ +// component-registry.spec.ts + +import { registerComponentFactory, getComponentFactory } from './entry-component'; +import { BoxTrackerHitComponentFactory } from './box-tracker-hit.component'; + +describe('Component Registry', () => { + it('should register and retrieve BoxTrackerHitComponentFactory correctly', () => { + const factory = new BoxTrackerHitComponentFactory(); + registerComponentFactory(factory); + + const retrievedFactory = getComponentFactory('BoxTrackerHit'); + + expect(retrievedFactory).toBeDefined(); + expect(retrievedFactory).toBe(factory); + }); + + it('should return undefined for unregistered component types', () => { + const retrievedFactory = getComponentFactory('UnknownType'); + + expect(retrievedFactory).toBeUndefined(); + }); +}); diff --git a/firebird-ng/src/app/model/data-exchange.spec.ts b/firebird-ng/src/app/model/data-exchange.spec.ts new file mode 100644 index 0000000..529a739 --- /dev/null +++ b/firebird-ng/src/app/model/data-exchange.spec.ts @@ -0,0 +1,64 @@ +// data-exchange.spec.ts + +import { DataExchange } from './data-exchange'; +import { Entry } from './entry'; +import { BoxTrackerHitComponent, BoxTrackerHit, BoxTrackerHitComponentFactory } from './box-tracker-hit.component'; +import { registerComponentFactory } from './entry-component'; + +// Register the BoxTrackerHitComponentFactory +registerComponentFactory(new BoxTrackerHitComponentFactory()); + +describe('DataExchange with BoxTrackerHitComponent', () => { + it('should serialize and deserialize correctly', () => { + // Create hits + const hit1 = new BoxTrackerHit([1, 2, 3], [10, 10, 1], [4, 1], [0.001, 0.0001]); + const hit2 = new BoxTrackerHit([4, 5, 6], [10, 10, 2], [5, 1], [0.002, 0.0002]); + + // Create component + const component = new BoxTrackerHitComponent('TestComponent', 'TestOriginType'); + component.hits.push(hit1, hit2); + + // Create entry + const entry = new Entry(); + entry.id = 'event1'; + entry.components.push(component); + + // Create DataExchange + const dataExchange = new DataExchange(); + dataExchange.version = '0.01'; + dataExchange.origin = { fileName: 'sample.dat' }; + dataExchange.entries.push(entry); + + // Serialize + const serialized = dataExchange.toDexObject(); + + // Deserialize + const deserialized = DataExchange.fromDexObj(serialized); + + // Assertions + expect(deserialized.version).toBe(dataExchange.version); + expect(deserialized.origin).toEqual(dataExchange.origin); + expect(deserialized.entries.length).toBe(1); + + const deserializedEntry = deserialized.entries[0]; + expect(deserializedEntry.id).toBe(entry.id); + expect(deserializedEntry.components.length).toBe(1); + + const deserializedComponent = deserializedEntry.components[0] as BoxTrackerHitComponent; + expect(deserializedComponent.name).toBe(component.name); + expect(deserializedComponent.type).toBe(component.type); + expect(deserializedComponent.originType).toBe(component.originType); + expect(deserializedComponent.hits.length).toBe(2); + + // Check hits + for (let i = 0; i < deserializedComponent.hits.length; i++) { + const originalHit = component.hits[i]; + const deserializedHit = deserializedComponent.hits[i]; + + expect(deserializedHit.position).toEqual(originalHit.position); + expect(deserializedHit.dimensions).toEqual(originalHit.dimensions); + expect(deserializedHit.time).toEqual(originalHit.time); + expect(deserializedHit.energyDeposit).toEqual(originalHit.energyDeposit); + } + }); +}); diff --git a/firebird-ng/src/app/model/data-exchange.ts b/firebird-ng/src/app/model/data-exchange.ts index ea8c673..b0bac1b 100644 --- a/firebird-ng/src/app/model/data-exchange.ts +++ b/firebird-ng/src/app/model/data-exchange.ts @@ -8,10 +8,10 @@ export class DataExchange { entries: Entry[] = [] - toObj() { + toDexObject() { let objEntries:any[] = []; for(const entry of this.entries) { - objEntries.push(entry.toObj()); + objEntries.push(entry.toDexObject()); } return { version: this.version, @@ -20,12 +20,12 @@ export class DataExchange { } } - static fromFirebirdEventObj(obj: any): DataExchange { + static fromDexObj(obj: any): DataExchange { let result = new DataExchange(); result.version = obj["version"]; result.origin = obj["origin"]; for(const objEntry of obj["entries"]) { - result.entries.push(Entry.fromFirebirdEventObj(objEntry)); + result.entries.push(Entry.fromDexObject(objEntry)); } return result; } diff --git a/firebird-ng/src/app/model/entry-component.spec.ts b/firebird-ng/src/app/model/entry-component.spec.ts new file mode 100644 index 0000000..861b433 --- /dev/null +++ b/firebird-ng/src/app/model/entry-component.spec.ts @@ -0,0 +1,113 @@ +// entry-component.spec.ts + +import { + EntryComponent, + EntryComponentFactory, + registerComponentFactory, + getComponentFactory, + _resetComponentRegistry, +} from './entry-component'; + +describe('EntryComponent', () => { + it('should not allow instantiation of abstract class', () => { + // Attempting to instantiate an abstract class should result in a compile-time error. + // This test ensures that the class is abstract by design. + + // Uncommenting the following line should cause a TypeScript error. + // const component = new EntryComponent('name', 'type'); + + // Since TypeScript prevents instantiation of abstract classes at compile time, + // we can simulate this in the test by checking that an error is thrown. + + expect(() => { + // Force a runtime check by attempting to instantiate via casting. + (EntryComponent as any).call(null, 'name', 'type'); + }).toThrowError(); + }); +}); + +describe('Component Registry', () => { + // Define a TestComponentFactory for testing + class TestComponentFactory implements EntryComponentFactory { + type: string = 'TestType'; + + fromDexObject(obj: any): EntryComponent { + return new TestComponent(obj['name'], obj['originType']); + } + } + + // Define TestComponent class extending EntryComponent + class TestComponent extends EntryComponent { + constructor(name: string, originType?: string) { + super(name, 'TestType', originType); + } + + toDexObject(): any { + return { + name: this.name, + type: this.type, + originType: this.originType, + }; + } + } + + beforeEach(() => { + // Reset the component registry before each test + _resetComponentRegistry(); + }); + + it('should register and retrieve component factories correctly', () => { + const factory = new TestComponentFactory(); + + // Register the factory + registerComponentFactory(factory); + + // Retrieve the factory + const retrievedFactory = getComponentFactory('TestType'); + + expect(retrievedFactory).toBeDefined(); + expect(retrievedFactory).toBe(factory); + }); + + it('should return undefined for unregistered component types', () => { + const retrievedFactory = getComponentFactory('UnknownType'); + + expect(retrievedFactory).toBeUndefined(); + }); + + it('should overwrite existing factory when registering a factory with the same type', () => { + const factory1 = new TestComponentFactory(); + const factory2 = new TestComponentFactory(); + + // Register the first factory + registerComponentFactory(factory1); + + // Register the second factory with the same type + registerComponentFactory(factory2); + + // Retrieve the factory + const retrievedFactory = getComponentFactory('TestType'); + + expect(retrievedFactory).toBe(factory2); + }); + + it('should use the correct factory to create component instances', () => { + const factory = new TestComponentFactory(); + registerComponentFactory(factory); + + const dexObject = { + name: 'TestComponent', + type: 'TestType', + originType: 'TestOrigin', + }; + + const retrievedFactory = getComponentFactory('TestType'); + expect(retrievedFactory).toBeDefined(); + + const component = retrievedFactory!.fromDexObject(dexObject); + expect(component).toBeInstanceOf(EntryComponent); + expect(component.name).toBe('TestComponent'); + expect(component.type).toBe('TestType'); + expect(component.originType).toBe('TestOrigin'); + }); +}); diff --git a/firebird-ng/src/app/model/entry-component.ts b/firebird-ng/src/app/model/entry-component.ts index e520b18..663fb4d 100644 --- a/firebird-ng/src/app/model/entry-component.ts +++ b/firebird-ng/src/app/model/entry-component.ts @@ -1,16 +1,108 @@ +/** + * The EntryComponent class is an abstract base class for all entry components. + */ export abstract class EntryComponent { + /** + * The name of the component, intended to be a unique identifier in UI menus and such. + */ name: string; + + /** + * The type of the component, used to identify the component class + * and facilitate deserialization and factory lookup. + */ type: string; + + /** + * An optional string indicating the origin type of the component, + * such as the original EDM4EIC/EDM4HEP/C++ data type from which it was derived. + */ originType?: string; - constructor(name: string, type: string, originType?: string) { + /** + * The constructor is protected to prevent direct instantiation of the abstract class. + * Only derived classes can call this constructor when they implement their own constructors. + * + * @param name - The name of the component. + * @param type - The type of the component. + * @param originType - Optional origin type of the component. + */ + protected constructor(name: string, type: string, originType?: string) { this.name = name; this.type = type; this.originType = originType; } - // Instance method to serialize the object - abstract toSpecialObject(): any; + /** + * Abstract method that must be implemented by derived classes. + * This method should serialize the component instance into a JSON-compatible object + * following the Data Exchange format (DexObject). + * + * @returns A JSON-compatible object representing the serialized component. + */ + abstract toDexObject(): any; +} + +/** + * The EntryComponentFactory interface defines the structure for factory classes + * that are responsible for creating instances of EntryComponent subclasses. + */ +export interface EntryComponentFactory { + + /** + * The type of the component that this factory creates. + * This should match the `type` property of the components it creates. + */ + type: string; + + /** + * Method to create an instance of an EntryComponent subclass from a deserialized object. + * The method takes a generic object (typically parsed from JSON) and returns an instance + * of the corresponding EntryComponent subclass. + * + * @param obj - The deserialized object from which to create the component. + * @returns An instance of an EntryComponent subclass. + */ + fromDexObject(obj: any): EntryComponent; +} + +/** + * The componentRegistry is a mapping from component type strings to their corresponding factories. + * It is used to look up the appropriate factory when deserializing components from JSON data. + * This registry enables the system to support multiple component types dynamically. + */ +const componentRegistry: { [type: string]: EntryComponentFactory } = {}; + +/** + * Registers a new component factory in the registry. + * This allows the factory to be used during deserialization to create instances + * of the component it represents. + * + * @param factory - The factory to register. + */ +export function registerComponentFactory(factory: EntryComponentFactory): void { + componentRegistry[factory.type] = factory; +} +/** + * Retrieves a component factory from the registry based on the component type. + * + * @param type - The type of the component. + * @returns The corresponding EntryComponentFactory, or undefined if not found. + */ +export function getComponentFactory(type: string): EntryComponentFactory | undefined { + return componentRegistry[type]; +} + +/** + * Resets the component registry. + * This function is intended for internal use during testing. + * + * @internal + */ +export function _resetComponentRegistry(): void { + for (const key in componentRegistry) { + delete componentRegistry[key]; + } } diff --git a/firebird-ng/src/app/model/entry.ts b/firebird-ng/src/app/model/entry.ts index bf1e480..aa0ef38 100644 --- a/firebird-ng/src/app/model/entry.ts +++ b/firebird-ng/src/app/model/entry.ts @@ -1,13 +1,40 @@ +import {EntryComponent, EntryComponentFactory, getComponentFactory} from "./entry-component"; + export class Entry { id: string = ""; - components: Component = []; + components: EntryComponent[] = []; - toObj() { - return {} + toDexObject(): any { + const objComponents: any[] = []; + for (const component of this.components) { + objComponents.push(component.toDexObject()); + } + return { + id: this.id, + components: objComponents, + }; } - static fromFirebirdEventObj(obj: any): void { - throw new Error("Method not implemented."); + static fromDexObject(obj: any): Entry { + let result = new Entry(); + result.id = obj["id"]; + for(const objComponent of obj["components"]) { + const compType = obj["type"]; + + if(!compType) { + console.warn(`A problem with entry component type (a required field). It is: '${compType}'`); + continue; + } + + const factory = getComponentFactory(compType); + if(factory === null || factory === undefined ) { + console.warn(`Can't find EntryComponent factory for type name: '${compType}'`) + } + else { + result.components.push(factory.fromDexObject(compType)); + } + } + return result; } } diff --git a/firebird-ng/src/app/model/event-component.ts b/firebird-ng/src/app/model/event-component.ts deleted file mode 100644 index e69de29..0000000 diff --git a/firebird-ng/src/app/model/object-serializable.interface.ts b/firebird-ng/src/app/model/object-serializable.interface.ts deleted file mode 100644 index c69ffda..0000000 --- a/firebird-ng/src/app/model/object-serializable.interface.ts +++ /dev/null @@ -1,14 +0,0 @@ - -export interface ToFirebirdObject { - /** Serialize to Firebird event object */ - toObj():any; -} - -export interface - -export function from( - ctor: IObjectSerializable, - data: any -): ClockInterface { - return new ctor(hour, minute); -} diff --git a/firebird-ng/src/app/model/tracker-hit-box.ts b/firebird-ng/src/app/model/tracker-hit-box.ts deleted file mode 100644 index e69de29..0000000