feat(sprint2): wizard 'Nuevo Proyecto' + 5 generadores paramétricos de casco

Generadores paramétricos (arshipdesign/parametric/):
  - wizard_planing.py    → V-fondo, chine dura, deadrise variable AP→FP
  - wizard_cruiser.py    → Carena redonda, plan form Lackenby, Cm ajustable
  - wizard_workboat.py   → Sección cajón, pantoque duro, fondo plano
  - wizard_sailing_mono.py → Velero fin keel, sección fina, LCB a popa
  - series60.py          → Serie 60 / mercante full, Cm ~ 0.96
  - __init__.py          → API unificada generate_hull(family, lpp, beam, draft)
                           + HullFamily enum con labels, rangos Cb, descripciones

Wizard UI (arshipdesign/ui/dialogs/wizards.py):
  - NewShipWizard: QDialog 4 pasos con barra de progreso animada
  - _StepDimensions:  nombre, Lpp, B, puntal, calado + ratios L/B y B/T en vivo
  - _StepFamily:      6 FamilyCard con HullThumbnail QPainter (sección maestra)
  - _StepRefine:      sliders Cb y LCB, spinboxes discretización
  - _StepPreview:     tabla hidrostáticos completa (V, D, Cb, LCB, KB, Awp...)
  - Al aceptar → Hull cargado en visor 3D del viewport Perspectiva

MainWindow:
  - _on_new_project() abre NewShipWizard (antes creaba proyecto vacío)
  - Tras accept(): carga hull en Viewer3DWidget si disponible

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 07:52:46 -04:00
parent 503e00bfc9
commit 002c00aff3
8 changed files with 1685 additions and 15 deletions
+112 -2
View File
@@ -1,2 +1,112 @@
"""Wizard workboat. Stub — Sprint 11."""
raise NotImplementedError("wizard_workboat Sprint 11")
"""
Generador paramétrico — Workboat / Supply / Remolcador (sección cajón).
Genera cascos de trabajo con:
- Sección transversal rectangular-redondeada
- Fondo casi plano (quilla de barra)
- Radio de pantoque duro
- Cuadernas muy llenas (Cm 0.920.97)
- Proa de bulbo cilíndrico o proa plana
Parámetros típicos:
Cb: 0.60 0.75
Velocidad/Froude: Fn 0.12 0.22
Autor: Álvaro Romero | Sprint 2A — AR-ShipDesign
"""
from __future__ import annotations
import numpy as np
from arshipdesign.core.hull import Hull
from arshipdesign.core.offsets import OffsetsTable
def make_workboat_hull(
name: str = "Workboat / Supply",
lpp: float = 15.0,
beam: float = 5.0,
draft: float = 1.80,
depth: float = 2.60,
cb: float = 0.67,
lcb_frac: float = 0.50,
bilge_radius_frac: float = 0.12, # radio de pantoque / calado
flat_bottom_frac: float = 0.85, # ancho fondo plano / manga
n_stations: int = 21,
n_waterlines: int = 11,
) -> Hull:
"""Genera un casco tipo workboat con sección cajón.
Parámetros
----------
bilge_radius_frac : float
Radio del pantoque como fracción del calado (0.080.18).
flat_bottom_frac : float
Anchura del fondo plano como fracción de la manga (0.800.92).
"""
x_sta = np.linspace(0.0, lpp, n_stations)
z_wl = np.linspace(0.0, draft, n_waterlines)
xi = (x_sta / lpp - 0.5) * 2.0
lcb_shift = 2.0 * (lcb_frac - 0.5)
# Plan form: muy llena, extremos redondeados pero no agudos
f_plan = _workboat_plan_form(xi, cb, lcb_shift)
# Geometría del pantoque
r_bilge = draft * bilge_radius_frac # radio en metros
y_flat = (beam / 2.0) * flat_bottom_frac # semi-manga del fondo plano
data = np.zeros((n_stations, n_waterlines))
for i in range(n_stations):
y_wl = (beam / 2.0) * f_plan[i]
# Escalar fondo plano y radio de pantoque con el ancho de la estación
scale = f_plan[i]
y_flat_i = y_flat * scale
r_bilge_i = r_bilge * max(scale, 0.3)
for j, z in enumerate(z_wl):
if z <= r_bilge_i:
# ── Zona de pantoque redondeado ──────────────────────
# Arco de círculo: y² + (z - r)² = r² → y = sqrt(r² - (r-z)²)
dz = r_bilge_i - z
inner = max(0.0, r_bilge_i**2 - dz**2)
y = y_flat_i - r_bilge_i + np.sqrt(inner)
else:
# ── Zona de costado (cuasi-vertical) ─────────────────
# Ligero flare hacia afuera
flare = 0.04
y = y_flat_i + (y_wl - y_flat_i) * (z - r_bilge_i) / (draft - r_bilge_i + 1e-9)
y = min(y, y_wl)
data[i, j] = max(0.0, y)
offsets = OffsetsTable(
x_stations=x_sta,
z_waterlines=z_wl,
data=data,
station_labels=[f"S{i}" for i in range(n_stations)],
lpp=lpp, beam=beam, draft=draft,
)
return Hull(
name=name, lpp=lpp, beam=beam, depth=depth, draft=draft, offsets=offsets
)
def _workboat_plan_form(
xi: np.ndarray, cb: float, lcb_shift: float
) -> np.ndarray:
"""Plan form para workboat: extremos redondeados pero llenos."""
n = _workboat_plan_exponent(cb)
xi_s = np.clip(xi - lcb_shift, -1.0, 1.0)
f = np.maximum(0.0, 1.0 - np.abs(xi_s) ** n)
return f
def _workboat_plan_exponent(cb: float) -> float:
"""Cb → exponente plan form (workboat más llena que displacement)."""
cb_vals = [0.55, 0.62, 0.68, 0.73, 0.78]
n_vals = [2.5, 3.0, 3.8, 4.5, 5.5]
return float(np.interp(cb, cb_vals, n_vals))