"""Material and color definitions for visual robot components.
Defines the appearance of robot links, including RGBA colors and
texture references.
Core Components:
- Material: Container for color and texture associations.
- Color: RGBA spatial color representation [0, 1].
"""
from __future__ import annotations
from dataclasses import dataclass, replace
from ..constants import (
DEFAULT_MATERIAL_RGBA,
)
from ..exceptions import RobotValidationError, ValidationErrorCode
[docs]
@dataclass(frozen=True)
class Color:
"""RGBA color representation with components in range [0.0, 1.0]."""
r: float # Red (0.0 - 1.0)
g: float # Green (0.0 - 1.0)
b: float # Blue (0.0 - 1.0)
a: float = 1.0 # Alpha (0.0 - 1.0)
[docs]
def __post_init__(self) -> None:
"""Validate color values."""
for component in (self.r, self.g, self.b, self.a):
if not 0.0 <= component <= 1.0:
raise RobotValidationError(
ValidationErrorCode.OUT_OF_RANGE,
"Color component must be in range [0.0, 1.0]",
target="ColorComponent",
value=component,
)
[docs]
@classmethod
def white(cls) -> Color:
"""Standard white color (1.0, 1.0, 1.0, 1.0)."""
return cls(1.0, 1.0, 1.0, 1.0)
[docs]
@classmethod
def black(cls) -> Color:
"""Standard black color (0.0, 0.0, 0.0, 1.0)."""
return cls(0.0, 0.0, 0.0, 1.0)
[docs]
@classmethod
def grey(cls) -> Color:
"""Standard grey color from constants."""
return cls(*DEFAULT_MATERIAL_RGBA)
[docs]
def to_tuple(self) -> tuple[float, float, float, float]:
"""Convert to RGBA tuple."""
return (self.r, self.g, self.b, self.a)
[docs]
def __str__(self) -> str:
"""String representation formatted as 'R G B A'."""
return f"{self.r} {self.g} {self.b} {self.a}"
[docs]
@dataclass(frozen=True)
class Material:
"""Material properties defining the visual surface of a robot link."""
name: str
color: Color | None = None
texture: str | None = None # Path to texture file
[docs]
def __post_init__(self) -> None:
"""Validate material has at least color or texture."""
if self.color is None and self.texture is None:
raise RobotValidationError(
ValidationErrorCode.VALUE_EMPTY,
f"Material '{self.name}' must have either color or texture",
target="MaterialDefinition",
value=self.name,
)
[docs]
def with_prefix(self, prefix: str) -> Material:
"""Create a new material with a prefixed name."""
return replace(self, name=f"{prefix}{self.name}")