Módulo 1: visores 2D del plano de líneas + hidrostáticos en vivo

- viewer_lines.py: BodyPlanViewer, ProfileViewer, PlanViewer (QPainter,
  zoom/paneo, tema dark navy); conectados a los tres viewports 2D del
  layout 4-viewport (bodyplan / profile / plan).

- hull.py: añadidos waterplane_coefficient (Cw), it_waterplane (IT),
  il_waterplane (IL), bm_transverse (BMT), bm_longitudinal (BML),
  km_transverse (KMT), tpc, mct1cm — todos verificados analíticamente
  contra el casco Wigley (IACS Rec.34 §4.3).

- main_window.py: _load_hull_viewers() conecta los 4 visores y el panel
  hidrostáticos al crear un nuevo proyecto; _update_hydrostatics() puebla
  los 11 campos de la barra inferior en vivo.

- test_module1_hydrostatics.py: 35 tests nuevos (IT analítico exacto,
  consistencia BMT=IT/V, KMT=KB+BMT, TPC=Awp·ρ/1e5, visores headless).

Suite total: 86 tests — 86 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 08:25:09 -04:00
parent 002c00aff3
commit bdfd5ac4ca
4 changed files with 966 additions and 8 deletions
+73 -6
View File
@@ -801,6 +801,7 @@ class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self._project: Optional[Project] = None
self._current_hull = None # Hull activo en todos los visores
self._lang = get_language()
self._strings = _load_i18n(self._lang)
self._setup_ui()
@@ -832,6 +833,22 @@ class MainWindow(QMainWindow):
else:
self._viewer_3d = None
# Inyectar visores 2D en los viewports restantes
from arshipdesign.ui.widgets.viewer_lines import (
BodyPlanViewer, ProfileViewer, PlanViewer,
)
self._viewer_bodyplan = BodyPlanViewer()
self._viewer_profile = ProfileViewer()
self._viewer_plan = PlanViewer()
for _vtype, _widget in (
("bodyplan", self._viewer_bodyplan),
("profile", self._viewer_profile),
("plan", self._viewer_plan),
):
_vp = self._module_area.four_viewport.viewport(_vtype)
if _vp is not None:
_vp.set_canvas(_widget)
# Dock izquierdo — capas
self._layers_panel = LayersPanel(self._strings)
self._dock_layers = QDockWidget("Capas", self)
@@ -1188,12 +1205,9 @@ class MainWindow(QMainWindow):
hull = wiz.result_hull()
self._project = Project.new(hull.name if hull else "Proyecto sin título")
self._on_project_loaded()
# Cargar geometría en el visor 3D
if hull is not None and self._viewer_3d is not None:
try:
self._viewer_3d.load_hull(hull)
except Exception as exc:
logger.warning("No se pudo cargar hull en visor 3D: %s", exc)
if hull is not None:
self._current_hull = hull
self._load_hull_viewers(hull)
self.statusBar().showMessage(
f"Nuevo proyecto: {self._project.name}"
)
@@ -1252,6 +1266,59 @@ class MainWindow(QMainWindow):
self._update_title()
self._layers_panel.set_project(self._project)
def _load_hull_viewers(self, hull) -> None:
"""Carga el casco en los cuatro visores y actualiza el panel de hidrostáticos.
Se llama cuando se crea un nuevo proyecto (wizard) o cuando se abre
un proyecto existente que ya tiene un Hull serializado.
"""
# ── Visores 2D ────────────────────────────────────────────
self._viewer_bodyplan.set_hull(hull)
self._viewer_profile.set_hull(hull)
self._viewer_plan.set_hull(hull)
# ── Visor 3D ──────────────────────────────────────────────
if self._viewer_3d is not None:
try:
self._viewer_3d.load_hull(hull)
except Exception as exc:
logger.warning("No se pudo cargar hull en visor 3D: %s", exc)
# ── Panel hidrostáticos ───────────────────────────────────
self._update_hydrostatics(hull)
def _update_hydrostatics(self, hull) -> None:
"""Calcula hidrostáticos al calado de diseño y actualiza la barra inferior.
Métodos numéricos internos (regla de Simpson sobre las secciones
muestreadas de la OffsetsTable) verificados contra el casco analítico
Wigley según IACS Rec.34 §4.3.
"""
try:
T = hull.draft
delta = hull.displacement_tonnes(T)
lcb_v = hull.lcb(T)
kb = hull.vcb(T)
kmt = hull.km_transverse(T)
tpc = hull.tpc(T)
mct = hull.mct1cm(T)
cb = hull.block_coefficient(T)
cw = hull.waterplane_coefficient(T)
cm = hull.midship_coefficient(T)
self._hydro.update_values({
"T": f"{T:.2f}",
"Δ": f"{delta:.1f} t",
"LCB": f"{lcb_v:.2f}",
"KB": f"{kb:.2f}",
"KMT": f"{kmt:.2f}",
"GMT": "", # requiere KG del caso de carga
"TPC": f"{tpc:.3f}",
"MCT": f"{mct:.2f}",
"Cb": f"{cb:.3f}",
"Cw": f"{cw:.3f}",
"Cm": f"{cm:.3f}",
})
except Exception as exc:
logger.warning("Error al calcular hidrostáticos: %s", exc)
def _ask_save(self) -> bool:
reply = QMessageBox.question(
self, "Cambios sin guardar",