"""Sistema de coordenadas naval. Regla de oro #11: coordenadas navales consistentes en TODO el código. - X: desde Perpendicular de Popa (Pp), positivo hacia proa. - Y: desde Línea de Crujía (CL), positivo a estribor, negativo a babor. - Z: desde Línea Base (BL), positivo hacia arriba. Todo en metros (SI). `ShipCoord` es inmutable (`frozen=True`). Cualquier transformación a pantalla pasa por renderers (UI) explícitos, NO por métodos de esta clase. """ from __future__ import annotations from typing import ClassVar from pydantic import BaseModel, ConfigDict, Field class ShipCoord(BaseModel): """Punto en el marco del buque (Pp / CL / BL). Buques típicos del segmento 30-40 m: - Eslora total: 20 - 60 m - Manga máxima: 5 - 15 m - Calado: 0.5 - 5 m - Altura sobre BL: -3 m (quilla) a +20 m (mástil) Los validadores permiten un margen extra para cubrir buques en desarrollo y errores de captura del integrador. """ model_config = ConfigDict(frozen=True, extra="forbid") x_pp: float = Field( ..., ge=-5.0, le=200.0, description="Metros desde Perpendicular de Popa, positivo hacia proa.", ) y_cl: float = Field( ..., ge=-30.0, le=30.0, description="Metros desde Línea de Crujía, +estribor / -babor.", ) z_bl: float = Field( ..., ge=-10.0, le=50.0, description="Metros desde Línea Base, positivo hacia arriba.", ) ORIGIN: ClassVar[str] = "Pp/CL/BL" UNIT: ClassVar[str] = "m" def as_tuple(self) -> tuple[float, float, float]: """Devuelve (x_pp, y_cl, z_bl) para serialización o cálculos numpy.""" return (self.x_pp, self.y_cl, self.z_bl) def is_starboard(self) -> bool: """True si está a estribor (y_cl > 0).""" return self.y_cl > 0.0 def is_port(self) -> bool: """True si está a babor (y_cl < 0).""" return self.y_cl < 0.0 def is_centerline(self) -> bool: """True si está en línea de crujía (y_cl == 0, con tolerancia mm).""" return abs(self.y_cl) < 1e-3 def distance_to(self, other: ShipCoord) -> float: """Distancia euclídea 3D en metros entre dos puntos del buque.""" dx = self.x_pp - other.x_pp dy = self.y_cl - other.y_cl dz = self.z_bl - other.z_bl return (dx * dx + dy * dy + dz * dz) ** 0.5 def __str__(self) -> str: return f"ShipCoord(x_pp={self.x_pp:.2f}, y_cl={self.y_cl:+.2f}, z_bl={self.z_bl:+.2f}) [m]"