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
+77 -48
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)
_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,34 +338,47 @@ 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 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], ot.data[i, j])
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):
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(
ot, y_b: float
@@ -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]])
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))