/** * Value Object: LiveryDecal * Represents a decal/logo placed on a livery */ export type DecalType = 'sponsor' | 'user'; export interface LiveryDecalProps { id: string; imageUrl: string; x: number; y: number; width: number; height: number; rotation: number; // Degrees, 0-360 zIndex: number; type: DecalType; } export class LiveryDecal { readonly id: string; readonly imageUrl: string; readonly x: number; readonly y: number; readonly width: number; readonly height: number; readonly rotation: number; readonly zIndex: number; readonly type: DecalType; private constructor(props: LiveryDecalProps) { this.id = props.id; this.imageUrl = props.imageUrl; this.x = props.x; this.y = props.y; this.width = props.width; this.height = props.height; this.rotation = props.rotation; this.zIndex = props.zIndex; this.type = props.type; } static create(props: Omit & { rotation?: number }): LiveryDecal { const propsWithRotation = { ...props, rotation: props.rotation ?? 0, }; this.validate(propsWithRotation); return new LiveryDecal(propsWithRotation); } private static validate(props: LiveryDecalProps): void { if (!props.id || props.id.trim().length === 0) { throw new RacingDomainValidationError('LiveryDecal ID is required'); } if (!props.imageUrl || props.imageUrl.trim().length === 0) { throw new RacingDomainValidationError('LiveryDecal imageUrl is required'); } if (props.x < 0 || props.x > 1) { throw new RacingDomainValidationError('LiveryDecal x coordinate must be between 0 and 1 (normalized)'); } if (props.y < 0 || props.y > 1) { throw new RacingDomainValidationError('LiveryDecal y coordinate must be between 0 and 1 (normalized)'); } if (props.width <= 0 || props.width > 1) { throw new RacingDomainValidationError('LiveryDecal width must be between 0 and 1 (normalized)'); } if (props.height <= 0 || props.height > 1) { throw new RacingDomainValidationError('LiveryDecal height must be between 0 and 1 (normalized)'); } if (!Number.isInteger(props.zIndex) || props.zIndex < 0) { throw new RacingDomainValidationError('LiveryDecal zIndex must be a non-negative integer'); } if (props.rotation < 0 || props.rotation > 360) { throw new RacingDomainValidationError('LiveryDecal rotation must be between 0 and 360 degrees'); } if (!props.type) { throw new RacingDomainValidationError('LiveryDecal type is required'); } } /** * Move decal to new position */ moveTo(x: number, y: number): LiveryDecal { return LiveryDecal.create({ ...this, x, y, }); } /** * Resize decal */ resize(width: number, height: number): LiveryDecal { return LiveryDecal.create({ ...this, width, height, }); } /** * Change z-index */ setZIndex(zIndex: number): LiveryDecal { return LiveryDecal.create({ ...this, zIndex, }); } /** * Rotate decal */ rotate(rotation: number): LiveryDecal { // Normalize rotation to 0-360 range const normalizedRotation = ((rotation % 360) + 360) % 360; return LiveryDecal.create({ ...this, rotation: normalizedRotation, }); } /** * Get CSS transform string for rendering */ getCssTransform(): string { return `rotate(${this.rotation}deg)`; } /** * Check if this decal overlaps with another */ overlapsWith(other: LiveryDecal): boolean { const thisRight = this.x + this.width; const thisBottom = this.y + this.height; const otherRight = other.x + other.width; const otherBottom = other.y + other.height; return !( thisRight <= other.x || this.x >= otherRight || thisBottom <= other.y || this.y >= otherBottom ); } }