feat: AR-VMS-Seaman initial commit — Python FastAPI + PySide6 (runtime server + desktop studio client)
This commit is contained in:
@@ -214,37 +214,106 @@ class VesselCanvas(QWidget):
|
||||
self._scene.addLine(0, rect.top(), 0, rect.bottom(), cl_pen)
|
||||
|
||||
def _draw_vessel_plan(self, vessel) -> None:
|
||||
"""Silueta del buque en vista de planta.
|
||||
|
||||
Eje X de escena = x_pp (popa en 0, proa positiva).
|
||||
Eje Y de escena = -y_cl (estribor en y negativo, babor en y positivo).
|
||||
|
||||
Forma realista de yacht motor planeo:
|
||||
- Popa cuadrada (transom)
|
||||
- Sección paralela media (max beam de ~25% a ~70% LOA)
|
||||
- Proa puntiaguda con curva tipo bow flare
|
||||
- Casco simétrico babor/estribor
|
||||
"""
|
||||
from PySide6.QtGui import QPainterPath
|
||||
|
||||
L = vessel.length_overall_m * self._px_per_m
|
||||
B = vessel.beam_max_m * self._px_per_m
|
||||
b2 = B / 2
|
||||
|
||||
poly = QPolygonF(
|
||||
[
|
||||
# Stern (popa) — square-ish
|
||||
QPointF(0, -b2 * 0.85),
|
||||
QPointF(0, b2 * 0.85),
|
||||
# Mid-stern out to full beam
|
||||
QPointF(L * 0.18, b2),
|
||||
# Parallel mid-body
|
||||
QPointF(L * 0.70, b2),
|
||||
# Bow taper
|
||||
QPointF(L * 0.92, b2 * 0.55),
|
||||
QPointF(L, 0),
|
||||
QPointF(L * 0.92, -b2 * 0.55),
|
||||
QPointF(L * 0.70, -b2),
|
||||
QPointF(L * 0.18, -b2),
|
||||
]
|
||||
)
|
||||
# ----- Casco (hull outline) usando bezier para curvas suaves -----
|
||||
path = QPainterPath()
|
||||
# Stern transom (esquina popa estribor)
|
||||
path.moveTo(0, -b2 * 0.90)
|
||||
# Quarter aft (popa-estribor) — beam crece rápido
|
||||
path.cubicTo(L * 0.05, -b2 * 0.96, L * 0.12, -b2, L * 0.22, -b2)
|
||||
# Sección paralela estribor
|
||||
path.lineTo(L * 0.62, -b2)
|
||||
# Forward shoulder (transición a la proa)
|
||||
path.cubicTo(L * 0.78, -b2 * 0.95, L * 0.90, -b2 * 0.60, L * 0.97, -b2 * 0.18)
|
||||
# Punta de proa (curvatura aguda)
|
||||
path.cubicTo(L * 1.005, -b2 * 0.05, L * 1.005, b2 * 0.05, L * 0.97, b2 * 0.18)
|
||||
# Forward shoulder babor (espejo)
|
||||
path.cubicTo(L * 0.90, b2 * 0.60, L * 0.78, b2 * 0.95, L * 0.62, b2)
|
||||
# Sección paralela babor
|
||||
path.lineTo(L * 0.22, b2)
|
||||
# Quarter aft babor
|
||||
path.cubicTo(L * 0.12, b2, L * 0.05, b2 * 0.96, 0, b2 * 0.90)
|
||||
# Transom (popa rectangular)
|
||||
path.lineTo(0, -b2 * 0.90)
|
||||
path.closeSubpath()
|
||||
|
||||
# Casco gris claro con gradiente top-down (azul claro centro)
|
||||
grad = QLinearGradient(0, -b2, 0, b2)
|
||||
grad.setColorAt(0.0, QColor(C_SAND))
|
||||
grad.setColorAt(0.5, QColor("#94A3B8"))
|
||||
grad.setColorAt(1.0, QColor(C_FOG))
|
||||
brush = QBrush(grad)
|
||||
grad.setColorAt(0.0, QColor("#D4DBE5"))
|
||||
grad.setColorAt(0.5, QColor("#E6EAF0"))
|
||||
grad.setColorAt(1.0, QColor("#D4DBE5"))
|
||||
pen = QPen(QColor(C_ABYSS))
|
||||
pen.setWidthF(2)
|
||||
pen.setCosmetic(True)
|
||||
self._scene.addPolygon(poly, pen, brush)
|
||||
self._scene.addPath(path, pen, QBrush(grad))
|
||||
|
||||
# ----- Superestructura (visible desde planta) -----
|
||||
super_path = QPainterPath()
|
||||
# Cabina principal (main deck), centrada longitudinalmente
|
||||
sx0 = L * 0.30
|
||||
sx1 = L * 0.72
|
||||
sb = b2 * 0.55
|
||||
super_path.moveTo(sx0, -sb)
|
||||
super_path.lineTo(sx1 - 8, -sb)
|
||||
super_path.cubicTo(sx1, -sb, sx1 + 6, -sb * 0.5, sx1 + 6, 0)
|
||||
super_path.cubicTo(sx1 + 6, sb * 0.5, sx1, sb, sx1 - 8, sb)
|
||||
super_path.lineTo(sx0, sb)
|
||||
super_path.cubicTo(sx0 - 6, sb, sx0 - 8, sb * 0.5, sx0 - 8, 0)
|
||||
super_path.cubicTo(sx0 - 8, -sb * 0.5, sx0 - 6, -sb, sx0, -sb)
|
||||
super_path.closeSubpath()
|
||||
super_brush = QBrush(QColor("#F2F5F9"))
|
||||
super_pen = QPen(QColor(C_ABYSS))
|
||||
super_pen.setWidthF(1.2)
|
||||
super_pen.setCosmetic(True)
|
||||
self._scene.addPath(super_path, super_pen, super_brush)
|
||||
|
||||
# ----- Flybridge superior (más estrecho) -----
|
||||
fb_path = QPainterPath()
|
||||
fbx0 = L * 0.42
|
||||
fbx1 = L * 0.66
|
||||
fbb = b2 * 0.38
|
||||
fb_path.addRoundedRect(fbx0, -fbb, fbx1 - fbx0, 2 * fbb, 6, 6)
|
||||
fb_pen = QPen(QColor(C_ABYSS))
|
||||
fb_pen.setWidthF(1.0)
|
||||
fb_pen.setCosmetic(True)
|
||||
self._scene.addPath(fb_path, fb_pen, QBrush(QColor("#FFFFFF")))
|
||||
|
||||
# Mástil / radar arch
|
||||
mast = self._scene.addEllipse(
|
||||
L * 0.54 - 3, -3, 6, 6,
|
||||
QPen(QColor(C_CYAN), 1.5), QBrush(QColor(C_CYAN))
|
||||
)
|
||||
_ = mast
|
||||
|
||||
# ----- Ventanas / windscreen (línea cyan brillante) -----
|
||||
win_pen = QPen(QColor(C_CYAN))
|
||||
win_pen.setWidthF(1.5)
|
||||
win_pen.setCosmetic(True)
|
||||
self._scene.addLine(sx1 - 6, -sb * 0.3, sx1 + 4, 0, win_pen)
|
||||
self._scene.addLine(sx1 + 4, 0, sx1 - 6, sb * 0.3, win_pen)
|
||||
|
||||
# ----- Centerline (línea de crujía, referencia) -----
|
||||
cl_pen = QPen(QColor(C_CYAN_DEEP))
|
||||
cl_pen.setWidthF(0.7)
|
||||
cl_pen.setStyle(Qt.DashLine)
|
||||
cl_pen.setCosmetic(True)
|
||||
self._scene.addLine(0, 0, L, 0, cl_pen)
|
||||
|
||||
def _draw_bulkhead(self, x_px: float, beam_m: float, name: str) -> None:
|
||||
b2 = beam_m * self._px_per_m / 2
|
||||
@@ -258,39 +327,234 @@ class VesselCanvas(QWidget):
|
||||
text.setPos(x_px - text.boundingRect().width() / 2, b2 + 6)
|
||||
|
||||
def _draw_equipment(self, eq) -> None:
|
||||
"""Dibuja un equipo con silueta característica según su sistema."""
|
||||
center = ship_to_scene(eq.location, self._px_per_m)
|
||||
|
||||
# color por sistema (simplificado: motor cyan, genset amber, otros sand)
|
||||
sys_v = eq.system_id.value
|
||||
if sys_v == "main_engine":
|
||||
color = QColor(C_CYAN)
|
||||
elif sys_v == "genset":
|
||||
color = QColor(C_WARN)
|
||||
else:
|
||||
color = QColor(C_SAND)
|
||||
|
||||
# Halo (the returned QGraphicsItem stays owned by the scene)
|
||||
if sys_v == "main_engine":
|
||||
self._draw_main_engine_icon(center, eq.tag_prefix)
|
||||
elif sys_v == "genset":
|
||||
self._draw_genset_icon(center, eq.tag_prefix)
|
||||
elif sys_v in ("fuel_tanks", "water_tanks", "fuel", "potable_water", "grey_black_tanks"):
|
||||
self._draw_tank_icon(center, eq.tag_prefix)
|
||||
elif sys_v in ("bilge", "fw_cooling", "sw_cooling", "lube_oil", "hydraulic_oil"):
|
||||
self._draw_pump_icon(center, eq.tag_prefix)
|
||||
elif sys_v == "thruster":
|
||||
self._draw_thruster_icon(center, eq.tag_prefix)
|
||||
elif sys_v in ("hvac", "engine_vent", "refrigeration", "heating"):
|
||||
self._draw_hvac_icon(center, eq.tag_prefix)
|
||||
else:
|
||||
self._draw_generic_icon(center, eq.tag_prefix, C_SAND)
|
||||
|
||||
# ----- Iconos de equipos --------------------------------------------
|
||||
|
||||
def _label_below(self, cx: float, cy: float, text: str, dy: float = 18.0) -> None:
|
||||
t = self._scene.addText(text, ui_font(8))
|
||||
t.setDefaultTextColor(QColor(C_FOAM))
|
||||
rect = t.boundingRect()
|
||||
t.setPos(cx - rect.width() / 2, cy + dy)
|
||||
|
||||
def _draw_main_engine_icon(self, center, prefix: str) -> None:
|
||||
"""Motor diésel marino visto desde arriba: bloque + 2 turbos + alternador."""
|
||||
from PySide6.QtCore import QRectF
|
||||
from PySide6.QtGui import QPainterPath
|
||||
cx, cy = center.x(), center.y()
|
||||
w, h = 34.0, 18.0
|
||||
# Block (cuerpo principal)
|
||||
block_pen = QPen(QColor(C_ABYSS))
|
||||
block_pen.setWidthF(1.5)
|
||||
block_pen.setCosmetic(True)
|
||||
grad = QLinearGradient(0, cy - h / 2, 0, cy + h / 2)
|
||||
grad.setColorAt(0.0, QColor(C_CYAN))
|
||||
grad.setColorAt(1.0, QColor(C_CYAN_DEEP))
|
||||
block_path = QPainterPath()
|
||||
block_path.addRoundedRect(cx - w / 2, cy - h / 2, w, h, 3, 3)
|
||||
self._scene.addPath(block_path, block_pen, QBrush(grad))
|
||||
# Cilindros (V12: 6 puntos cada lado)
|
||||
cyl_pen = QPen(QColor(C_ABYSS))
|
||||
cyl_pen.setWidthF(0.8)
|
||||
cyl_pen.setCosmetic(True)
|
||||
cyl_brush = QBrush(QColor(C_FOAM))
|
||||
for i in range(6):
|
||||
x = cx - w / 2 + 3 + i * (w - 6) / 5
|
||||
# cilindros estribor (arriba)
|
||||
self._scene.addEllipse(x - 1.5, cy - h / 2 + 2, 3, 3, cyl_pen, cyl_brush)
|
||||
# cilindros babor (abajo)
|
||||
self._scene.addEllipse(x - 1.5, cy + h / 2 - 5, 3, 3, cyl_pen, cyl_brush)
|
||||
# Turbos (popa = lado izq, x menor)
|
||||
turbo_pen = QPen(QColor(C_ABYSS))
|
||||
turbo_pen.setWidthF(1.0)
|
||||
turbo_pen.setCosmetic(True)
|
||||
self._scene.addEllipse(
|
||||
center.x() - 14,
|
||||
center.y() - 14,
|
||||
28,
|
||||
28,
|
||||
QPen(color, 1.2, Qt.SolidLine),
|
||||
QBrush(QColor(color.red(), color.green(), color.blue(), 40)),
|
||||
cx - w / 2 - 4, cy - 4, 5, 5, turbo_pen, QBrush(QColor("#94A3B8"))
|
||||
)
|
||||
# Dot
|
||||
# Alternador (proa, lado derecho)
|
||||
self._scene.addRect(
|
||||
cx + w / 2 - 1, cy - 3, 5, 6, turbo_pen, QBrush(QColor("#94A3B8"))
|
||||
)
|
||||
# Halo de selección sutil
|
||||
halo_pen = QPen(QColor(C_CYAN))
|
||||
halo_pen.setWidthF(0.6)
|
||||
halo_pen.setStyle(Qt.DashLine)
|
||||
halo_pen.setCosmetic(True)
|
||||
halo_path = QPainterPath()
|
||||
halo_path.addRoundedRect(cx - w / 2 - 6, cy - h / 2 - 4, w + 12, h + 8, 6, 6)
|
||||
self._scene.addPath(halo_path, halo_pen, QBrush(Qt.NoBrush))
|
||||
self._label_below(cx, cy, prefix, dy=h / 2 + 8)
|
||||
|
||||
def _draw_genset_icon(self, center, prefix: str) -> None:
|
||||
"""Genset con cabina silenciosa: rectángulo grande + ventilación + chimenea."""
|
||||
from PySide6.QtGui import QPainterPath
|
||||
cx, cy = center.x(), center.y()
|
||||
w, h = 28.0, 16.0
|
||||
pen = QPen(QColor(C_ABYSS))
|
||||
pen.setWidthF(1.5)
|
||||
pen.setCosmetic(True)
|
||||
grad = QLinearGradient(0, cy - h / 2, 0, cy + h / 2)
|
||||
grad.setColorAt(0.0, QColor(C_WARN))
|
||||
grad.setColorAt(1.0, QColor("#C0760F"))
|
||||
gp = QPainterPath()
|
||||
gp.addRoundedRect(cx - w / 2, cy - h / 2, w, h, 3, 3)
|
||||
self._scene.addPath(gp, pen, QBrush(grad))
|
||||
# Rejilla de ventilación (líneas verticales)
|
||||
vent_pen = QPen(QColor(C_ABYSS))
|
||||
vent_pen.setWidthF(0.6)
|
||||
vent_pen.setCosmetic(True)
|
||||
for i in range(5):
|
||||
x = cx + 2 + i * 3
|
||||
self._scene.addLine(x, cy - h / 2 + 3, x, cy + h / 2 - 3, vent_pen)
|
||||
# Chimenea (escape) — círculo pequeño en proa
|
||||
self._scene.addEllipse(
|
||||
center.x() - 6,
|
||||
center.y() - 6,
|
||||
12,
|
||||
12,
|
||||
QPen(QColor(C_ABYSS), 1.5),
|
||||
QBrush(color),
|
||||
cx - w / 2 + 3, cy - 2, 4, 4, pen, QBrush(QColor("#5A6B7F"))
|
||||
)
|
||||
# Label
|
||||
text = self._scene.addText(eq.tag_prefix, ui_font(8))
|
||||
text.setDefaultTextColor(QColor(C_FOAM))
|
||||
text.setPos(center.x() - text.boundingRect().width() / 2, center.y() + 12)
|
||||
# Símbolo de generación (rayo) en el centro
|
||||
from PySide6.QtGui import QPainterPath
|
||||
bolt = QPainterPath()
|
||||
bolt.moveTo(cx - 2, cy - 4)
|
||||
bolt.lineTo(cx + 1, cy - 1)
|
||||
bolt.lineTo(cx - 1, cy - 1)
|
||||
bolt.lineTo(cx + 2, cy + 4)
|
||||
bolt_pen = QPen(QColor(C_FOAM))
|
||||
bolt_pen.setWidthF(1.5)
|
||||
bolt_pen.setCosmetic(True)
|
||||
self._scene.addPath(bolt, bolt_pen, QBrush(Qt.NoBrush))
|
||||
self._label_below(cx, cy, prefix, dy=h / 2 + 8)
|
||||
|
||||
def _draw_pump_icon(self, center, prefix: str) -> None:
|
||||
"""Bomba centrífuga: círculo + voluta + descarga."""
|
||||
cx, cy = center.x(), center.y()
|
||||
r = 8.0
|
||||
pen = QPen(QColor(C_ABYSS))
|
||||
pen.setWidthF(1.5)
|
||||
pen.setCosmetic(True)
|
||||
self._scene.addEllipse(
|
||||
cx - r, cy - r, 2 * r, 2 * r, pen, QBrush(QColor("#5BC0EB"))
|
||||
)
|
||||
# Aspas en cruz
|
||||
aspas_pen = QPen(QColor(C_ABYSS))
|
||||
aspas_pen.setWidthF(1.5)
|
||||
aspas_pen.setCosmetic(True)
|
||||
self._scene.addLine(cx - r * 0.7, cy, cx + r * 0.7, cy, aspas_pen)
|
||||
self._scene.addLine(cx, cy - r * 0.7, cx, cy + r * 0.7, aspas_pen)
|
||||
# Tubería de descarga (sale por arriba)
|
||||
self._scene.addRect(cx - 2, cy - r - 4, 4, 4, pen, QBrush(QColor("#94A3B8")))
|
||||
self._label_below(cx, cy, prefix, dy=r + 4)
|
||||
|
||||
def _draw_tank_icon(self, center, prefix: str) -> None:
|
||||
"""Tanque visto desde planta: rectángulo redondeado con sub-divisiones."""
|
||||
from PySide6.QtGui import QPainterPath
|
||||
cx, cy = center.x(), center.y()
|
||||
w, h = 22.0, 14.0
|
||||
pen = QPen(QColor(C_ABYSS))
|
||||
pen.setWidthF(1.5)
|
||||
pen.setCosmetic(True)
|
||||
grad = QLinearGradient(0, cy - h / 2, 0, cy + h / 2)
|
||||
grad.setColorAt(0.0, QColor("#5BC0EB"))
|
||||
grad.setColorAt(1.0, QColor("#1B7FB5"))
|
||||
tp = QPainterPath()
|
||||
tp.addRoundedRect(cx - w / 2, cy - h / 2, w, h, 2, 2)
|
||||
self._scene.addPath(tp, pen, QBrush(grad))
|
||||
# Diaphragm interno
|
||||
diaph_pen = QPen(QColor(C_ABYSS))
|
||||
diaph_pen.setWidthF(0.6)
|
||||
diaph_pen.setStyle(Qt.DashLine)
|
||||
diaph_pen.setCosmetic(True)
|
||||
self._scene.addLine(cx, cy - h / 2 + 2, cx, cy + h / 2 - 2, diaph_pen)
|
||||
# Manhole circle (top)
|
||||
self._scene.addEllipse(cx - 2, cy - h / 2 - 1, 4, 4, pen, QBrush(QColor("#F2F5F9")))
|
||||
self._label_below(cx, cy, prefix, dy=h / 2 + 6)
|
||||
|
||||
def _draw_thruster_icon(self, center, prefix: str) -> None:
|
||||
"""Hélice de proa/popa: círculo con aspas radiales."""
|
||||
cx, cy = center.x(), center.y()
|
||||
r = 9.0
|
||||
pen = QPen(QColor(C_ABYSS))
|
||||
pen.setWidthF(1.5)
|
||||
pen.setCosmetic(True)
|
||||
self._scene.addEllipse(
|
||||
cx - r, cy - r, 2 * r, 2 * r, pen, QBrush(QColor("#3A6BA8"))
|
||||
)
|
||||
# 4 aspas radiales
|
||||
from PySide6.QtGui import QPainterPath
|
||||
aspas = QPainterPath()
|
||||
for ang in (0, 90, 180, 270):
|
||||
import math
|
||||
a = math.radians(ang)
|
||||
aspas.moveTo(cx, cy)
|
||||
aspas.lineTo(cx + r * 0.75 * math.cos(a), cy + r * 0.75 * math.sin(a))
|
||||
aspas_pen = QPen(QColor(C_FOAM))
|
||||
aspas_pen.setWidthF(1.6)
|
||||
aspas_pen.setCosmetic(True)
|
||||
self._scene.addPath(aspas, aspas_pen, QBrush(Qt.NoBrush))
|
||||
self._scene.addEllipse(cx - 2, cy - 2, 4, 4, pen, QBrush(QColor(C_FOAM)))
|
||||
self._label_below(cx, cy, prefix, dy=r + 4)
|
||||
|
||||
def _draw_hvac_icon(self, center, prefix: str) -> None:
|
||||
"""Unidad HVAC: rectángulo + aspas de ventilador en frente."""
|
||||
from PySide6.QtGui import QPainterPath
|
||||
cx, cy = center.x(), center.y()
|
||||
w, h = 18.0, 14.0
|
||||
pen = QPen(QColor(C_ABYSS))
|
||||
pen.setWidthF(1.5)
|
||||
pen.setCosmetic(True)
|
||||
hp = QPainterPath()
|
||||
hp.addRoundedRect(cx - w / 2, cy - h / 2, w, h, 2, 2)
|
||||
self._scene.addPath(hp, pen, QBrush(QColor("#B8C2D1")))
|
||||
# Ventilador (círculo con 3 aspas curvas)
|
||||
r = 5
|
||||
self._scene.addEllipse(
|
||||
cx - r, cy - r, 2 * r, 2 * r, pen, QBrush(QColor("#5A6B7F"))
|
||||
)
|
||||
aspas_pen = QPen(QColor(C_FOAM))
|
||||
aspas_pen.setWidthF(1.2)
|
||||
aspas_pen.setCosmetic(True)
|
||||
from PySide6.QtGui import QPainterPath
|
||||
for ang in (0, 120, 240):
|
||||
import math
|
||||
a = math.radians(ang)
|
||||
asp = QPainterPath()
|
||||
asp.moveTo(cx, cy)
|
||||
asp.quadTo(
|
||||
cx + r * 0.7 * math.cos(a + 0.6),
|
||||
cy + r * 0.7 * math.sin(a + 0.6),
|
||||
cx + r * 0.85 * math.cos(a),
|
||||
cy + r * 0.85 * math.sin(a),
|
||||
)
|
||||
self._scene.addPath(asp, aspas_pen, QBrush(Qt.NoBrush))
|
||||
self._label_below(cx, cy, prefix, dy=h / 2 + 5)
|
||||
|
||||
def _draw_generic_icon(self, center, prefix: str, color: str) -> None:
|
||||
"""Fallback genérico (rectángulo neutro) para sistemas no contemplados."""
|
||||
from PySide6.QtGui import QPainterPath
|
||||
cx, cy = center.x(), center.y()
|
||||
w, h = 14.0, 14.0
|
||||
pen = QPen(QColor(C_ABYSS))
|
||||
pen.setWidthF(1.2)
|
||||
pen.setCosmetic(True)
|
||||
gp = QPainterPath()
|
||||
gp.addRoundedRect(cx - w / 2, cy - h / 2, w, h, 3, 3)
|
||||
self._scene.addPath(gp, pen, QBrush(QColor(color)))
|
||||
self._label_below(cx, cy, prefix, dy=h / 2 + 4)
|
||||
|
||||
def _draw_axes(self, length_m: float, beam_m: float) -> None:
|
||||
# Ruler X
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
"""Welcome screen del Studio.
|
||||
|
||||
Pantalla inicial cuando no hay proyecto abierto. Muestra al usuario las dos
|
||||
acciones principales (crear nuevo / abrir existente) como botones grandes,
|
||||
más una lista de proyectos recientes y enlaces a docs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from vmssailor.studio.theme import (
|
||||
C_ABYSS,
|
||||
C_CYAN,
|
||||
C_CYAN_DEEP,
|
||||
C_FOAM,
|
||||
C_FOG,
|
||||
C_HORIZON,
|
||||
C_MIDNIGHT,
|
||||
C_SAND,
|
||||
C_STEEL,
|
||||
display_font,
|
||||
mono_font,
|
||||
ui_font,
|
||||
)
|
||||
|
||||
|
||||
BRAND_ROOT = Path(__file__).resolve().parents[2] / "docs" / "brand"
|
||||
|
||||
|
||||
class WelcomeScreen(QWidget):
|
||||
"""Pantalla de bienvenida con CTAs grandes."""
|
||||
|
||||
newProjectRequested = Signal()
|
||||
openProjectRequested = Signal()
|
||||
openRecentRequested = Signal(str) # path
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("welcomeScreen")
|
||||
self.setStyleSheet(
|
||||
f"#welcomeScreen {{ background: {C_ABYSS}; }}"
|
||||
)
|
||||
|
||||
outer = QVBoxLayout(self)
|
||||
outer.setContentsMargins(0, 0, 0, 0)
|
||||
outer.setSpacing(0)
|
||||
outer.addStretch(1)
|
||||
|
||||
center = QWidget()
|
||||
center.setMaximumWidth(880)
|
||||
cl = QVBoxLayout(center)
|
||||
cl.setSpacing(0)
|
||||
cl.setContentsMargins(48, 0, 48, 0)
|
||||
|
||||
# Hero with logo + title
|
||||
hero = QHBoxLayout()
|
||||
hero.setSpacing(24)
|
||||
|
||||
logo_path = BRAND_ROOT / "logo-mark.svg"
|
||||
if logo_path.exists():
|
||||
logo = QLabel()
|
||||
pix = QIcon(str(logo_path)).pixmap(120, 120)
|
||||
logo.setPixmap(pix)
|
||||
hero.addWidget(logo)
|
||||
|
||||
title_box = QVBoxLayout()
|
||||
title_box.setSpacing(4)
|
||||
title = QLabel("VMS-Sailor Studio")
|
||||
title.setFont(display_font(36, 700))
|
||||
title.setStyleSheet(f"color: {C_FOAM};")
|
||||
title_box.addWidget(title)
|
||||
sub = QLabel("VESSEL · MANAGEMENT · SYSTEM")
|
||||
sub.setFont(ui_font(11))
|
||||
sub.setStyleSheet(f"color: {C_HORIZON}; letter-spacing: 4px;")
|
||||
title_box.addWidget(sub)
|
||||
body = QLabel(
|
||||
"Herramienta de ingeniería para configurar el VMS-Sailor de cada buque. "
|
||||
"Crea un nuevo proyecto desde el wizard o abre un .vmsproj existente."
|
||||
)
|
||||
body.setWordWrap(True)
|
||||
body.setFont(ui_font(11))
|
||||
body.setStyleSheet(f"color: {C_SAND}; margin-top: 12px;")
|
||||
title_box.addWidget(body)
|
||||
title_box.addStretch(1)
|
||||
hero.addLayout(title_box, 1)
|
||||
cl.addLayout(hero)
|
||||
cl.addSpacing(32)
|
||||
|
||||
# Two big CTAs
|
||||
ctas = QHBoxLayout()
|
||||
ctas.setSpacing(16)
|
||||
ctas.addWidget(
|
||||
self._make_cta(
|
||||
title="Nuevo desde wizard",
|
||||
subtitle="Wizard de 8 pasos: tipo de buque, plantilla, dimensiones,\n"
|
||||
"sistemas, equipos, topología, confirmación.",
|
||||
signal=self.newProjectRequested,
|
||||
primary=True,
|
||||
shortcut="Ctrl+N",
|
||||
),
|
||||
1,
|
||||
)
|
||||
ctas.addWidget(
|
||||
self._make_cta(
|
||||
title="Abrir proyecto",
|
||||
subtitle="Carga un archivo .vmsproj existente desde disco.\n"
|
||||
"Edita equipos, mímicos, tags y alarmas.",
|
||||
signal=self.openProjectRequested,
|
||||
primary=False,
|
||||
shortcut="Ctrl+O",
|
||||
),
|
||||
1,
|
||||
)
|
||||
cl.addLayout(ctas)
|
||||
cl.addSpacing(28)
|
||||
|
||||
# Quick links / docs
|
||||
links_label = QLabel("DOCUMENTACIÓN")
|
||||
links_label.setFont(ui_font(9))
|
||||
links_label.setStyleSheet(
|
||||
f"color: {C_FOG}; letter-spacing: 2.5px; margin-bottom: 8px;"
|
||||
)
|
||||
cl.addWidget(links_label)
|
||||
|
||||
links_row = QHBoxLayout()
|
||||
links_row.setSpacing(8)
|
||||
for txt, hint in (
|
||||
("Sprint 0 — Fundaciones", "docs/architecture.md"),
|
||||
("Sistema de coordenadas", "docs/coords.md"),
|
||||
("Design System", "docs/design_system.md"),
|
||||
("Mockups visuales", "docs/mockups/index.html"),
|
||||
):
|
||||
chip = QLabel(f" {txt} ")
|
||||
chip.setFont(mono_font(9))
|
||||
chip.setStyleSheet(
|
||||
f"color: {C_SAND}; background: {C_MIDNIGHT}; "
|
||||
f"border: 1px solid {C_STEEL}; border-radius: 12px; padding: 4px 10px;"
|
||||
)
|
||||
chip.setToolTip(hint)
|
||||
links_row.addWidget(chip)
|
||||
links_row.addStretch(1)
|
||||
cl.addLayout(links_row)
|
||||
|
||||
outer.addWidget(center, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||
outer.addStretch(2)
|
||||
|
||||
footer = QLabel(
|
||||
"Propiedad intelectual de Álvaro · El Studio no se distribuye al cliente"
|
||||
)
|
||||
footer.setFont(mono_font(9))
|
||||
footer.setStyleSheet(f"color: {C_FOG}; padding: 16px;")
|
||||
footer.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
outer.addWidget(footer)
|
||||
|
||||
def _make_cta(
|
||||
self,
|
||||
title: str,
|
||||
subtitle: str,
|
||||
signal,
|
||||
primary: bool,
|
||||
shortcut: str = "",
|
||||
) -> QFrame:
|
||||
frame = QFrame()
|
||||
frame.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
bg_hover = C_CYAN_DEEP if primary else C_STEEL
|
||||
border = C_CYAN if primary else C_IRON_FALLBACK
|
||||
bg = C_MIDNIGHT if not primary else "rgba(0,217,255,0.08)"
|
||||
frame.setStyleSheet(
|
||||
f"""
|
||||
QFrame {{
|
||||
background: {bg};
|
||||
border: 1px solid {border};
|
||||
border-radius: 14px;
|
||||
min-height: 180px;
|
||||
}}
|
||||
QFrame:hover {{
|
||||
background: {bg_hover};
|
||||
border-color: {C_CYAN};
|
||||
}}
|
||||
"""
|
||||
)
|
||||
lay = QVBoxLayout(frame)
|
||||
lay.setContentsMargins(24, 22, 24, 22)
|
||||
lay.setSpacing(8)
|
||||
|
||||
t = QLabel(title)
|
||||
t.setFont(display_font(22, 700))
|
||||
t.setStyleSheet(f"color: {C_FOAM};")
|
||||
lay.addWidget(t)
|
||||
|
||||
s = QLabel(subtitle)
|
||||
s.setFont(ui_font(10))
|
||||
s.setStyleSheet(f"color: {C_FOG};")
|
||||
s.setWordWrap(True)
|
||||
lay.addWidget(s)
|
||||
|
||||
lay.addStretch(1)
|
||||
kbd_row = QHBoxLayout()
|
||||
kbd_row.addStretch(1)
|
||||
if shortcut:
|
||||
kbd = QLabel(shortcut)
|
||||
kbd.setFont(mono_font(9))
|
||||
kbd.setStyleSheet(
|
||||
f"color: {C_FOG}; background: {C_ABYSS}; "
|
||||
f"border: 1px solid {C_STEEL}; border-radius: 4px; padding: 2px 8px;"
|
||||
)
|
||||
kbd_row.addWidget(kbd)
|
||||
arrow = QLabel("→")
|
||||
arrow.setFont(display_font(22, 700))
|
||||
arrow.setStyleSheet(f"color: {C_CYAN}; margin-left: 8px;")
|
||||
kbd_row.addWidget(arrow)
|
||||
lay.addLayout(kbd_row)
|
||||
|
||||
frame.mousePressEvent = lambda _ev: signal.emit() # type: ignore[method-assign]
|
||||
return frame
|
||||
|
||||
|
||||
# Fallback color para borde no-primary (no exportado en theme aún si quieres)
|
||||
C_IRON_FALLBACK = "#2C3E5C"
|
||||
Reference in New Issue
Block a user