/** * Value Object: LiveryDecal * Represents a decal/logo placed on a livery */ import { RacingDomainValidationError } from '../errors/RacingDomainError'; import type { IValueObject } from '@core/shared/domain'; 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 implements IValueObject { 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 props(): LiveryDecalProps { return { id: this.id, imageUrl: this.imageUrl, x: this.x, y: this.y, width: this.width, height: this.height, rotation: this.rotation, zIndex: this.zIndex, type: this.type, }; } /** * 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 ); } equals(other: IValueObject): boolean { const a = this.props; const b = other.props; return ( a.id === b.id && a.imageUrl === b.imageUrl && a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height && a.rotation === b.rotation && a.zIndex === b.zIndex && a.type === b.type ); } }