feat(stability): Módulo 3 — Curva GZ + criterios IMO IS Code 2008

- gz_integrator.py: GZCurve, GZPoint, compute_gz_wall_sided (fórmula
  pared lateral), compute_gz_direct (integración Sutherland-Hodgman)
- imo_is2008.py: IMOCriterion, IMOResult, check_imo_is2008 —
  6 criterios A.2.1.1–A.2.1.6 del IS Code 2008 Cap.2
- gz_curve_widget.py: GZCurveWidget QPainter — curva cian, áreas
  sombreadas, líneas IMO, marcador AVS, tabla PASS/FAIL integrada
- main_window.py: GZCurveWidget en MOD_STABILITY, _compute_and_show_gz,
  _on_show_stability conectado al ribbon
- dark.qss: estilos GZCurveWidget
- test_module3_stability.py: 33 tests S-01..S-28 (315 total, todos pasan)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 13:59:32 -04:00
parent 62de89d63c
commit 0f85935fc8
7 changed files with 1871 additions and 3 deletions
+49 -2
View File
@@ -47,6 +47,8 @@ from PySide6.QtWidgets import (
from arshipdesign import __version__
from arshipdesign.core.project import Project
from arshipdesign.utils.logger import get_logger
from arshipdesign.stability import compute_gz_wall_sided, GZCurve, check_imo_is2008
from arshipdesign.ui.widgets.gz_curve_widget import GZCurveWidget
from arshipdesign.utils.settings import (
add_recent_file,
get_language,
@@ -813,6 +815,7 @@ class MainWindow(QMainWindow):
super().__init__()
self._project: Optional[Project] = None
self._current_hull = None # Hull activo en todos los visores
self._gz_widget: Optional[GZCurveWidget] = None
self._lang = get_language()
self._strings = _load_i18n(self._lang)
self._setup_ui()
@@ -878,6 +881,10 @@ class MainWindow(QMainWindow):
self._hydro_chart = HydrostaticsChartWidget()
self._module_area.set_module_widget(ModuleArea.MOD_CURVES, self._hydro_chart)
# Módulo de estabilidad GZ (sustituye el placeholder MOD_STABILITY)
self._gz_widget = GZCurveWidget()
self._module_area.set_module_widget(ModuleArea.MOD_STABILITY, self._gz_widget)
# Dock izquierdo — capas
self._layers_panel = LayersPanel(self._strings)
self._dock_layers = QDockWidget("Capas", self)
@@ -981,7 +988,7 @@ class MainWindow(QMainWindow):
g = self._ribbon.new_group(RibbonBar.TAB_ANALYSIS, "Estabilidad")
g.add_button(_spi(sp.SP_FileDialogDetailedView), "Curva GZ", "Curva GZ estática",
lambda: self._module_area.activate(M.MOD_STABILITY), False)
self._on_show_stability)
g.add_button(_spi(sp.SP_FileDialogDetailedView), "IMO IS2008", "Criterios IMO IS Code 2008", enabled=False)
g.add_button(_spi(sp.SP_FileDialogDetailedView), "Avería", "Estabilidad en avería", enabled=False)
@@ -1114,7 +1121,7 @@ class MainWindow(QMainWindow):
slot=self._on_export_hydrostatics_csv)
sm = m.addMenu("Estabilidad")
self._add_action(sm, "Curva GZ — Estabilidad estática", slot=lambda: self._module_area.activate(M.MOD_STABILITY), enabled=False)
self._add_action(sm, "Curva GZ — Estabilidad estática", slot=self._on_show_stability)
self._add_action(sm, "Criterios IMO IS Code 2008", enabled=False)
self._add_action(sm, "Criterio de viento A.749(18)", enabled=False)
self._add_action(sm, "Estabilidad en avería (SOLAS 2009)", enabled=False)
@@ -1330,6 +1337,8 @@ class MainWindow(QMainWindow):
logger.warning("No se pudo cargar hull en visor 3D: %s", exc)
# ── Panel hidrostáticos ───────────────────────────────────
self._update_hydrostatics(hull)
# ── Curva GZ (si el módulo está activo o precalcular) ─────
self._compute_and_show_gz()
def _on_offsets_dragging(self, offsets_table) -> None:
"""Slot ligero — actualiza vistas 2D durante drag sin resetear zoom ni actualizar 3D."""
@@ -1474,6 +1483,44 @@ class MainWindow(QMainWindow):
from PySide6.QtWidgets import QMessageBox
QMessageBox.critical(self, "Error al exportar", str(exc))
# ─────────────────────────────────────────────────────────
# CURVA GZ — ESTABILIDAD
# ─────────────────────────────────────────────────────────
def _compute_and_show_gz(self) -> None:
"""Calcula la curva GZ wall-sided y actualiza el widget de estabilidad."""
if self._current_hull is None:
return
if self._gz_widget is None:
return
try:
hull = self._current_hull
kg = hull.depth * 0.55
self.statusBar().showMessage("Calculando curva GZ…")
QApplication.processEvents()
gz_curve = compute_gz_wall_sided(hull, hull.draft, kg=kg)
imo_result = check_imo_is2008(gz_curve)
self._gz_widget.set_curve(gz_curve, imo_result)
# Actualizar indicador IMO en la barra de hidrostáticos
self._hydro.set_imo_status(
imo_result.overall_passed,
"" if imo_result.overall_passed else "GZ",
)
self.statusBar().showMessage(
f"Curva GZ calculada — {hull.name} "
f"GM={gz_curve.gm:.3f}m GZmax={gz_curve.gz_max:.3f}m "
f"AVS={gz_curve.avs:.0f}° "
f"IMO={'CUMPLE' if imo_result.overall_passed else 'FALLA'}"
)
except Exception as exc:
logger.warning("Error al calcular curva GZ: %s", exc)
def _on_show_stability(self) -> None:
"""Muestra el módulo de estabilidad GZ (calcula si hay casco disponible)."""
if self._current_hull is not None:
self._compute_and_show_gz()
self._module_area.activate(ModuleArea.MOD_STABILITY)
def _ask_save(self) -> bool:
reply = QMessageBox.question(
self, "Cambios sin guardar",