rename to core
This commit is contained in:
190
core/racing/domain/value-objects/LiveryDecal.ts
Normal file
190
core/racing/domain/value-objects/LiveryDecal.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Value Object: LiveryDecal
|
||||
* Represents a decal/logo placed on a livery
|
||||
*/
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/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<LiveryDecalProps> {
|
||||
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 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)`;
|
||||
}
|
||||
|
||||
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<LiveryDecalProps>): 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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user