Modulo 1: serializacion Hull / Project en formato .arsd (Task 11)
- hull.py: Hull.to_dict() serializa a dict JSON con formato hull_v1 (arrays numpy -> listas Python); Hull.from_dict() deserializa con validacion de claves y forma de array. - project.py: Project.hull (property lazy) deserializa el Hull desde ship_data; Project.set_hull() persiste el Hull y marca is_modified. - main_window.py: _on_new_project guarda el Hull en el proyecto; _on_project_loaded restaura el Hull en todos los visores al abrir un archivo .arsd; _on_hull_changed_from_editor mantiene el proyecto sincronizado con ediciones en el editor de offsets. - test_serialization.py: 26 tests (round-trip dict, round-trip ZIP, 5 familias parametricas, escritura atomica, proyecto sin Hull). Suite total: 112 tests -- 112 passed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -389,6 +389,76 @@ class Hull:
|
||||
mesh = pv.PolyData(all_pts, faces_arr)
|
||||
return mesh.triangulate()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Serialización JSON (.arsd)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Serializa el Hull a un diccionario JSON-serializable.
|
||||
|
||||
Formato interno: ``hull_v1``.
|
||||
Los arrays numpy se convierten a listas de Python para compatibilidad
|
||||
con json.dumps sin dependencias adicionales.
|
||||
|
||||
IACS Rec.34 §6 — trazabilidad de datos de entrada (offsets guardados
|
||||
fielmente con la precisión de la tabla original).
|
||||
"""
|
||||
ot = self.offsets
|
||||
return {
|
||||
"format": "hull_v1",
|
||||
"name": self.name,
|
||||
"lpp": self.lpp,
|
||||
"beam": self.beam,
|
||||
"depth": self.depth,
|
||||
"draft": self.draft,
|
||||
"offsets": {
|
||||
"lpp": ot.lpp,
|
||||
"beam": ot.beam,
|
||||
"draft": ot.draft,
|
||||
"x_stations": ot.x_stations.tolist(),
|
||||
"z_waterlines": ot.z_waterlines.tolist(),
|
||||
"station_labels": list(ot.station_labels),
|
||||
"data": ot.data.tolist(), # (n_sta, n_wl)
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "Hull":
|
||||
"""Deserializa un Hull desde un diccionario (leído de un archivo .arsd).
|
||||
|
||||
Compatible con los formatos ``hull_v1`` y datos heredados sin versión.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : dict
|
||||
Diccionario generado por ``Hull.to_dict()``.
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
Si faltan campos obligatorios.
|
||||
ValueError
|
||||
Si las dimensiones de la tabla son inconsistentes.
|
||||
"""
|
||||
od = data["offsets"]
|
||||
offsets = OffsetsTable(
|
||||
x_stations = np.array(od["x_stations"], dtype=float),
|
||||
z_waterlines = np.array(od["z_waterlines"], dtype=float),
|
||||
data = np.array(od["data"], dtype=float),
|
||||
station_labels = od.get("station_labels", []),
|
||||
lpp = float(od["lpp"]),
|
||||
beam = float(od["beam"]),
|
||||
draft = float(od["draft"]),
|
||||
)
|
||||
return cls(
|
||||
name = str(data["name"]),
|
||||
lpp = float(data["lpp"]),
|
||||
beam = float(data["beam"]),
|
||||
depth = float(data["depth"]),
|
||||
draft = float(data["draft"]),
|
||||
offsets = offsets,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dunder
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -248,6 +248,45 @@ class Project:
|
||||
"""Marca el proyecto como modificado."""
|
||||
self._is_modified = True
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# HULL
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
@property
|
||||
def hull(self):
|
||||
"""Hull activo del proyecto, o None si no hay geometría guardada.
|
||||
|
||||
El Hull se deserializa bajo demanda desde ship_data["hull"].
|
||||
La primera llamada realiza la conversión; las siguientes también
|
||||
(el objeto no se cachea para mantener la coherencia con ediciones
|
||||
posteriores de ship_data).
|
||||
|
||||
Returns
|
||||
-------
|
||||
Hull | None
|
||||
"""
|
||||
hull_data = self.ship_data.get("hull")
|
||||
if not hull_data or hull_data.get("format") not in ("hull_v1",):
|
||||
return None
|
||||
try:
|
||||
from arshipdesign.core.hull import Hull
|
||||
return Hull.from_dict(hull_data)
|
||||
except Exception as exc:
|
||||
logger.warning("No se pudo deserializar el Hull: %s", exc)
|
||||
return None
|
||||
|
||||
def set_hull(self, hull) -> None:
|
||||
"""Serializa el Hull en ship_data y marca el proyecto como modificado.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
hull : Hull
|
||||
El casco a guardar. Se serializa llamando a ``hull.to_dict()``.
|
||||
"""
|
||||
self.ship_data["hull"] = hull.to_dict()
|
||||
self._is_modified = True
|
||||
logger.debug("Hull '%s' guardado en proyecto '%s'", hull.name, self.name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Project(name={self.name!r}, path={self.path})"
|
||||
|
||||
|
||||
@@ -1224,6 +1224,7 @@ class MainWindow(QMainWindow):
|
||||
self._on_project_loaded()
|
||||
if hull is not None:
|
||||
self._current_hull = hull
|
||||
self._project.set_hull(hull) # persistir en ship_data
|
||||
self._load_hull_viewers(hull)
|
||||
self.statusBar().showMessage(
|
||||
f"Nuevo proyecto: {self._project.name}"
|
||||
@@ -1282,6 +1283,12 @@ class MainWindow(QMainWindow):
|
||||
def _on_project_loaded(self) -> None:
|
||||
self._update_title()
|
||||
self._layers_panel.set_project(self._project)
|
||||
# Restaurar Hull si el proyecto contiene geometría guardada
|
||||
hull = self._project.hull
|
||||
if hull is not None:
|
||||
self._current_hull = hull
|
||||
self._load_hull_viewers(hull)
|
||||
logger.info("Hull '%s' restaurado desde proyecto", hull.name)
|
||||
|
||||
def _load_hull_viewers(self, hull, *, _skip_offsets_editor: bool = False) -> None:
|
||||
"""Carga el casco en todos los visores (2D, 3D, offsets) y actualiza hidrostáticos.
|
||||
@@ -1306,8 +1313,10 @@ class MainWindow(QMainWindow):
|
||||
self._update_hydrostatics(hull)
|
||||
|
||||
def _on_hull_changed_from_editor(self, hull) -> None:
|
||||
"""Slot: el editor de offsets reconstruyo el Hull — propagar a los demas visores."""
|
||||
"""Slot: el editor de offsets reconstruyo el Hull — propagar a visores y proyecto."""
|
||||
self._current_hull = hull
|
||||
if self._project is not None:
|
||||
self._project.set_hull(hull) # mantener proyecto sincronizado
|
||||
# _skip_offsets_editor=True para no re-poblar la tabla (ya esta actualizada)
|
||||
self._load_hull_viewers(hull, _skip_offsets_editor=True)
|
||||
self.statusBar().showMessage(f"Offsets actualizados — {hull.name}")
|
||||
|
||||
Reference in New Issue
Block a user