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:
@@ -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,34 +338,47 @@ 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 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):
|
for i in range(n_sta):
|
||||||
path = QPainterPath()
|
path = QPainterPath()
|
||||||
for j in range(n_wl):
|
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:
|
if j == 0:
|
||||||
path.moveTo(pt)
|
path.moveTo(pt)
|
||||||
else:
|
else:
|
||||||
path.lineTo(pt)
|
path.lineTo(pt)
|
||||||
p.drawPath(path)
|
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(
|
def _compute_buttock_pts(
|
||||||
ot, y_b: float
|
ot, y_b: float
|
||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user