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:
2026-05-27 08:33:34 -04:00
parent 2137b0a228
commit 3b0d5e9e50
4 changed files with 376 additions and 1 deletions
+70
View File
@@ -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
# ------------------------------------------------------------------
+39
View File
@@ -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})"