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
+158 -2
View File
@@ -1,2 +1,158 @@
"""Wizard planeador. Stub — Sprint 11."""
raise NotImplementedError("wizard_planing — Sprint 11")
"""
Generador paramétrico — Casco Planeador (V-fondo, chine dura).
Genera una tabla de offsets realista para embarcaciones de planeo:
- Fondo en V con ángulo de astilla muerta variable
- Chine dura recorriendo toda la eslora
- Popa de espejo plana
- Proa aguda con alto ángulo de astilla muerta
- Sección transversal: dos regiones (bajo/sobre chine)
Parámetros típicos:
Cb: 0.40 0.48
Deadrise midship: 12° 22°
Deadrise bow: 35° 55°
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
# ---------------------------------------------------------------------------
# API pública
# ---------------------------------------------------------------------------
def make_planing_hull(
name: str = "Planeador",
lpp: float = 10.0,
beam: float = 3.0,
draft: float = 0.70,
depth: float = 1.20,
deadrise_mid: float = 18.0, # grados en cuaderna maestra
deadrise_bow: float = 45.0, # grados en proa
deadrise_stern: float = 8.0, # grados en popa
chine_frac: float = 0.55, # z_chine / draft
flare: float = 0.10, # ensanchamiento por encima del chine [fracción]
n_stations: int = 21,
n_waterlines: int = 11,
) -> Hull:
"""Genera un casco planeador con forma V y chine dura.
Parámetros
----------
deadrise_mid : float
Ángulo de astilla muerta [°] en la cuaderna maestra (midship).
Valores típicos: 12°–22°.
deadrise_bow : float
Ángulo de astilla muerta [°] en proa.
Valores típicos: 35°–55°.
chine_frac : float
Altura del chine como fracción del calado (01).
flare : float
Fracción de ensanchamiento por encima del chine (0 = sin ensanche).
"""
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 # normalizado ∈ [1, 1], 0=midship
# ── Plan form: ancho en línea de agua por estación ─────────────────
# El planeador tiene popa muy ancha y proa más estrecha
# f_stern > f_bow para que la popa sea la sección más llena
f_plan = _planing_plan_form(xi) # ∈ [0, 1]
# ── Ángulo de astilla muerta varía a lo largo de la eslora ─────────
dr_arr = _deadrise_distribution(xi, deadrise_mid, deadrise_bow, deadrise_stern)
data = np.zeros((n_stations, n_waterlines))
for i in range(n_stations):
y_max = (beam / 2.0) * f_plan[i]
z_c = draft * chine_frac # altura del chine
dr = np.radians(dr_arr[i]) # ángulo de astilla en esta estación
for j, z in enumerate(z_wl):
if z <= z_c:
# ── Zona V-fondo (bajo el chine) ──────────────────────
# y_chine se calcula desde el ángulo de astilla muerta
# tan(dr) = z_c / y_chine → y_chine = z_c / tan(dr)
tan_dr = max(np.tan(dr), 0.01)
y_chine_dr = z_c / tan_dr
y_chine = min(y_chine_dr, y_max)
y = y_chine * (z / z_c)
else:
# ── Zona sobre el chine (costado) ─────────────────────
tan_dr = max(np.tan(dr), 0.01)
y_chine_dr = z_c / tan_dr
y_chine = min(y_chine_dr, y_max)
# Ligero ensanchamiento por encima del chine
y = y_chine + (y_max - y_chine) * flare * (z - z_c) / (draft - z_c + 1e-9)
y = min(y, y_max)
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
)
# ---------------------------------------------------------------------------
# Helpers internos
# ---------------------------------------------------------------------------
def _planing_plan_form(xi: np.ndarray) -> np.ndarray:
"""
Plan form del planeador: sección de popa llena, proa estrecha.
xi ∈ [1, 1], xi=1 AP (popa), xi=+1 FP (proa).
El planeador tiene la sección maestra desplazada hacia popa (~35% Lpp desde FP).
"""
# Apex en xi ≈ 0.3 (30% desde popa) → máximo ancho allí
apex = -0.30
# Forma asimétrica: run suave hacia popa, proa afilada
f = np.zeros_like(xi)
for k, x in enumerate(xi):
if x >= apex:
# Desde apex hacia proa: caída rápida (proa aguda)
t = (x - apex) / (1.0 - apex)
f[k] = (1.0 - t ** 1.6)
else:
# Desde apex hacia popa: espejo plano, caída suave
t = (apex - x) / (1.0 + apex)
f[k] = (1.0 - t ** 2.8)
return np.maximum(f, 0.0)
def _deadrise_distribution(
xi: np.ndarray,
dr_mid: float,
dr_bow: float,
dr_stern: float,
) -> np.ndarray:
"""Distribución del ángulo de astilla muerta a lo largo de la eslora.
Varía suavemente de dr_stern (popa) → dr_mid (midship) → dr_bow (proa).
xi ∈ [1, 1], 1=AP, +1=FP.
"""
dr = np.zeros_like(xi)
for k, x in enumerate(xi):
if x >= 0.0:
# midship → proa
t = x
dr[k] = dr_mid + (dr_bow - dr_mid) * t ** 1.5
else:
# midship → popa
t = -x
dr[k] = dr_mid + (dr_stern - dr_mid) * t ** 2.0
return dr