Files
AR-Shipdesign/arshipdesign/parametric/wizard_planing.py
T
alro65 002c00aff3 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>
2026-05-27 07:52:46 -04:00

159 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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