From 58228080e82f6c9c0119ba4d546fa1182c98bd9b Mon Sep 17 00:00:00 2001 From: alro1965 Date: Wed, 27 May 2026 17:54:08 -0400 Subject: [PATCH] fix(ui): smooth B-spline section/waterline curves + 3D ready-signal sync - viewer_3d.py: add `ready` Signal emitted once QtInteractor finishes init - main_window.py: connect ready signal to sync active hull into 3D viewer on startup - viewer_lines.py: add _smooth_pts helper; replace straight polylines in BodyPlanViewer and PlanViewer CAPA 3 with B-spline interpolated curves (80 sample points) Co-Authored-By: Claude Sonnet 4.6 --- arshipdesign/ui/main_window.py | 10 ++++++ arshipdesign/ui/widgets/viewer_3d.py | 5 ++- arshipdesign/ui/widgets/viewer_lines.py | 48 ++++++++++++++++++++----- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/arshipdesign/ui/main_window.py b/arshipdesign/ui/main_window.py index faf47e9..ec34a83 100644 --- a/arshipdesign/ui/main_window.py +++ b/arshipdesign/ui/main_window.py @@ -841,6 +841,7 @@ class MainWindow(QMainWindow): from arshipdesign.ui.widgets.viewer_3d import Viewer3DWidget, _PYVISTA_OK if _PYVISTA_OK: self._viewer_3d = Viewer3DWidget() + self._viewer_3d.ready.connect(self._on_3d_viewer_ready) vp = self._module_area.four_viewport.viewport("perspective") if vp is not None: vp.set_canvas(self._viewer_3d) @@ -1357,6 +1358,15 @@ class MainWindow(QMainWindow): self._load_hull_viewers(hull, _skip_offsets_editor=True) self.statusBar().showMessage(f"Offsets actualizados — {hull.name}") + def _on_3d_viewer_ready(self) -> None: + """El plotter 3D terminó de inicializarse — sincronizar con el casco activo.""" + if self._current_hull is not None and self._viewer_3d is not None: + try: + self._current_hull.invalidate() + self._viewer_3d.load_hull(self._current_hull) + except Exception as exc: + logger.warning("Error al sincronizar 3D tras init: %s", exc) + def _on_offsets_edited_from_viewer(self, offsets_table) -> None: """Slot: fin del drag — persistir + actualizar 3D + hidrostáticos. diff --git a/arshipdesign/ui/widgets/viewer_3d.py b/arshipdesign/ui/widgets/viewer_3d.py index f557d05..a025bb4 100644 --- a/arshipdesign/ui/widgets/viewer_3d.py +++ b/arshipdesign/ui/widgets/viewer_3d.py @@ -17,7 +17,7 @@ import logging from typing import Optional import numpy as np -from PySide6.QtCore import Qt, QTimer +from PySide6.QtCore import Qt, QTimer, Signal from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget logger = logging.getLogger("ui.viewer_3d") @@ -50,6 +50,8 @@ class Viewer3DWidget(QWidget): >>> v.reset_camera() """ + ready: Signal = Signal() + def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self._plotter: Optional["QtInteractor"] = None @@ -114,6 +116,7 @@ class Viewer3DWidget(QWidget): else: self._load_default_wigley() self._ready = True + self.ready.emit() logger.info("Viewer3DWidget: QtInteractor iniciado correctamente") except Exception as exc: diff --git a/arshipdesign/ui/widgets/viewer_lines.py b/arshipdesign/ui/widgets/viewer_lines.py index ed337bb..61f5792 100644 --- a/arshipdesign/ui/widgets/viewer_lines.py +++ b/arshipdesign/ui/widgets/viewer_lines.py @@ -365,6 +365,25 @@ def _draw_cnet_planview(p: QPainter, ot, w2s_fn) -> None: p.drawPath(path) +def _smooth_pts(pts_2d: np.ndarray, n: int = 60) -> np.ndarray: + """Muestrea n puntos de una B-spline interpolada a través de pts_2d. + + pts_2d : shape (m, 2) + Returns shape (n, 2). Si hay < 4 puntos o falla el spline, + devuelve los puntos originales sin modificar. + """ + from arshipdesign.geometry.nurbs_curve import BSplineCurve + m = len(pts_2d) + if m < 4: + return pts_2d + try: + k = min(3, m - 1) + curve = BSplineCurve(pts_2d, degree=k) + return curve.sample(n) # shape (n, 2) + except Exception: + return pts_2d + + # ───────────────────────────────────────────────────────────────────────────── # 1. Body Plan — secciones transversales # ───────────────────────────────────────────────────────────────────────────── @@ -475,14 +494,20 @@ class BodyPlanViewer(_BaseViewer): z_arr = ot.z_waterlines sign = 1.0 if is_fwd else -1.0 + # Smooth B-spline through the section data points + raw = np.column_stack([y_arr * sign, z_arr]) + # Close to keel: append (0, 0) as the last interpolation point + keel_row = np.array([[0.0, 0.0]]) + raw_closed = np.vstack([raw, keel_row]) + smooth = _smooth_pts(raw_closed, n=80) + path = QPainterPath() - for k, (y, z) in enumerate(zip(y_arr, z_arr)): - pt = self._w2s(sign * y, z) - if k == 0: + for k_pt in range(len(smooth)): + pt = self._w2s(smooth[k_pt, 0], smooth[k_pt, 1]) + if k_pt == 0: path.moveTo(pt) else: path.lineTo(pt) - path.lineTo(self._w2s(0.0, 0.0)) p.drawPath(path) # Flotación de diseño (encima de todo lo anterior) @@ -691,15 +716,20 @@ class PlanViewer(_BaseViewer): width = 2.2 else: color = QColor(_WATERLINE) - color.setAlphaF(0.40 + 0.50 * frac) - width = 1.1 + color.setAlphaF(0.45 + 0.45 * frac) + width = 1.2 p.setPen(QPen(color, width)) p.setBrush(Qt.BrushStyle.NoBrush) + + # Smooth B-spline through the waterline data points + raw = np.column_stack([ot.x_stations, ot.data[:, j]]) + smooth = _smooth_pts(raw, n=80) + path = QPainterPath() - for i, (x, y) in enumerate(zip(ot.x_stations, ot.data[:, j])): - pt = self._w2s(x, y) - if i == 0: + for k_pt in range(len(smooth)): + pt = self._w2s(smooth[k_pt, 0], smooth[k_pt, 1]) + if k_pt == 0: path.moveTo(pt) else: path.lineTo(pt)