diff --git a/arshipdesign/ui/widgets/viewer_lines.py b/arshipdesign/ui/widgets/viewer_lines.py index d889651..3a872d2 100644 --- a/arshipdesign/ui/widgets/viewer_lines.py +++ b/arshipdesign/ui/widgets/viewer_lines.py @@ -44,28 +44,30 @@ _GRID_STA = QColor(38, 55, 88, 80) # líneas de estación _GRID_WL = QColor(40, 60, 95, 70) # líneas de agua (referencia) _AXIS = QColor("#3e4255") -# ── Malla de control (control net) — thin, muted ─────────────────────── -# Capa intermedia entre grilla y curvas del casco. -# Conecta los nodos formando el poliedro de control. -_CNET_TRAN = QColor(50, 80, 130, 140) # aristas transversales (a lo largo de estación) -_CNET_LONG = QColor(35, 90, 80, 110) # aristas longitudinales (a lo largo de LdA) +# ── Malla de control (poliedro de control) — gris neutro, muy tenue ──── +# Los nodos SON los vértices del polígono de control; la curva del casco +# pasa CERCA de ellos (interpolante aquí, aproximante en NURBS clásico). +# El gris neutro evita confusión con las curvas de casco (verde/ámbar/azul). +_CNET_TRAN = QColor(110, 120, 140, 90) # aristas (dirección estación) +_CNET_LONG = QColor(100, 112, 132, 75) # aristas (dirección LdA) # ── Curvas del casco (sobre la malla) ────────────────────────────────── -_WATERLINE = QColor("#2a82c0") # líneas de agua -_WL_DESIGN = QColor("#00ccff") # flotación de diseño -_SECTION = QColor("#3a9e52") # secciones de proa -_SECTION_AFT = QColor("#2a78c0") # secciones de popa -_MIDSHIP = QColor("#d89020") # cuaderna maestra +_WATERLINE = QColor("#2878C8") # líneas de agua — azul +_WL_DESIGN = QColor("#00D0FF") # flotación de diseño — cian +_SECTION = QColor("#22CC58") # estaciones de proa — VERDE +_SECTION_AFT = QColor("#C8A010") # estaciones de popa — ÁMBAR +_MIDSHIP = QColor("#FF7020") # cuaderna maestra — naranja _DECK = QColor("#7058b8") # cubierta _KEEL = QColor("#c85858") # quilla _TEXT = QColor("#7a8ba8") -# ── Nodos (handles) — encima de todo, color único: NARANJA ───────────── -# El naranja no existe en ninguna curva del casco → cero ambigüedad. -_NODE_NORMAL = QColor("#FF8000") # naranja: estado de reposo -_NODE_HOVER = QColor("#FFD700") # oro: hover -_NODE_DRAG = QColor("#FF2020") # rojo vivo: arrastrando -_NODE_R = 4.5 # px semi-lado del cuadrado +# ── Nodos (vértices del polígono de control) — gris-azulado ──────────── +# Gris claro = convenio DELFTship/Maxsurf para puntos de control. +# Pequeño (3 px) para no tapar las curvas del casco. +_NODE_NORMAL = QColor("#A8B8D0") # gris-azulado: reposo +_NODE_HOVER = QColor("#E0EAFF") # casi blanco: hover +_NODE_DRAG = QColor("#FF3838") # rojo: arrastrando +_NODE_R = 3.0 # px semi-lado (era 4.5) _CPT_HIT = 16.0 # px umbral de captura (alias legacy) _CPT_RADIUS = _NODE_R # alias legacy @@ -336,33 +338,46 @@ def _draw_cnet_bodyplan(p: QPainter, ot, w2s_fn) -> None: def _draw_cnet_planview(p: QPainter, ot, w2s_fn) -> None: - """Dibuja la malla de control en la Vista de Planta. + """Dibuja el poliedro de control completo en la Vista de Planta. - Solo aristas TRANSVERSALES: por cada estación i, una polilínea - vertical que conecta sus nodos a lo largo de todas las LdA - (x constante, y varía de 0 a manga máxima en esa estación). - Esto muestra claramente «este nodo pertenece a esta estación» y - distingue los nodos longitudinales (en la waterline) de los - transversales (en la estación). + Se dibujan DOS direcciones (igual que DELFTship): + • Dirección estación (aristas verticales en planta): misma estación, + distintas LdA → muestra cómo varía la manga con la altura. + • Dirección LdA (aristas horizontales en planta): misma LdA, distintas + estaciones → el polígono de control de la línea de agua. - Las aristas longitudinales (waterlines) se omiten aquí porque la - Capa 3 ya las dibuja como las propias curvas del casco, más bold. + Ambas direcciones se dibujan en BABOR y ESTRIBOR (simetría). + La Capa 3 superpone las curvas del casco en colores saturados encima, + lo que hace visualmente evidente la diferencia poliedro ↔ curva suave. """ n_sta = ot.n_stations n_wl = ot.n_waterlines - - pen_t = QPen(_CNET_TRAN, 0.7, Qt.PenStyle.SolidLine) - p.setPen(pen_t) + pen = QPen(_CNET_TRAN, 0.7, Qt.PenStyle.SolidLine) + p.setPen(pen) p.setBrush(Qt.BrushStyle.NoBrush) - for i in range(n_sta): - path = QPainterPath() + + for sign in (1.0, -1.0): # estribor (+) y babor (−) + # ── Dirección estación: nodos de la misma estación a lo largo de LdA + for i in range(n_sta): + path = QPainterPath() + for j in range(n_wl): + pt = w2s_fn(ot.x_stations[i], sign * ot.data[i, j]) + if j == 0: + path.moveTo(pt) + else: + path.lineTo(pt) + p.drawPath(path) + + # ── Dirección LdA: nodos de la misma LdA a lo largo de estaciones for j in range(n_wl): - pt = w2s_fn(ot.x_stations[i], ot.data[i, j]) - if j == 0: - path.moveTo(pt) - else: - path.lineTo(pt) - p.drawPath(path) + path = QPainterPath() + for i in range(n_sta): + pt = w2s_fn(ot.x_stations[i], sign * ot.data[i, j]) + if i == 0: + path.moveTo(pt) + else: + path.lineTo(pt) + p.drawPath(path) def _compute_buttock_pts( @@ -792,11 +807,12 @@ class PlanViewer(_BaseViewer): if self._hull is None: return None y_max = self._hull.offsets.max_half_breadth + # Mostrar AMBOS semiplanos (estribor + babor) simétricamente return ( -self._hull.lpp * 0.05, - -y_max * 0.15, + -y_max * 1.22, self._hull.lpp * 1.05, - y_max * 1.25, + y_max * 1.22, ) # ── Edición ─────────────────────────────────────────────────────────────── @@ -842,19 +858,24 @@ class PlanViewer(_BaseViewer): y_max = ot.max_half_breadth # ══ CAPA 1: Grilla de referencia ══════════════════════════════ - # Estaciones — líneas verticales tenues - p.setPen(QPen(_GRID_STA, 0.5, Qt.PenStyle.DotLine)) - for x in ot.x_stations: - p.drawLine(self._w2s(x, 0), self._w2s(x, y_max * 1.15)) - - # Eje de crujía - p.setPen(QPen(_AXIS, 0.8, Qt.PenStyle.DashLine)) + # Eje de crujía — línea continua que divide babor y estribor + p.setPen(QPen(_AXIS, 1.2)) p.drawLine(self._w2s(0, 0), self._w2s(self._hull.lpp, 0)) - # ══ CAPA 2: Malla de control ══════════════════════════════════ + # Estaciones — líneas verticales en AMBOS semiplanos + p.setPen(QPen(_GRID_STA, 0.5, Qt.PenStyle.DotLine)) + for x in ot.x_stations: + p.drawLine(self._w2s(x, -y_max * 1.10), self._w2s(x, y_max * 1.10)) + + # ══ CAPA 2: Poliedro de control (ambas mitades) ════════════════ _draw_cnet_planview(p, ot, self._w2s) - # ══ CAPA 3: Curvas del casco (waterlines como contornos) ══════ + # ══ CAPA 3: Líneas de agua (ambos semiplanos) ══════════════════ + # Cada línea de agua se dibuja como contorno cerrado: + # eje de crujía (AP) → semi-manga estribor → eje crujía (FP) + # → semi-manga babor → eje crujía (AP) + # Las curvas se cierran en el eje de crujía porque el casco es + # simétrico y la línea de agua termina en y=0 en AP y FP. for j in range(n_wl): z = ot.z_waterlines[j] frac = j / max(n_wl - 1, 1) @@ -871,20 +892,28 @@ class PlanViewer(_BaseViewer): 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]]) + raw = np.column_stack([ot.x_stations, ot.data[:, j]]) smooth = _smooth_pts(raw, n=80) + n_smo = len(smooth) + # Coordenadas del eje de crujía en AP y FP (donde la LdA termina) + ap_x = float(smooth[0, 0]) + fp_x = float(smooth[-1, 0]) + + # Semiplano estribor (y > 0) + cierre → semiplano babor (y < 0) path = QPainterPath() - 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.moveTo(self._w2s(ap_x, 0.0)) # inicio en CL-AP + for k in range(n_smo): # estribor: AP→FP + path.lineTo(self._w2s(float(smooth[k, 0]), float(smooth[k, 1]))) + path.lineTo(self._w2s(fp_x, 0.0)) # cierre CL-FP + for k in range(n_smo - 1, -1, -1): # babor: FP→AP + path.lineTo(self._w2s(float(smooth[k, 0]), -float(smooth[k, 1]))) + path.closeSubpath() # cierre CL-AP p.drawPath(path) - # ══ CAPA 4: Nodos (cuadrados naranjas) ════════════════════════ + # ══ CAPA 4: Nodos (estribor — lado editable) ══════════════════ + # Solo se muestran los nodos del semiplano estribor (positivo). + # Babor es simétrico → editar un nodo actualiza ambos lados. for i in range(ot.n_stations): for j in range(n_wl): self._draw_control_point(p, self._screen_pt(i, j), (i, j))