Files
gridpilot.gg/packages/racing/domain/value-objects/LiveryDecal.ts
2025-12-10 12:38:55 +01:00

157 lines
3.7 KiB
TypeScript

/**
* 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<LiveryDecalProps, 'rotation'> & { 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 Error('LiveryDecal ID is required');
}
if (!props.imageUrl || props.imageUrl.trim().length === 0) {
throw new Error('LiveryDecal imageUrl is required');
}
if (props.x < 0 || props.x > 1) {
throw new Error('LiveryDecal x coordinate must be between 0 and 1 (normalized)');
}
if (props.y < 0 || props.y > 1) {
throw new Error('LiveryDecal y coordinate must be between 0 and 1 (normalized)');
}
if (props.width <= 0 || props.width > 1) {
throw new Error('LiveryDecal width must be between 0 and 1 (normalized)');
}
if (props.height <= 0 || props.height > 1) {
throw new Error('LiveryDecal height must be between 0 and 1 (normalized)');
}
if (!Number.isInteger(props.zIndex) || props.zIndex < 0) {
throw new Error('LiveryDecal zIndex must be a non-negative integer');
}
if (props.rotation < 0 || props.rotation > 360) {
throw new Error('LiveryDecal rotation must be between 0 and 360 degrees');
}
if (!props.type) {
throw new Error('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
);
}
}