Módulo 2 completo: HydrostaticsChartWidget + integración + tests (Tasks 14–16)

Task 14 — HydrostaticsChartWidget (QPainter):
- 9 paneles cuadrícula 3x3: Δ, V, Awp, LCB/LCF, KB/BMT, KMT/KML,
  TPC, MCT, Cb/Cw/Cm
- Cursor vertical compartido: clic/arrastre en cualquier panel mueve
  el cursor en todos y actualiza la barra de valores
- _InfoBar: franja superior con valores interpolados al calado activo
- _nice_ticks(): escala de ejes legible sin dependencias externas
- Sin dependencias externas (sólo PySide6 + numpy)

Task 15 — Integración en MainWindow:
- MOD_CURVES cargado con HydrostaticsChartWidget (sustituye placeholder)
- _on_compute_hydrostatics(): calcula HydrostaticCurves.compute(n=30)
- _on_show_hydrostatics(): abre el módulo (calculando si no hay datos)
- _on_export_hydrostatics_csv(): exporta CSV con QFileDialog
- Ribbon tab Análisis: botones Calcular, Curvas, Exp. CSV activos
- Menú Análisis → Hidrostática: 3 acciones funcionando
- dark.qss: estilos para hydrostaticsChart, hydroInfoBar, hydroPlaceholder

Task 16 — Tests V&V (58 tests):
- Widget headless W-01..W-08: construcción, set_curves, señales, clampeo
- CSV V037..V044: columnas, filas, monotonicidad, separadores, decimal coma
- at_draft V045..V049: interpolación lineal, clampeo, tipo retorno
- 5 familias V050..V055: Δ monótona, V>0, Cb∈(0,1), KMT>KB, KML>KMT, TPC>0
- IACS Rec.34 §4.3 V056..V062: Cb=4/9, Cw=2/3, KB, LCB=LCF=L/2, Cp=Cb/Cm,
  convergencia de malla <2%

Total: 282 tests, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 09:21:27 -04:00
parent 98ff57ed08
commit 4630d2d19f
4 changed files with 1129 additions and 4 deletions
+80 -4
View File
@@ -870,6 +870,11 @@ class MainWindow(QMainWindow):
self._offsets_editor.hull_changed.connect(self._on_hull_changed_from_editor)
self._module_area.set_module_widget(ModuleArea.MOD_OFFSETS, self._offsets_editor)
# Visor de curvas hidrostáticas (sustituye el placeholder MOD_CURVES)
from arshipdesign.ui.widgets.hydrostatics_chart import HydrostaticsChartWidget
self._hydro_chart = HydrostaticsChartWidget()
self._module_area.set_module_widget(ModuleArea.MOD_CURVES, self._hydro_chart)
# Dock izquierdo — capas
self._layers_panel = LayersPanel(self._strings)
self._dock_layers = QDockWidget("Capas", self)
@@ -964,9 +969,12 @@ class MainWindow(QMainWindow):
# ── Tab ANÁLISIS ──────────────────────────────────────────
g = self._ribbon.new_group(RibbonBar.TAB_ANALYSIS, "Hidrostática")
g.add_button(_spi(sp.SP_FileDialogDetailedView), "Calcular", "Calcular hidrostáticos", enabled=False)
g.add_button(_spi(sp.SP_FileDialogDetailedView), "Calcular", "Calcular curvas hidrostáticas",
self._on_compute_hydrostatics)
g.add_button(_spi(sp.SP_FileDialogDetailedView), "Curvas", "Curvas hidrostáticas",
lambda: self._module_area.activate(M.MOD_CURVES), False)
self._on_show_hydrostatics)
g.add_button(_spi(sp.SP_DialogSaveButton), "Exp. CSV", "Exportar curvas como CSV",
self._on_export_hydrostatics_csv)
g = self._ribbon.new_group(RibbonBar.TAB_ANALYSIS, "Estabilidad")
g.add_button(_spi(sp.SP_FileDialogDetailedView), "Curva GZ", "Curva GZ estática",
@@ -1095,8 +1103,12 @@ class MainWindow(QMainWindow):
m = mb.addMenu("Análisis")
sm = m.addMenu("Hidrostática")
self._add_action(sm, "Calcular hidrostáticos", enabled=False)
self._add_action(sm, "Curvas hidrostáticas", slot=lambda: self._module_area.activate(M.MOD_CURVES), enabled=False)
self._add_action(sm, "Calcular hidrostáticos",
slot=self._on_compute_hydrostatics)
self._add_action(sm, "Curvas hidrostáticas",
slot=self._on_show_hydrostatics)
self._add_action(sm, "Exportar curvas CSV…",
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)
@@ -1387,6 +1399,70 @@ class MainWindow(QMainWindow):
except Exception as exc:
logger.warning("Error al calcular hidrostáticos: %s", exc)
# ─────────────────────────────────────────────────────────
# CURVAS HIDROSTÁTICAS
# ─────────────────────────────────────────────────────────
def _on_compute_hydrostatics(self) -> None:
"""Calcula las curvas hidrostáticas y muestra el módulo."""
if self._current_hull is None:
from PySide6.QtWidgets import QMessageBox
QMessageBox.information(
self, "Sin casco", "Crea o abre un proyecto con un casco definido."
)
return
try:
from arshipdesign.hydrostatics.curves_of_form import HydrostaticCurves
self.statusBar().showMessage("Calculando curvas hidrostáticas…")
QApplication.processEvents()
curves = HydrostaticCurves.compute(
self._current_hull, n_points=30, rho=1025.0
)
self._hydro_chart.set_curves(curves)
self._module_area.activate(ModuleArea.MOD_CURVES)
self.statusBar().showMessage(
f"Curvas hidrostáticas calculadas — {curves.hull_name} "
f"({len(curves.points)} puntos, T: "
f"{curves.drafts[0]:.2f}{curves.drafts[-1]:.2f} m)"
)
except Exception as exc:
logger.error("Error al calcular curvas: %s", exc)
from PySide6.QtWidgets import QMessageBox
QMessageBox.critical(self, "Error al calcular", str(exc))
def _on_show_hydrostatics(self) -> None:
"""Muestra el módulo de curvas (sin recalcular si ya hay datos)."""
if self._hydro_chart.curves is None and self._current_hull is not None:
self._on_compute_hydrostatics()
else:
self._module_area.activate(ModuleArea.MOD_CURVES)
def _on_export_hydrostatics_csv(self) -> None:
"""Exporta las curvas hidrostáticas como CSV."""
if self._hydro_chart.curves is None:
from PySide6.QtWidgets import QMessageBox
QMessageBox.information(
self, "Sin datos", "Calcula las curvas hidrostáticas primero."
)
return
curves = self._hydro_chart.curves
default_name = f"{curves.hull_name}_hidrostatics.csv".replace(" ", "_")
path, _ = QFileDialog.getSaveFileName(
self, "Exportar curvas hidrostáticas",
str(Path.home() / default_name),
"CSV (*.csv);;Todos los archivos (*)",
)
if not path:
return
try:
lines = curves.to_csv_lines(sep=",", decimal=".")
Path(path).write_text("\n".join(lines) + "\n", encoding="utf-8")
self.statusBar().showMessage(f"CSV exportado: {path}")
except Exception as exc:
logger.error("Error al exportar CSV: %s", exc)
from PySide6.QtWidgets import QMessageBox
QMessageBox.critical(self, "Error al exportar", str(exc))
def _ask_save(self) -> bool:
reply = QMessageBox.question(
self, "Cambios sin guardar",