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 <noreply@anthropic.com>
This commit is contained in:
@@ -841,6 +841,7 @@ class MainWindow(QMainWindow):
|
|||||||
from arshipdesign.ui.widgets.viewer_3d import Viewer3DWidget, _PYVISTA_OK
|
from arshipdesign.ui.widgets.viewer_3d import Viewer3DWidget, _PYVISTA_OK
|
||||||
if _PYVISTA_OK:
|
if _PYVISTA_OK:
|
||||||
self._viewer_3d = Viewer3DWidget()
|
self._viewer_3d = Viewer3DWidget()
|
||||||
|
self._viewer_3d.ready.connect(self._on_3d_viewer_ready)
|
||||||
vp = self._module_area.four_viewport.viewport("perspective")
|
vp = self._module_area.four_viewport.viewport("perspective")
|
||||||
if vp is not None:
|
if vp is not None:
|
||||||
vp.set_canvas(self._viewer_3d)
|
vp.set_canvas(self._viewer_3d)
|
||||||
@@ -1357,6 +1358,15 @@ class MainWindow(QMainWindow):
|
|||||||
self._load_hull_viewers(hull, _skip_offsets_editor=True)
|
self._load_hull_viewers(hull, _skip_offsets_editor=True)
|
||||||
self.statusBar().showMessage(f"Offsets actualizados — {hull.name}")
|
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:
|
def _on_offsets_edited_from_viewer(self, offsets_table) -> None:
|
||||||
"""Slot: fin del drag — persistir + actualizar 3D + hidrostáticos.
|
"""Slot: fin del drag — persistir + actualizar 3D + hidrostáticos.
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import logging
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PySide6.QtCore import Qt, QTimer
|
from PySide6.QtCore import Qt, QTimer, Signal
|
||||||
from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget
|
from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget
|
||||||
|
|
||||||
logger = logging.getLogger("ui.viewer_3d")
|
logger = logging.getLogger("ui.viewer_3d")
|
||||||
@@ -50,6 +50,8 @@ class Viewer3DWidget(QWidget):
|
|||||||
>>> v.reset_camera()
|
>>> v.reset_camera()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
ready: Signal = Signal()
|
||||||
|
|
||||||
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._plotter: Optional["QtInteractor"] = None
|
self._plotter: Optional["QtInteractor"] = None
|
||||||
@@ -114,6 +116,7 @@ class Viewer3DWidget(QWidget):
|
|||||||
else:
|
else:
|
||||||
self._load_default_wigley()
|
self._load_default_wigley()
|
||||||
self._ready = True
|
self._ready = True
|
||||||
|
self.ready.emit()
|
||||||
logger.info("Viewer3DWidget: QtInteractor iniciado correctamente")
|
logger.info("Viewer3DWidget: QtInteractor iniciado correctamente")
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -365,6 +365,25 @@ def _draw_cnet_planview(p: QPainter, ot, w2s_fn) -> None:
|
|||||||
p.drawPath(path)
|
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
|
# 1. Body Plan — secciones transversales
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -475,14 +494,20 @@ class BodyPlanViewer(_BaseViewer):
|
|||||||
z_arr = ot.z_waterlines
|
z_arr = ot.z_waterlines
|
||||||
sign = 1.0 if is_fwd else -1.0
|
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()
|
path = QPainterPath()
|
||||||
for k, (y, z) in enumerate(zip(y_arr, z_arr)):
|
for k_pt in range(len(smooth)):
|
||||||
pt = self._w2s(sign * y, z)
|
pt = self._w2s(smooth[k_pt, 0], smooth[k_pt, 1])
|
||||||
if k == 0:
|
if k_pt == 0:
|
||||||
path.moveTo(pt)
|
path.moveTo(pt)
|
||||||
else:
|
else:
|
||||||
path.lineTo(pt)
|
path.lineTo(pt)
|
||||||
path.lineTo(self._w2s(0.0, 0.0))
|
|
||||||
p.drawPath(path)
|
p.drawPath(path)
|
||||||
|
|
||||||
# Flotación de diseño (encima de todo lo anterior)
|
# Flotación de diseño (encima de todo lo anterior)
|
||||||
@@ -691,15 +716,20 @@ class PlanViewer(_BaseViewer):
|
|||||||
width = 2.2
|
width = 2.2
|
||||||
else:
|
else:
|
||||||
color = QColor(_WATERLINE)
|
color = QColor(_WATERLINE)
|
||||||
color.setAlphaF(0.40 + 0.50 * frac)
|
color.setAlphaF(0.45 + 0.45 * frac)
|
||||||
width = 1.1
|
width = 1.2
|
||||||
|
|
||||||
p.setPen(QPen(color, width))
|
p.setPen(QPen(color, width))
|
||||||
p.setBrush(Qt.BrushStyle.NoBrush)
|
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()
|
path = QPainterPath()
|
||||||
for i, (x, y) in enumerate(zip(ot.x_stations, ot.data[:, j])):
|
for k_pt in range(len(smooth)):
|
||||||
pt = self._w2s(x, y)
|
pt = self._w2s(smooth[k_pt, 0], smooth[k_pt, 1])
|
||||||
if i == 0:
|
if k_pt == 0:
|
||||||
path.moveTo(pt)
|
path.moveTo(pt)
|
||||||
else:
|
else:
|
||||||
path.lineTo(pt)
|
path.lineTo(pt)
|
||||||
|
|||||||
Reference in New Issue
Block a user