Lines plan visual overhaul: correct colors, full plan view symmetry

Color palette:
- Control net (nodes + connecting lines): neutral grey (#A8B8D0 / 110,120,140)
  — matches DELFTship convention for control polygon handles
- Forward stations (proa): bright green #22CC58
- Aft stations (popa): amber #C8A010
- Midship: orange #FF7020
- Node size reduced 4.5→3.0 px so hull curves dominate visually

Plan view (Vista de Planta):
- World bbox now symmetric: y ∈ [−B/2·1.22, +B/2·1.22] shows BOTH halves
- Waterlines drawn as closed contours: CL-AP → starboard curve → CL-FP
  → port curve (mirrored) → close at CL-AP
  Every waterline terminates at the centerline at bow and stern
- Control net grid: both directions (station-arm + waterline-arm) drawn
  on port AND starboard — same visual language as DELFTship control polygon
- Station reference lines span full beam (both sides)
- Centerline (eje de crujía) drawn as solid line dividing the two halves
- Edit nodes remain on starboard only; port updates symmetrically

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 21:05:20 -04:00
parent 0cf7df2763
commit 36584f782c
+85 -56
View File
@@ -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) _GRID_WL = QColor(40, 60, 95, 70) # líneas de agua (referencia)
_AXIS = QColor("#3e4255") _AXIS = QColor("#3e4255")
# ── Malla de control (control net) — thin, muted ─────────────────────── # ── Malla de control (poliedro de control) — gris neutro, muy tenue ────
# Capa intermedia entre grilla y curvas del casco. # Los nodos SON los vértices del polígono de control; la curva del casco
# Conecta los nodos formando el poliedro de control. # pasa CERCA de ellos (interpolante aquí, aproximante en NURBS clásico).
_CNET_TRAN = QColor(50, 80, 130, 140) # aristas transversales (a lo largo de estación) # El gris neutro evita confusión con las curvas de casco (verde/ámbar/azul).
_CNET_LONG = QColor(35, 90, 80, 110) # aristas longitudinales (a lo largo de LdA) _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) ────────────────────────────────── # ── Curvas del casco (sobre la malla) ──────────────────────────────────
_WATERLINE = QColor("#2a82c0") # líneas de agua _WATERLINE = QColor("#2878C8") # líneas de agua — azul
_WL_DESIGN = QColor("#00ccff") # flotación de diseño _WL_DESIGN = QColor("#00D0FF") # flotación de diseño — cian
_SECTION = QColor("#3a9e52") # secciones de proa _SECTION = QColor("#22CC58") # estaciones de proa — VERDE
_SECTION_AFT = QColor("#2a78c0") # secciones de popa _SECTION_AFT = QColor("#C8A010") # estaciones de popa — ÁMBAR
_MIDSHIP = QColor("#d89020") # cuaderna maestra _MIDSHIP = QColor("#FF7020") # cuaderna maestra — naranja
_DECK = QColor("#7058b8") # cubierta _DECK = QColor("#7058b8") # cubierta
_KEEL = QColor("#c85858") # quilla _KEEL = QColor("#c85858") # quilla
_TEXT = QColor("#7a8ba8") _TEXT = QColor("#7a8ba8")
# ── Nodos (handles) — encima de todo, color único: NARANJA ───────────── # ── Nodos (vértices del polígono de control) — gris-azulado ────────────
# El naranja no existe en ninguna curva del casco → cero ambigüedad. # Gris claro = convenio DELFTship/Maxsurf para puntos de control.
_NODE_NORMAL = QColor("#FF8000") # naranja: estado de reposo # Pequeño (3 px) para no tapar las curvas del casco.
_NODE_HOVER = QColor("#FFD700") # oro: hover _NODE_NORMAL = QColor("#A8B8D0") # gris-azulado: reposo
_NODE_DRAG = QColor("#FF2020") # rojo vivo: arrastrando _NODE_HOVER = QColor("#E0EAFF") # casi blanco: hover
_NODE_R = 4.5 # px semi-lado del cuadrado _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_HIT = 16.0 # px umbral de captura (alias legacy)
_CPT_RADIUS = _NODE_R # 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: 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 Se dibujan DOS direcciones (igual que DELFTship):
vertical que conecta sus nodos a lo largo de todas las LdA • Dirección estación (aristas verticales en planta): misma estación,
(x constante, y varía de 0 a manga máxima en esa estación). distintas LdA → muestra cómo varía la manga con la altura.
Esto muestra claramente «este nodo pertenece a esta estación» y • Dirección LdA (aristas horizontales en planta): misma LdA, distintas
distingue los nodos longitudinales (en la waterline) de los estaciones → el polígono de control de la línea de agua.
transversales (en la estación).
Las aristas longitudinales (waterlines) se omiten aquí porque la Ambas direcciones se dibujan en BABOR y ESTRIBOR (simetría).
Capa 3 ya las dibuja como las propias curvas del casco, más bold. 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_sta = ot.n_stations
n_wl = ot.n_waterlines n_wl = ot.n_waterlines
pen = QPen(_CNET_TRAN, 0.7, Qt.PenStyle.SolidLine)
pen_t = QPen(_CNET_TRAN, 0.7, Qt.PenStyle.SolidLine) p.setPen(pen)
p.setPen(pen_t)
p.setBrush(Qt.BrushStyle.NoBrush) 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): for j in range(n_wl):
pt = w2s_fn(ot.x_stations[i], ot.data[i, j]) path = QPainterPath()
if j == 0: for i in range(n_sta):
path.moveTo(pt) pt = w2s_fn(ot.x_stations[i], sign * ot.data[i, j])
else: if i == 0:
path.lineTo(pt) path.moveTo(pt)
p.drawPath(path) else:
path.lineTo(pt)
p.drawPath(path)
def _compute_buttock_pts( def _compute_buttock_pts(
@@ -792,11 +807,12 @@ class PlanViewer(_BaseViewer):
if self._hull is None: if self._hull is None:
return None return None
y_max = self._hull.offsets.max_half_breadth y_max = self._hull.offsets.max_half_breadth
# Mostrar AMBOS semiplanos (estribor + babor) simétricamente
return ( return (
-self._hull.lpp * 0.05, -self._hull.lpp * 0.05,
-y_max * 0.15, -y_max * 1.22,
self._hull.lpp * 1.05, self._hull.lpp * 1.05,
y_max * 1.25, y_max * 1.22,
) )
# ── Edición ─────────────────────────────────────────────────────────────── # ── Edición ───────────────────────────────────────────────────────────────
@@ -842,19 +858,24 @@ class PlanViewer(_BaseViewer):
y_max = ot.max_half_breadth y_max = ot.max_half_breadth
# ══ CAPA 1: Grilla de referencia ══════════════════════════════ # ══ CAPA 1: Grilla de referencia ══════════════════════════════
# Estaciones — líneas verticales tenues # Eje de crujía — línea continua que divide babor y estribor
p.setPen(QPen(_GRID_STA, 0.5, Qt.PenStyle.DotLine)) p.setPen(QPen(_AXIS, 1.2))
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))
p.drawLine(self._w2s(0, 0), self._w2s(self._hull.lpp, 0)) 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) _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): for j in range(n_wl):
z = ot.z_waterlines[j] z = ot.z_waterlines[j]
frac = j / max(n_wl - 1, 1) frac = j / max(n_wl - 1, 1)
@@ -871,20 +892,28 @@ class PlanViewer(_BaseViewer):
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]])
raw = np.column_stack([ot.x_stations, ot.data[:, j]])
smooth = _smooth_pts(raw, n=80) 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() path = QPainterPath()
for k_pt in range(len(smooth)): path.moveTo(self._w2s(ap_x, 0.0)) # inicio en CL-AP
pt = self._w2s(smooth[k_pt, 0], smooth[k_pt, 1]) for k in range(n_smo): # estribor: AP→FP
if k_pt == 0: path.lineTo(self._w2s(float(smooth[k, 0]), float(smooth[k, 1])))
path.moveTo(pt) path.lineTo(self._w2s(fp_x, 0.0)) # cierre CL-FP
else: for k in range(n_smo - 1, -1, -1): # babor: FP→AP
path.lineTo(pt) path.lineTo(self._w2s(float(smooth[k, 0]), -float(smooth[k, 1])))
path.closeSubpath() # cierre CL-AP
p.drawPath(path) 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 i in range(ot.n_stations):
for j in range(n_wl): for j in range(n_wl):
self._draw_control_point(p, self._screen_pt(i, j), (i, j)) self._draw_control_point(p, self._screen_pt(i, j), (i, j))