""" NewShipWizard — Wizard de "Nuevo Proyecto" estilo DELFTship. Pasos: 1. Dimensiones principales (nombre, Lpp, manga, puntal, calado) 2. Familia de carena (cards visuales con thumbnail SVG) 3. Refinamiento (Cb slider, LCB, opciones avanzadas) 4. Preview (hidrostáticos + mini info) Al aceptar, el wizard devuelve un objeto Hull listo para cargar en el visor 3D y los 4 viewports. Autor: Álvaro Romero | Sprint 2B — AR-ShipDesign """ from __future__ import annotations from typing import Optional import numpy as np from PySide6.QtCore import Qt, QSize, Signal from PySide6.QtCore import QPointF from PySide6.QtGui import QColor, QFont, QPainter, QPen, QBrush, QPolygonF from PySide6.QtWidgets import ( QApplication, QDialog, QDialogButtonBox, QDoubleSpinBox, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QScrollArea, QSizePolicy, QSlider, QSpinBox, QStackedWidget, QVBoxLayout, QWidget, ) from arshipdesign.core.hull import Hull from arshipdesign.parametric import HullFamily, generate_hull # ───────────────────────────────────────────────────────────────────────────── # Colores del tema (igual que dark.qss) # ───────────────────────────────────────────────────────────────────────────── _BG = "#2c3042" _PANEL = "#343848" _ACCENT = "#4da8ff" _TEXT = "#cdd6f4" _MUTED = "#7a8ba8" _BORDER = "#3e4255" _GOLD = "#e8a020" _GREEN = "#48a858" _CARD_HL = "#1e2550" # ───────────────────────────────────────────────────────────────────────────── # Thumbnails de cada familia (dibujados en QPainter) # ───────────────────────────────────────────────────────────────────────────── class HullThumbnail(QWidget): """Miniatura 2D de la sección maestra de cada familia.""" def __init__( self, family: HullFamily, size: int = 80, selected: bool = False, parent: Optional[QWidget] = None, ) -> None: super().__init__(parent) self.family = family self.selected = selected self.setFixedSize(size, size) def paintEvent(self, event) -> None: # type: ignore[override] p = QPainter(self) p.setRenderHint(QPainter.RenderHint.Antialiasing) w, h = self.width(), self.height() # Fondo bg = QColor(_CARD_HL if self.selected else _PANEL) p.fillRect(0, 0, w, h, bg) # Borde border_col = QColor(_ACCENT if self.selected else _BORDER) p.setPen(QPen(border_col, 2 if self.selected else 1)) p.drawRect(1, 1, w - 2, h - 2) # Sección del casco p.setPen(QPen(QColor(_ACCENT), 2)) p.setBrush(QBrush(QColor(30, 60, 100, 160))) self._draw_section(p, w, h) p.end() def _draw_section(self, p: QPainter, w: int, h: int) -> None: mx, my = w / 2, h * 0.72 # centro de referencia scale = w * 0.38 fam = self.family if fam == HullFamily.PLANING: # V-fondo con chine dura pts = [ (-1.0, 0.0), # chine estribor (-0.30, -1.0), # quilla (keel) ( 0.30, -1.0), # quilla (keel) ( 1.0, 0.0), # chine babor ( 1.0, 0.35), # cubierta estribor (-1.0, 0.35), # cubierta babor ] elif fam in (HullFamily.DISPLACEMENT, HullFamily.SEMI_DISP): # Carena redonda — arco n = 16 angles = np.linspace(np.pi, 0, n) pts_lower = [(np.cos(a), -np.sin(a) * 0.9) for a in angles] pts = pts_lower + [(1.0, 0.30), (-1.0, 0.30)] elif fam == HullFamily.WORKBOAT: # Sección cajón — pantoque duro r = 0.20 # radio relativo pts = [ (-1.0, 0.30), (-1.0, -r), (-(1.0 - r), -1.0), ( (1.0 - r), -1.0), ( 1.0, -r), ( 1.0, 0.30), ] elif fam == HullFamily.SAILING: # Sección fina en V pts = [ (-0.70, 0.30), (-0.85, 0.0), (-0.50, -0.50), ( 0.0, -1.0), ( 0.50, -0.50), ( 0.85, 0.0), ( 0.70, 0.30), ] else: # MERCHANT # Sección muy llena — fondo casi plano r = 0.10 pts = [ (-1.0, 0.30), (-1.0, -r), (-(1.0 - r), -1.0), ( (1.0 - r), -1.0), ( 1.0, -r), ( 1.0, 0.30), ] poly = QPolygonF([ QPointF(mx + px * scale, my + py * scale) for px, py in pts ]) p.drawPolygon(poly) # Línea de agua p.setPen(QPen(QColor(_GOLD), 1, Qt.PenStyle.DashLine)) p.drawLine(int(mx - scale * 1.05), int(my), int(mx + scale * 1.05), int(my)) # ───────────────────────────────────────────────────────────────────────────── # Card de familia de carena (seleccionable) # ───────────────────────────────────────────────────────────────────────────── class FamilyCard(QFrame): selected = Signal(HullFamily) def __init__(self, family: HullFamily, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.family = family self._selected = False self.setFixedWidth(150) self.setCursor(Qt.CursorShape.PointingHandCursor) self._build() def _build(self) -> None: lo = QVBoxLayout(self) lo.setContentsMargins(8, 8, 8, 8) lo.setSpacing(4) lo.setAlignment(Qt.AlignmentFlag.AlignHCenter) self._thumb = HullThumbnail(self.family, size=80) lo.addWidget(self._thumb, 0, Qt.AlignmentFlag.AlignHCenter) lbl = QLabel(self.family.label_es) lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) lbl.setWordWrap(True) lbl.setStyleSheet(f"color:{_TEXT}; font-weight:600; font-size:11px;") lo.addWidget(lbl) cb_lo, cb_hi = self.family.cb_range rng = QLabel(f"Cb {cb_lo:.2f}–{cb_hi:.2f}") rng.setAlignment(Qt.AlignmentFlag.AlignCenter) rng.setStyleSheet(f"color:{_MUTED}; font-size:10px;") lo.addWidget(rng) self._update_style() def set_selected(self, sel: bool) -> None: self._selected = sel self._thumb.selected = sel self._thumb.update() self._update_style() def _update_style(self) -> None: if self._selected: self.setStyleSheet( f"FamilyCard{{background:{_CARD_HL}; border:2px solid {_ACCENT};" f"border-radius:6px;}}" ) else: self.setStyleSheet( f"FamilyCard{{background:{_PANEL}; border:1px solid {_BORDER};" f"border-radius:6px;}}" f"FamilyCard:hover{{border:1px solid {_ACCENT};}}" ) def mousePressEvent(self, event) -> None: # type: ignore[override] self.selected.emit(self.family) # ───────────────────────────────────────────────────────────────────────────── # Pasos del wizard # ───────────────────────────────────────────────────────────────────────────── class _StepDimensions(QWidget): """Paso 1: Nombre + dimensiones principales.""" def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self._build() def _build(self) -> None: lo = QVBoxLayout(self) lo.setContentsMargins(20, 10, 20, 10) lo.setSpacing(16) title = QLabel("Dimensiones principales") title.setStyleSheet(f"color:{_ACCENT}; font-size:16px; font-weight:700;") lo.addWidget(title) grid = QGridLayout() grid.setSpacing(10) def add_row(row, label, widget): lbl = QLabel(label) lbl.setStyleSheet(f"color:{_TEXT}; font-size:12px;") grid.addWidget(lbl, row, 0) grid.addWidget(widget, row, 1) self._name = QLineEdit("Mi Embarcación") self._name.setStyleSheet( f"background:{_PANEL}; color:{_TEXT}; border:1px solid {_BORDER};" f"border-radius:4px; padding:4px 8px; font-size:12px;" ) add_row(0, "Nombre del proyecto:", self._name) def spin(mini, maxi, val, dec=2, suffix=" m"): s = QDoubleSpinBox() s.setRange(mini, maxi) s.setValue(val) s.setDecimals(dec) s.setSuffix(suffix) s.setStyleSheet( f"background:{_PANEL}; color:{_TEXT}; border:1px solid {_BORDER};" f"border-radius:4px; padding:2px 6px; font-size:12px;" ) return s self._lpp = spin(2.0, 200.0, 15.0) self._beam = spin(0.5, 40.0, 4.0) self._depth = spin(0.3, 30.0, 2.30) self._draft = spin(0.2, 25.0, 1.60) add_row(1, "Eslora entre perp. (Lpp):", self._lpp) add_row(2, "Manga máxima (B):", self._beam) add_row(3, "Puntal de trazado (D):", self._depth) add_row(4, "Calado de diseño (T):", self._draft) # Validación dinámica self._draft.valueChanged.connect(self._check_draft) self._depth.valueChanged.connect(self._check_draft) lo.addLayout(grid) # Ratios de referencia self._ratio_lbl = QLabel() self._ratio_lbl.setStyleSheet(f"color:{_MUTED}; font-size:10px;") lo.addWidget(self._ratio_lbl) self._lpp.valueChanged.connect(self._update_ratios) self._beam.valueChanged.connect(self._update_ratios) self._draft.valueChanged.connect(self._update_ratios) self._update_ratios() lo.addStretch() def _check_draft(self) -> None: if self._draft.value() >= self._depth.value(): self._draft.setValue(self._depth.value() * 0.70) def _update_ratios(self) -> None: lpp = self._lpp.value() b = self._beam.value() t = self._draft.value() lb = lpp / b if b > 0 else 0 bt = b / t if t > 0 else 0 self._ratio_lbl.setText( f"Eslora/Manga = {lb:.1f} · Manga/Calado = {bt:.1f}" ) # Acceso def get_values(self) -> dict: return { "name": self._name.text().strip() or "Nueva Embarcación", "lpp": self._lpp.value(), "beam": self._beam.value(), "depth": self._depth.value(), "draft": self._draft.value(), } class _StepFamily(QWidget): """Paso 2: Selección de familia de carena.""" family_changed = Signal(HullFamily) def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self._selected = HullFamily.DISPLACEMENT self._cards: dict[HullFamily, FamilyCard] = {} self._build() def _build(self) -> None: lo = QVBoxLayout(self) lo.setContentsMargins(20, 10, 20, 10) lo.setSpacing(14) title = QLabel("Tipo de carena") title.setStyleSheet(f"color:{_ACCENT}; font-size:16px; font-weight:700;") lo.addWidget(title) subtitle = QLabel("Selecciona la familia que mejor se adapta a tu proyecto.") subtitle.setStyleSheet(f"color:{_MUTED}; font-size:11px;") lo.addWidget(subtitle) # Grid de cards cards_widget = QWidget() cards_lo = QHBoxLayout(cards_widget) cards_lo.setSpacing(10) cards_lo.setContentsMargins(0, 0, 0, 0) for fam in HullFamily: card = FamilyCard(fam) card.selected.connect(self._on_select) self._cards[fam] = card cards_lo.addWidget(card) cards_lo.addStretch() lo.addWidget(cards_widget) # Descripción self._desc = QLabel() self._desc.setWordWrap(True) self._desc.setStyleSheet( f"color:{_TEXT}; font-size:11px; background:{_PANEL};" f"border:1px solid {_BORDER}; border-radius:4px; padding:8px;" ) lo.addWidget(self._desc) lo.addStretch() self._on_select(HullFamily.DISPLACEMENT) def _on_select(self, fam: HullFamily) -> None: for f, c in self._cards.items(): c.set_selected(f == fam) self._selected = fam self._desc.setText(fam.description_es) self.family_changed.emit(fam) def get_family(self) -> HullFamily: return self._selected class _StepRefine(QWidget): """Paso 3: Refinamiento de parámetros (Cb, LCB, opciones).""" def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self._family = HullFamily.DISPLACEMENT self._cb_min = 0.45 self._cb_max = 0.65 self._build() def _build(self) -> None: lo = QVBoxLayout(self) lo.setContentsMargins(20, 10, 20, 10) lo.setSpacing(16) title = QLabel("Refinamiento") title.setStyleSheet(f"color:{_ACCENT}; font-size:16px; font-weight:700;") lo.addWidget(title) # ── Cb ──────────────────────────────────────────────────────── grp_cb = QGroupBox("Coeficiente de bloque (Cb)") grp_cb.setStyleSheet( f"QGroupBox{{color:{_TEXT}; border:1px solid {_BORDER};" f"border-radius:4px; margin-top:8px; font-size:11px;}}" f"QGroupBox::title{{subcontrol-origin:margin; left:8px; padding:0 4px;}}" ) cb_lo = QVBoxLayout(grp_cb) self._cb_slider = QSlider(Qt.Orientation.Horizontal) self._cb_slider.setRange(30, 82) # × 0.01 self._cb_slider.setValue(55) self._cb_slider.setStyleSheet( f"QSlider::groove:horizontal{{height:4px; background:{_BORDER}; border-radius:2px;}}" f"QSlider::handle:horizontal{{width:14px; height:14px; margin:-5px 0;" f"border-radius:7px; background:{_ACCENT};}}" f"QSlider::sub-page:horizontal{{background:{_ACCENT}; border-radius:2px;}}" ) self._cb_val_lbl = QLabel("Cb = 0.55") self._cb_val_lbl.setStyleSheet(f"color:{_ACCENT}; font-size:13px; font-weight:700;") self._cb_slider.valueChanged.connect(self._cb_changed) h = QHBoxLayout() self._cb_min_lbl = QLabel("0.45") self._cb_max_lbl = QLabel("0.65") self._cb_min_lbl.setStyleSheet(f"color:{_MUTED}; font-size:10px;") self._cb_max_lbl.setStyleSheet(f"color:{_MUTED}; font-size:10px;") h.addWidget(self._cb_min_lbl) h.addWidget(self._cb_slider, 1) h.addWidget(self._cb_max_lbl) cb_lo.addLayout(h) cb_lo.addWidget(self._cb_val_lbl, 0, Qt.AlignmentFlag.AlignHCenter) lo.addWidget(grp_cb) # ── LCB ─────────────────────────────────────────────────────── grp_lcb = QGroupBox("Centro longitudinal de carena (LCB)") grp_lcb.setStyleSheet(grp_cb.styleSheet()) lcb_lo = QVBoxLayout(grp_lcb) self._lcb_slider = QSlider(Qt.Orientation.Horizontal) self._lcb_slider.setRange(44, 58) # % de Lpp desde AP self._lcb_slider.setValue(52) self._lcb_slider.setStyleSheet(self._cb_slider.styleSheet()) self._lcb_val_lbl = QLabel("LCB = 52.0 % Lpp desde AP") self._lcb_val_lbl.setStyleSheet(f"color:{_ACCENT}; font-size:12px;") self._lcb_slider.valueChanged.connect(self._lcb_changed) h2 = QHBoxLayout() for txt in ("44%", " ", "58%"): l = QLabel(txt) l.setStyleSheet(f"color:{_MUTED}; font-size:10px;") if txt == " ": h2.addWidget(self._lcb_slider, 1) else: h2.addWidget(l) lcb_lo.addLayout(h2) lcb_lo.addWidget(self._lcb_val_lbl, 0, Qt.AlignmentFlag.AlignHCenter) lo.addWidget(grp_lcb) # ── Discretización ───────────────────────────────────────────── grp_disc = QGroupBox("Discretización") grp_disc.setStyleSheet(grp_cb.styleSheet()) disc_lo = QGridLayout(grp_disc) disc_lo.addWidget(QLabel("Estaciones:"), 0, 0) self._n_sta = QSpinBox() self._n_sta.setRange(7, 81) self._n_sta.setValue(21) self._n_sta.setSingleStep(2) self._n_sta.setStyleSheet( f"background:{_PANEL}; color:{_TEXT}; border:1px solid {_BORDER};" f"border-radius:4px; padding:2px 6px;" ) disc_lo.addWidget(self._n_sta, 0, 1) disc_lo.addWidget(QLabel("Líneas de agua:"), 1, 0) self._n_wl = QSpinBox() self._n_wl.setRange(5, 31) self._n_wl.setValue(11) self._n_wl.setSingleStep(2) self._n_wl.setStyleSheet(self._n_sta.styleSheet()) disc_lo.addWidget(self._n_wl, 1, 1) for lbl in grp_disc.findChildren(QLabel): lbl.setStyleSheet(f"color:{_TEXT}; font-size:11px;") lo.addWidget(grp_disc) lo.addStretch() def _cb_changed(self, val: int) -> None: cb = val / 100.0 self._cb_val_lbl.setText(f"Cb = {cb:.2f}") def _lcb_changed(self, val: int) -> None: self._lcb_val_lbl.setText(f"LCB = {val:.1f} % Lpp desde AP") def update_for_family(self, fam: HullFamily) -> None: """Actualiza límites del slider según la familia seleccionada.""" self._family = fam lo, hi = fam.cb_range self._cb_min = lo self._cb_max = hi self._cb_min_lbl.setText(f"{lo:.2f}") self._cb_max_lbl.setText(f"{hi:.2f}") # Escalar slider: rango 30–82 → lo–hi slider_lo = int(lo * 100) slider_hi = int(hi * 100) self._cb_slider.setRange(slider_lo, slider_hi) mid = (slider_lo + slider_hi) // 2 self._cb_slider.setValue(mid) def get_values(self) -> dict: return { "cb": self._cb_slider.value() / 100.0, "lcb_frac": self._lcb_slider.value() / 100.0, "n_stations": self._n_sta.value(), "n_waterlines": self._n_wl.value(), } class _StepPreview(QWidget): """Paso 4: Resumen de hidrostáticos calculados.""" def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self._hull: Optional[Hull] = None self._build() def _build(self) -> None: lo = QVBoxLayout(self) lo.setContentsMargins(20, 10, 20, 10) lo.setSpacing(10) title = QLabel("Resumen del proyecto") title.setStyleSheet(f"color:{_ACCENT}; font-size:16px; font-weight:700;") lo.addWidget(title) self._info_lbl = QLabel("Calculando…") self._info_lbl.setStyleSheet(f"color:{_MUTED}; font-size:11px;") lo.addWidget(self._info_lbl) # Tabla de hidrostáticos self._hydro_frame = QFrame() self._hydro_frame.setStyleSheet( f"background:{_PANEL}; border:1px solid {_BORDER}; border-radius:4px;" ) self._hydro_lo = QGridLayout(self._hydro_frame) self._hydro_lo.setSpacing(6) self._hydro_lo.setContentsMargins(14, 10, 14, 10) lo.addWidget(self._hydro_frame) # Thumbnail del body plan self._body_thumb = HullThumbnail(HullFamily.DISPLACEMENT, size=120) lo.addWidget(self._body_thumb, 0, Qt.AlignmentFlag.AlignHCenter) lo.addStretch() def _clear_hydro(self) -> None: while self._hydro_lo.count(): item = self._hydro_lo.takeAt(0) if item.widget(): item.widget().deleteLater() def update_hull(self, hull: Optional[Hull], family: HullFamily) -> None: self._clear_hydro() self._body_thumb.family = family self._body_thumb.selected = True self._body_thumb.update() if hull is None: self._info_lbl.setText("Sin embarcación generada.") return self._hull = hull self._info_lbl.setText( f"{hull.name} — {family.label_es}" ) rows = [ ("Lpp", f"{hull.lpp:.2f} m", "Eslora entre perpendiculares"), ("Manga (B)", f"{hull.beam:.2f} m", "Manga máxima"), ("Calado (T)", f"{hull.draft:.2f} m", "Calado de diseño"), ("Puntal (D)", f"{hull.depth:.2f} m", "Puntal de trazado"), ("Volumen (V)", f"{hull.volume_of_displacement():.2f} m³", "Volumen de carena"), ("Desplazamiento",f"{hull.displacement_tonnes():.2f} t", "Agua salada (ρ=1025)"), ("Cb", f"{hull.block_coefficient():.3f}", "Coeficiente de bloque"), ("LCB", f"{hull.lcb():.2f} m ({hull.lcb()/hull.lpp*100:.1f}% Lpp)", "Centro long. de carena desde AP"), ("KB", f"{hull.vcb():.3f} m", "Centro vertical de carena"), ("Awp", f"{hull.waterplane_area():.2f} m²", "Área plano de flotación"), ] for row_idx, (param, value, tooltip) in enumerate(rows): p_lbl = QLabel(param) v_lbl = QLabel(value) p_lbl.setStyleSheet(f"color:{_MUTED}; font-size:11px;") v_lbl.setStyleSheet(f"color:{_TEXT}; font-size:11px; font-weight:600;") p_lbl.setToolTip(tooltip) v_lbl.setToolTip(tooltip) self._hydro_lo.addWidget(p_lbl, row_idx, 0) self._hydro_lo.addWidget(v_lbl, row_idx, 1) def get_hull(self) -> Optional[Hull]: return self._hull # ───────────────────────────────────────────────────────────────────────────── # Wizard principal # ───────────────────────────────────────────────────────────────────────────── class NewShipWizard(QDialog): """Wizard de nuevo proyecto — 4 pasos. Uso: ---- >>> w = NewShipWizard(parent) >>> if w.exec() == QDialog.DialogCode.Accepted: ... hull = w.result_hull() """ def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.setWindowTitle("Nuevo Proyecto — AR-ShipDesign") self.setMinimumSize(780, 560) self.setModal(True) self.setStyleSheet(f""" QDialog {{ background: {_BG}; color: {_TEXT}; }} QLabel {{ color: {_TEXT}; background: transparent; }} """) self._step = 0 self._n_steps = 4 self._result: Optional[Hull] = None self._build() # ------------------------------------------------------------------ # Construcción # ------------------------------------------------------------------ def _build(self) -> None: root = QVBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) root.setSpacing(0) # ── Header ─────────────────────────────────────────────────── header = QWidget() header.setFixedHeight(56) header.setStyleSheet( f"background: {_PANEL}; border-bottom: 2px solid {_ACCENT};" ) h_lo = QHBoxLayout(header) h_lo.setContentsMargins(20, 0, 20, 0) title_lbl = QLabel("Nueva Embarcación") title_lbl.setStyleSheet( f"color:{_ACCENT}; font-size:18px; font-weight:700; background:transparent;" ) h_lo.addWidget(title_lbl) h_lo.addStretch() self._step_lbl = QLabel("Paso 1 de 4") self._step_lbl.setStyleSheet(f"color:{_MUTED}; font-size:11px; background:transparent;") h_lo.addWidget(self._step_lbl) root.addWidget(header) # ── Indicador de pasos ──────────────────────────────────────── self._step_bar = _StepBar( ["Dimensiones", "Carena", "Refinamiento", "Resumen"], parent=self, ) root.addWidget(self._step_bar) # ── Contenido (stackedWidget) ───────────────────────────────── self._stack = QStackedWidget() self._s1 = _StepDimensions() self._s2 = _StepFamily() self._s3 = _StepRefine() self._s4 = _StepPreview() self._stack.addWidget(self._s1) self._stack.addWidget(self._s2) self._stack.addWidget(self._s3) self._stack.addWidget(self._s4) root.addWidget(self._stack, 1) # Conectar familia → actualizar refinamiento self._s2.family_changed.connect(self._s3.update_for_family) # ── Botones ─────────────────────────────────────────────────── btn_bar = QWidget() btn_bar.setStyleSheet( f"background:{_PANEL}; border-top:1px solid {_BORDER};" ) btn_lo = QHBoxLayout(btn_bar) btn_lo.setContentsMargins(20, 10, 20, 10) self._btn_cancel = QPushButton("Cancelar") self._btn_back = QPushButton("← Atrás") self._btn_next = QPushButton("Siguiente →") self._btn_create = QPushButton("✔ Crear Embarcación") for btn in (self._btn_cancel, self._btn_back, self._btn_next): btn.setStyleSheet( f"QPushButton{{background:{_BORDER}; color:{_TEXT}; border:none;" f"border-radius:4px; padding:7px 18px; font-size:12px;}}" f"QPushButton:hover{{background:{_CARD_HL};}}" ) self._btn_create.setStyleSheet( f"QPushButton{{background:{_ACCENT}; color:#fff; border:none;" f"border-radius:4px; padding:7px 22px; font-size:12px; font-weight:700;}}" f"QPushButton:hover{{background:#5ab8ff;}}" ) btn_lo.addWidget(self._btn_cancel) btn_lo.addStretch() btn_lo.addWidget(self._btn_back) btn_lo.addWidget(self._btn_next) btn_lo.addWidget(self._btn_create) root.addWidget(btn_bar) # Conexiones self._btn_cancel.clicked.connect(self.reject) self._btn_back.clicked.connect(self._go_back) self._btn_next.clicked.connect(self._go_next) self._btn_create.clicked.connect(self._create) self._refresh_buttons() # ------------------------------------------------------------------ # Navegación # ------------------------------------------------------------------ def _go_next(self) -> None: if self._step < self._n_steps - 1: self._step += 1 self._stack.setCurrentIndex(self._step) if self._step == 3: self._build_preview() self._refresh_buttons() def _go_back(self) -> None: if self._step > 0: self._step -= 1 self._stack.setCurrentIndex(self._step) self._refresh_buttons() def _refresh_buttons(self) -> None: s = self._step self._step_lbl.setText(f"Paso {s+1} de {self._n_steps}") self._step_bar.set_active(s) self._btn_back.setVisible(s > 0) self._btn_next.setVisible(s < self._n_steps - 1) self._btn_create.setVisible(s == self._n_steps - 1) def _build_preview(self) -> None: """Genera el Hull y actualiza el paso 4.""" try: dims = self._s1.get_values() fam = self._s2.get_family() ref = self._s3.get_values() hull = generate_hull( family=fam, lpp=dims["lpp"], beam=dims["beam"], draft=dims["draft"], depth=dims["depth"], name=dims["name"], cb=ref["cb"], lcb_frac=ref["lcb_frac"], n_stations=ref["n_stations"], n_waterlines=ref["n_waterlines"], ) self._result = hull self._s4.update_hull(hull, fam) except Exception as exc: self._s4.update_hull(None, self._s2.get_family()) self._s4._info_lbl.setText(f"Error al generar: {exc}") def _create(self) -> None: if self._result is None: self._build_preview() if self._result is not None: self.accept() # ------------------------------------------------------------------ # Acceso externo # ------------------------------------------------------------------ def result_hull(self) -> Optional[Hull]: """Retorna el Hull generado (sólo válido tras accept()).""" return self._result # ───────────────────────────────────────────────────────────────────────────── # Barra visual de pasos # ───────────────────────────────────────────────────────────────────────────── class _StepBar(QWidget): def __init__(self, labels: list[str], parent: Optional[QWidget] = None) -> None: super().__init__(parent) self._labels = labels self._active = 0 self.setFixedHeight(40) def set_active(self, idx: int) -> None: self._active = idx self.update() def paintEvent(self, event) -> None: # type: ignore[override] p = QPainter(self) p.setRenderHint(QPainter.RenderHint.Antialiasing) n = len(self._labels) w, h = self.width(), self.height() step_w = w / n for i, lbl in enumerate(self._labels): cx = int(step_w * i + step_w / 2) cy = h // 2 # Línea de conexión if i > 0: prev_cx = int(step_w * (i - 1) + step_w / 2) color = _ACCENT if i <= self._active else _BORDER p.setPen(QPen(QColor(color), 2)) p.drawLine(prev_cx + 10, cy, cx - 10, cy) # Círculo r = 10 if i < self._active: # Completado p.setBrush(QBrush(QColor(_ACCENT))) p.setPen(Qt.PenStyle.NoPen) p.drawEllipse(cx - r, cy - r, r * 2, r * 2) p.setPen(QPen(QColor("#fff"), 2)) p.drawText(cx - r, cy - r, r * 2, r * 2, Qt.AlignmentFlag.AlignCenter, "✓") elif i == self._active: # Activo p.setBrush(QBrush(QColor(_ACCENT))) p.setPen(QPen(QColor("#fff"), 2)) p.drawEllipse(cx - r, cy - r, r * 2, r * 2) p.drawText(cx - r, cy - r, r * 2, r * 2, Qt.AlignmentFlag.AlignCenter, str(i + 1)) else: # Pendiente p.setBrush(QBrush(QColor(_PANEL))) p.setPen(QPen(QColor(_BORDER), 1)) p.drawEllipse(cx - r, cy - r, r * 2, r * 2) p.setPen(QPen(QColor(_MUTED), 1)) p.drawText(cx - r, cy - r, r * 2, r * 2, Qt.AlignmentFlag.AlignCenter, str(i + 1)) # Texto del paso p.setPen(QPen(QColor(_ACCENT if i == self._active else _MUTED))) p.drawText(cx - 50, cy + r + 2, 100, 14, Qt.AlignmentFlag.AlignCenter, lbl) p.end()