Files
Compass/ui/info_panel.py
T
2026-07-03 12:23:41 -04:00

950 lines
41 KiB
Python

"""
Right-side information panel — fully responsive.
• Large heading display
• Data fields (HDG T, ROT, PITCH, ROLL, VAR, HEAVE, YAW RATE)
• ROT arc indicator
• Boat attitude silhouettes (pitch / roll / yaw)
• Touch buttons
"""
import math
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QFrame, QSizePolicy)
from PyQt5.QtCore import Qt, pyqtSignal, QRectF, QPointF
from PyQt5.QtGui import (QFont, QPainter, QPen, QBrush,
QColor, QPainterPath)
import ui.styles as S
# ─────────────────────────────────────────────────────────────────────────────
# Info Panel
# ─────────────────────────────────────────────────────────────────────────────
class InfoPanel(QWidget):
night_toggled = pyqtSignal(bool)
port_requested = pyqtSignal()
hdg_mode_changed = pyqtSignal(str) # 'M' or 'T'
def __init__(self, parent=None):
super().__init__(parent)
self._night = False
self._hdg_mode = 'M'
self._build()
# ── Layout ─────────────────────────────────────────────────────────────────
def _build(self):
root = QVBoxLayout(self)
root.setContentsMargins(4, 4, 4, 4)
root.setSpacing(4)
self._root_layout = root
# Large magnetic heading
self.hdg_val = QLabel('---.-°M')
self.hdg_val.setAlignment(Qt.AlignCenter)
self.hdg_val.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)
root.addWidget(self.hdg_val, stretch=4)
# Data rows
self._rows = {}
fields = [
('HDG (T)', 'hdg_t', '---.-°T'),
('ROT', 'rot', '--- °/min'),
('PITCH', 'pitch', '+--.-°'),
('ROLL', 'roll', '+--.-°'),
('VAR', 'var', '--.-°-'),
('HEAVE', 'accel_z', '--.- m/s²'),
('YAW RATE', 'gyro_z', '--.- °/s'),
]
rows_wrap = QWidget()
rows_wrap.setObjectName('section_box')
rows_vl = QVBoxLayout(rows_wrap)
rows_vl.setContentsMargins(0, 0, 0, 0)
rows_vl.setSpacing(0)
for label, key, default in fields:
row, val = self._field_row(label, default)
self._rows[key] = val
rows_vl.addWidget(row)
root.addWidget(rows_wrap, stretch=3 * 7)
# ROT arc
self.rot_arc = RotArc()
self.rot_arc.setObjectName('section_box')
root.addWidget(self.rot_arc, stretch=5)
# Boat attitude silhouettes
import config
self.boat_att = BoatAttitudeWidget(vessel_type=config.VESSEL_TYPE)
self.boat_att.setObjectName('section_box')
root.addWidget(self.boat_att, stretch=7)
# Touch buttons
self._btn_container = QWidget()
self._btn_container.setObjectName('section_box')
self._btn_layout = QHBoxLayout(self._btn_container)
self._btn_hdg = self._btn('HDG °M', self._toggle_hdg_mode)
self._btn_night = self._btn('NIGHT', self._toggle_night)
self._btn_port = self._btn('PORTS', self.port_requested.emit)
self._btn_layout.addWidget(self._btn_hdg)
self._btn_layout.addWidget(self._btn_night)
self._btn_layout.addWidget(self._btn_port)
root.addWidget(self._btn_container, stretch=2)
self._apply_theme()
def _field_row(self, label_text, default):
row = QWidget()
row.setObjectName('field_row')
hl = QHBoxLayout(row)
hl.setContentsMargins(0, 0, 0, 0)
lbl = QLabel(label_text)
lbl.setObjectName('field_lbl')
lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
val = QLabel(default)
val.setObjectName('field_val')
val.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
hl.addWidget(lbl, stretch=4)
hl.addWidget(val, stretch=6)
return row, val
def _divider(self):
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFixedHeight(1)
line.setStyleSheet(f'background: {S.DIVIDER.name()};')
return line
def _btn(self, text, callback):
b = QPushButton(text)
b.setObjectName('nav_btn')
b.clicked.connect(callback)
return b
# ── Theme ──────────────────────────────────────────────────────────────────
def _apply_theme(self):
n = self._night
bg = S.N_BG.name() if n else S.PANEL_BG.name()
gold = S.N_GOLD.name() if n else S.GOLD.name()
bright = S.N_GOLD.name() if n else S.GOLD_BRIGHT.name()
white = S.N_WHITE.name() if n else S.WHITE.name()
dim = S.N_WHITE.name() if n else S.WHITE_DIM.name()
box_bg = S.BEZEL_DARK.name()
h = max(self.height(), 480)
w = max(self.width(), 200)
hdg_fs = max(14, h // 12)
data_fs = max(10, h // 38)
lbl_fs = max(9, h // 48)
btn_fs = max(9, h // 50)
btn_h = max(36, h // 14)
pad_h = max(2, h // 120)
pad_w = max(6, w // 25)
btn_sp = max(3, w // 60)
btn_rad = max(4, h // 100)
gap = max(4, h // 120) # spacing between frames
brad = max(4, h // 140) # border radius for section boxes
self._root_layout.setContentsMargins(gap, gap, gap, gap)
self._root_layout.setSpacing(gap)
self._btn_layout.setContentsMargins(btn_sp, btn_sp, btn_sp, btn_sp)
self._btn_layout.setSpacing(btn_sp)
self.setStyleSheet(f"""
QWidget {{
background: {bg};
color: {white};
font-family: 'Courier New';
}}
QWidget#section_box {{
background: {box_bg};
border: 1px solid {S.DIVIDER.name()};
border-radius: {brad}px;
}}
QWidget#field_row {{
border-bottom: 1px solid {S.DIVIDER.name()};
}}
QLabel#field_lbl {{
color: {dim};
font-size: {lbl_fs}px;
font-weight: bold;
padding-left: {pad_w}px;
padding-top: {pad_h}px;
padding-bottom: {pad_h}px;
}}
QLabel#field_val {{
color: {white};
font-size: {data_fs}px;
padding-right: {pad_w}px;
padding-top: {pad_h}px;
padding-bottom: {pad_h}px;
}}
QPushButton#nav_btn {{
background: {S.BEZEL_MID.name()};
color: {gold};
border: 1px solid {gold};
border-radius: {btn_rad}px;
font-size: {btn_fs}px;
font-weight: bold;
min-height: {btn_h}px;
}}
QPushButton#nav_btn:pressed {{
background: {gold};
color: {bg};
}}
""")
f = QFont('Courier New', hdg_fs, QFont.Bold)
self.hdg_val.setFont(f)
self.hdg_val.setStyleSheet(f'color: {bright}; background: transparent;')
self.rot_arc.set_night(n)
self.boat_att.set_night(n)
def resizeEvent(self, event):
super().resizeEvent(event)
self._apply_theme()
# ── Data update ────────────────────────────────────────────────────────────
def update_data(self, nav):
if self._hdg_mode == 'T':
hdg = nav.hdg_true_calc
suffix = '°T'
else:
hdg = nav.hdg_mag
suffix = '°M'
self.hdg_val.setText(f'{hdg:05.1f}{suffix}' if hdg is not None else f'---.--{suffix}')
self._set('hdg_t', self._fmt_hdg(nav.hdg_true_calc, '°T'))
self._set('rot', self._fmt_rot(nav.rot))
self._set('pitch', self._fmt_signed(nav.pitch, '°'))
self._set('roll', self._fmt_signed(nav.roll, '°'))
self._set('var', self._fmt_var(nav.variation))
self._set('accel_z', self._fmt_accel(nav.accel_z))
self._set('gyro_z', self._fmt_gyro(nav.gyro_z))
rot = nav.rot or 0.0
pitch = nav.pitch or 0.0
roll = nav.roll or 0.0
self.rot_arc.set_rot(rot)
self.boat_att.set_attitude(pitch, roll, rot)
def _set(self, key, text):
if key in self._rows:
self._rows[key].setText(text)
# ── Formatting ─────────────────────────────────────────────────────────────
@staticmethod
def _fmt_hdg(v, suffix):
return f'{v:05.1f}{suffix}' if v is not None else f'---.--{suffix}'
@staticmethod
def _fmt_rot(v):
return f'{v:+.1f} °/min' if v is not None else '--- °/min'
@staticmethod
def _fmt_signed(v, unit):
return f'{v:+.1f}{unit}' if v is not None else f'+--.-{unit}'
@staticmethod
def _fmt_var(v):
return f'{abs(v):.1f}°{"E" if v >= 0 else "W"}' if v is not None else '--.-°-'
@staticmethod
def _fmt_accel(v):
return f'{v:+.2f} m/s²' if v is not None else '--.- m/s²'
@staticmethod
def _fmt_gyro(v):
return f'{v:+.2f} °/s' if v is not None else '--.- °/s'
# ── Toggles ────────────────────────────────────────────────────────────────
def _toggle_hdg_mode(self):
self._hdg_mode = 'T' if self._hdg_mode == 'M' else 'M'
self._btn_hdg.setText(f'HDG °{self._hdg_mode}')
self.hdg_mode_changed.emit(self._hdg_mode)
def _toggle_night(self):
self._night = not self._night
self._apply_theme()
self.night_toggled.emit(self._night)
# ─────────────────────────────────────────────────────────────────────────────
# ROT Arc
# ─────────────────────────────────────────────────────────────────────────────
class RotArc(QWidget):
"""Semicircular rate-of-turn indicator. Fully proportional."""
def __init__(self, parent=None):
super().__init__(parent)
self._rot = 0.0
self._night = False
def set_rot(self, rot):
self._rot = rot or 0.0
self.update()
def set_night(self, on):
self._night = on
self.update()
def paintEvent(self, event):
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
w, h = self.width(), self.height()
if w < 10 or h < 10:
return
gold = S.N_GOLD if self._night else S.GOLD
white = S.N_WHITE if self._night else S.WHITE_DIM
red = S.N_RED if self._night else S.RED
blue = S.N_BLUE if self._night else S.BLUE
# Reserve top & bottom padding so text doesn't touch dividers
pad = max(6, h * 0.10)
cx = w / 2.0
cy = h * 0.60
r = min(w * 0.40, (h - pad * 2) * 0.68)
lw = max(2.0, r * 0.07)
# Grey track arc
p.setPen(QPen(S.BEZEL_MID, lw, Qt.SolidLine, Qt.RoundCap))
p.drawArc(int(cx - r), int(cy - r), int(r * 2), int(r * 2),
0, 180 * 16)
# Colored ROT arc (scale: ±60°/min = full 90° sweep)
rot_c = max(-60.0, min(60.0, self._rot))
if abs(rot_c) > 0.3:
span = -(rot_c / 60.0) * 90.0
color = blue if rot_c > 0 else red
p.setPen(QPen(color, lw * 1.4, Qt.SolidLine, Qt.RoundCap))
p.drawArc(int(cx - r), int(cy - r), int(r * 2), int(r * 2),
int(90 * 16), int(-span * 16))
# Center tick
p.setPen(QPen(gold, max(1.5, r * 0.04)))
p.drawLine(int(cx), int(cy - r * 1.10), int(cx), int(cy - r * 0.88))
# PORT / STBD labels — above the arc ends
side_fs = max(7, int(r * 0.17))
p.setFont(QFont('Arial', side_fs))
p.setPen(QPen(white))
side_h = max(14, int(r * 0.28))
p.drawText(int(cx - r - r * 0.1), int(cy - r * 0.18),
int(r * 0.9), side_h, Qt.AlignCenter, 'PORT')
p.drawText(int(cx + r * 0.2), int(cy - r * 0.18),
int(r * 0.9), side_h, Qt.AlignCenter, 'STBD')
# ROT value — with gap below arc
txt_fs = max(8, int(r * 0.22))
txt_h = max(16, int(r * 0.35))
p.setFont(QFont('Courier New', txt_fs, QFont.Bold))
p.setPen(QPen(white))
p.drawText(0, int(cy + r * 0.18), w, txt_h,
Qt.AlignCenter, f'{self._rot:+.1f} °/min')
# ─────────────────────────────────────────────────────────────────────────────
# Boat Attitude Widget
# ─────────────────────────────────────────────────────────────────────────────
class BoatAttitudeWidget(QWidget):
"""
Three animated boat silhouettes:
PITCH — side view, bow right, rotates fore/aft
ROLL — front view, tilts port/stbd
YAW — top view, rotates with ROT
"""
def __init__(self, vessel_type: str = 'motor_cruiser', parent=None):
super().__init__(parent)
self._pitch = 0.0
self._roll = 0.0
self._rot = 0.0
self._night = False
self._vessel_type = vessel_type
def set_attitude(self, pitch: float, roll: float, rot: float = 0.0):
self._pitch = pitch
self._roll = roll
self._rot = rot
self.update()
def set_night(self, on: bool):
self._night = on
self.update()
def paintEvent(self, event):
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
p.setRenderHint(QPainter.TextAntialiasing)
w, h = self.width(), self.height()
if w < 30 or h < 20:
return
pw = w / 3.0 # panel width per silhouette
self._draw_panel(p, 0, pw, h, self._pitch,
'PITCH', 'side', f'{self._pitch:+.1f}°')
self._draw_panel(p, pw, pw, h, self._roll,
'ROLL', 'front', f'{self._roll:+.1f}°')
self._draw_panel(p, pw * 2, pw, h, self._rot,
'YAW', 'top', f'{self._rot:+.1f}°/m')
def _draw_panel(self, p, x, pw, h, value, label, view, val_str):
gold = S.N_GOLD if self._night else S.GOLD
white = S.N_WHITE if self._night else S.WHITE
dim = S.N_WHITE if self._night else S.WHITE_DIM
mg = max(3, int(pw * 0.06))
lh = max(14, int(h * 0.15)) # label height
vh = max(14, int(h * 0.15)) # value height
boat_h = h - lh - vh
cx = x + pw / 2.0
cy = lh + boat_h / 2.0
r = min(pw * 0.42, boat_h * 0.44)
# Panel bg
p.setBrush(QBrush(S.BEZEL_DARK))
p.setPen(QPen(S.DIVIDER, 1))
p.drawRoundedRect(QRectF(x + mg, mg, pw - mg * 2, h - mg * 2),
max(3, mg * 0.6), max(3, mg * 0.6))
# Label
lfs = max(7, int(h * 0.085))
p.setFont(QFont('Arial', lfs, QFont.Bold))
p.setPen(QPen(gold))
p.drawText(QRectF(x, mg, pw, lh), Qt.AlignCenter, label)
# Waterline (fixed)
p.setPen(QPen(dim.darker(180) if not self._night else dim.darker(120),
max(1, int(r * 0.03)), Qt.DashLine))
p.drawLine(QPointF(x + mg * 2, cy), QPointF(x + pw - mg * 2, cy))
# Rotated boat silhouette
p.save()
p.translate(cx, cy)
vt = self._vessel_type
if view == 'side':
p.rotate(-value)
if vt == 'cargo': self._boat_side_cargo(p, r, gold)
else: self._boat_side(p, r, gold)
elif view == 'front':
p.rotate(value)
if vt == 'cargo': self._boat_front_cargo(p, r, gold)
else: self._boat_front(p, r, gold)
elif view == 'top':
vis = max(-45.0, min(45.0, value * 0.75))
p.rotate(vis)
if vt == 'cargo': self._boat_top_cargo(p, r, gold)
else: self._boat_top(p, r, gold)
p.restore()
# Value
vfs = max(7, int(h * 0.10))
p.setFont(QFont('Courier New', vfs, QFont.Bold))
p.setPen(QPen(white))
p.drawText(QRectF(x, h - vh - mg, pw, vh), Qt.AlignCenter, val_str)
# ── Boat shapes ────────────────────────────────────────────────────────────
def _boat_side(self, p, r, color):
"""Modern superyacht starboard profile — bow right, sleek low lines."""
lw = max(1.0, r * 0.025)
h_fill = color.darker(148)
h_pen = color.darker(185)
c_fill = color.darker(162)
win_col = QColor(28, 48, 95, 220)
# ── 1. HULL — sleek, low freeboard ────────────────────────
hull = QPainterPath()
hull.moveTo(-r*0.88, -r*0.10) # transom top
hull.lineTo(-r*0.92, r*0.14) # transom bottom
hull.quadTo(-r*0.20, r*0.30, r*0.28, r*0.22)
hull.quadTo( r*0.68, r*0.13, r*0.94, -r*0.03)
hull.lineTo( r*0.80, -r*0.28) # bow deck
hull.cubicTo(r*0.52, -r*0.22, r*0.04, -r*0.16, -r*0.30, -r*0.16)
hull.lineTo(-r*0.88, -r*0.10)
hull.closeSubpath()
p.setPen(QPen(h_pen, lw))
p.setBrush(QBrush(h_fill))
p.drawPath(hull)
# ── 2. RED BOOT STRIPE ─────────────────────────────────────
boot = QPainterPath()
boot.moveTo(-r*0.92, r*0.14)
boot.quadTo(-r*0.20, r*0.20, r*0.28, r*0.14)
boot.quadTo( r*0.68, r*0.06, r*0.94, -r*0.03)
boot.lineTo( r*0.90, r*0.02)
boot.quadTo( r*0.66, r*0.06, r*0.24, r*0.10)
boot.quadTo(-r*0.20, r*0.14, -r*0.92, r*0.08)
boot.closeSubpath()
p.setPen(Qt.NoPen)
p.setBrush(QBrush(QColor(172, 22, 22, 210)))
p.drawPath(boot)
# ── 3. MAIN SUPERSTRUCTURE — long, low, angular ───────────
sup = QPainterPath()
sup.moveTo(-r*0.76, -r*0.10)
sup.lineTo(-r*0.76, -r*0.42)
sup.lineTo(-r*0.70, -r*0.46)
sup.lineTo( r*0.36, -r*0.46)
sup.lineTo( r*0.44, -r*0.40)
sup.lineTo( r*0.44, -r*0.10)
sup.closeSubpath()
p.setPen(QPen(h_pen, lw * 0.85))
p.setBrush(QBrush(c_fill))
p.drawPath(sup)
# ── 4. LONG WINDOW STRIP ───────────────────────────────────
p.setPen(QPen(color.darker(122), max(1, r*0.018)))
p.setBrush(QBrush(win_col.lighter(140)))
p.drawRoundedRect(QRectF(-r*0.72, -r*0.43, r*1.06, r*0.22),
r*0.016, r*0.016)
# ── 5. BRIDGE DECK ─────────────────────────────────────────
br = QPainterPath()
br.moveTo(-r*0.38, -r*0.46)
br.lineTo(-r*0.38, -r*0.68)
br.lineTo(-r*0.32, -r*0.72)
br.lineTo( r*0.26, -r*0.72)
br.lineTo( r*0.32, -r*0.66)
br.lineTo( r*0.32, -r*0.46)
br.closeSubpath()
p.setBrush(QBrush(c_fill.darker(112)))
p.drawPath(br)
p.setBrush(QBrush(win_col.lighter(160)))
p.drawRoundedRect(QRectF(-r*0.32, -r*0.69, r*0.54, r*0.16),
r*0.012, r*0.012)
# ── 6. MAST & RADAR ────────────────────────────────────────
p.setPen(QPen(color.darker(128), max(1.5, r*0.028)))
p.drawLine(QPointF(r*0.00, -r*0.72), QPointF(r*0.00, -r*0.95))
p.setPen(QPen(h_pen, max(1, r*0.015)))
p.setBrush(QBrush(color.darker(160)))
p.drawEllipse(QPointF(r*0.00, -r*0.93), r*0.048, r*0.034)
# ── 7. SWIM PLATFORM ───────────────────────────────────────
plat = QPainterPath()
plat.moveTo(-r*0.88, r*0.14)
plat.lineTo(-r*0.98, r*0.14)
plat.lineTo(-r*0.98, r*0.20)
plat.lineTo(-r*0.88, r*0.20)
plat.closeSubpath()
p.setPen(QPen(h_pen, lw * 0.8))
p.setBrush(QBrush(h_fill.darker(110)))
p.drawPath(plat)
def _boat_front(self, p, r, color):
"""Modern superyacht bow-on — deep-V hull, wide flare, low superstructure."""
lw = max(1.0, r * 0.025)
h_fill = color.darker(148)
h_pen = color.darker(185)
c_fill = color.darker(162)
win_col = QColor(28, 48, 95, 220)
# ── HULL below waterline — sharp V ─────────────────────────
hull_v = QPainterPath()
hull_v.moveTo( 0, r*0.50)
hull_v.lineTo(-r*0.52, r*0.06)
hull_v.lineTo( r*0.52, r*0.06)
hull_v.closeSubpath()
p.setPen(QPen(h_pen, lw))
p.setBrush(QBrush(h_fill.darker(118)))
p.drawPath(hull_v)
# ── HULL TOPSIDES — wide flare ─────────────────────────────
hull_t = QPainterPath()
hull_t.moveTo(-r*0.52, r*0.06)
hull_t.quadTo(-r*0.84, -r*0.02, -r*0.88, -r*0.16)
hull_t.lineTo(-r*0.62, -r*0.24)
hull_t.lineTo( 0, -r*0.32)
hull_t.lineTo( r*0.62, -r*0.24)
hull_t.lineTo( r*0.88, -r*0.16)
hull_t.quadTo( r*0.84, -r*0.02, r*0.52, r*0.06)
hull_t.closeSubpath()
p.setPen(QPen(h_pen, lw))
p.setBrush(QBrush(h_fill))
p.drawPath(hull_t)
# Boot stripe
p.setPen(QPen(QColor(172, 22, 22, 210), max(2.0, r*0.034)))
p.drawLine(QPointF(-r*0.52, r*0.06), QPointF(r*0.52, r*0.06))
# ── WINDSHIELD — wide, angled ──────────────────────────────
ws = QPainterPath()
ws.moveTo(-r*0.26, -r*0.32)
ws.lineTo(-r*0.20, -r*0.58)
ws.lineTo( 0, -r*0.64)
ws.lineTo( r*0.20, -r*0.58)
ws.lineTo( r*0.26, -r*0.32)
ws.closeSubpath()
p.setPen(QPen(h_pen, lw * 0.85))
p.setBrush(QBrush(c_fill))
p.drawPath(ws)
# Glass panels (port + stbd)
for sign in (-1, 1):
pg = QPainterPath()
pg.moveTo(sign * r*0.22, -r*0.34)
pg.lineTo(sign * r*0.16, -r*0.54)
pg.lineTo(sign * r*0.02, -r*0.60)
pg.lineTo(sign * r*0.02, -r*0.34)
pg.closeSubpath()
p.setPen(QPen(color.darker(122), max(1, r*0.016)))
p.setBrush(QBrush(win_col.lighter(145)))
p.drawPath(pg)
# ── MAST ───────────────────────────────────────────────────
p.setPen(QPen(color.darker(128), max(1.5, r*0.030)))
p.drawLine(QPointF(0, -r*0.64), QPointF(0, -r*0.92))
# Radar dome
p.setPen(QPen(h_pen, max(1, r*0.015)))
p.setBrush(QBrush(color.darker(160)))
p.drawEllipse(QPointF(0, -r*0.90), r*0.054, r*0.038)
# ── NAV LIGHTS ─────────────────────────────────────────────
p.setPen(Qt.NoPen)
p.setBrush(QBrush(QColor(220, 40, 40, 240)))
p.drawEllipse(QPointF(-r*0.90, -r*0.14), r*0.036, r*0.036)
p.setBrush(QBrush(QColor(40, 205, 80, 240)))
p.drawEllipse(QPointF( r*0.90, -r*0.14), r*0.036, r*0.036)
# Keel stem
p.setPen(QPen(color.darker(120), max(1.0, r*0.030)))
p.drawLine(QPointF(0, r*0.50), QPointF(0, r*0.12))
# ── CARGO SHIP ────────────────────────────────────────────────────────────
def _boat_side_cargo(self, p, r, color):
"""Bulk carrier starboard profile — bow right, bridge + funnel at stern (left).
High freeboard, 4 prominent goalpost cranes, bulbous bow."""
lw = max(1.0, r * 0.022)
h_fill = color.darker(152)
h_pen = color.darker(190)
s_fill = color.darker(160)
crane_c = color.darker(122)
DK = -r * 0.14 # deck rail (above center = negative y)
WL = r * 0.24 # waterline
KL = r * 0.44 # keel
DT = DK - r*0.10 # deck top surface
# ── HULL — very elongated, high freeboard ──────────────────
hull = QPainterPath()
hull.moveTo(-r*0.93, DK)
hull.lineTo(-r*0.93, KL) # stern vertical
hull.lineTo( r*0.74, KL) # flat keel
hull.quadTo( r*0.94, KL, r*0.94, WL) # bow keel curve
hull.lineTo( r*0.94, DT - r*0.06) # bow stem
hull.quadTo( r*0.86, DT - r*0.10, r*0.64, DT - r*0.10)
hull.lineTo(-r*0.86, DT - r*0.10) # flat main deck
hull.lineTo(-r*0.93, DK)
hull.closeSubpath()
p.setPen(QPen(h_pen, lw))
p.setBrush(QBrush(h_fill))
p.drawPath(hull)
# Bulbous bow
p.setPen(QPen(h_pen, lw * 0.70))
p.setBrush(QBrush(h_fill.darker(114)))
p.drawEllipse(QPointF(r*0.96, WL + r*0.04), r*0.036, r*0.066)
# Boot stripe — bold red antifouling
p.setPen(QPen(QColor(148, 12, 12, 245), max(2.5, r * 0.042)))
p.drawLine(QPointF(-r*0.93, WL), QPointF(r*0.93, WL))
# ── BRIDGE CASTLE at STERN — tall, prominent ───────────────
# Level 1 — accommodation block (widest)
p.setPen(QPen(h_pen, lw * 0.84))
p.setBrush(QBrush(s_fill))
p.drawRect(QRectF(-r*0.93, DT - r*0.52, r*0.44, r*0.42))
# Level 2 — bridge deck
p.setBrush(QBrush(s_fill.darker(110)))
p.drawRect(QRectF(-r*0.91, DT - r*0.68, r*0.36, r*0.16))
# Level 3 — wheelhouse
p.setBrush(QBrush(s_fill.darker(122)))
p.drawRect(QRectF(-r*0.88, DT - r*0.82, r*0.26, r*0.14))
# Bridge windows row
p.setPen(QPen(color.darker(122), max(1, r*0.015)))
p.setBrush(QBrush(QColor(28, 48, 95, 230)))
for wx in (-r*0.84, -r*0.74, -r*0.64):
p.drawRect(QRectF(wx, DT - r*0.79, r*0.08, r*0.10))
# Funnel — tall, slim, forward of bridge
p.setPen(QPen(h_pen, lw * 0.78))
p.setBrush(QBrush(h_fill.darker(124)))
p.drawRect(QRectF(-r*0.60, DT - r*0.80, r*0.12, r*0.70))
p.setBrush(QBrush(h_fill.darker(132)))
p.drawRect(QRectF(-r*0.62, DT - r*0.88, r*0.16, r*0.10)) # cap
# ── GOALPOST CRANES — 4 prominent H-frames ─────────────────
ct = DT - r * 0.54 # crossbeam height
hw = r * 0.058
for gx in (-r*0.15, r*0.18, r*0.50, r*0.80):
lp, rp = gx - hw, gx + hw
# Vertical legs
p.setPen(QPen(crane_c, max(2.0, r * 0.034)))
p.drawLine(QPointF(lp, DT), QPointF(lp, ct))
p.drawLine(QPointF(rp, DT), QPointF(rp, ct))
# Crossbeam
p.setPen(QPen(crane_c, max(1.5, r * 0.025)))
p.drawLine(QPointF(lp - r*0.04, ct), QPointF(rp + r*0.04, ct))
# Derrick booms angled outward
p.setPen(QPen(crane_c, max(1.0, r * 0.018)))
p.drawLine(QPointF(lp, ct + r*0.10),
QPointF(lp - r*0.20, ct - r*0.18))
p.drawLine(QPointF(rp, ct + r*0.10),
QPointF(rp + r*0.20, ct - r*0.18))
# ── CARGO HATCH COAMINGS — 4 hatches ──────────────────────
p.setPen(QPen(h_pen, max(0.8, r * 0.015)))
p.setBrush(QBrush(h_fill.lighter(118)))
for hx in (-r*0.12, r*0.20, r*0.52, r*0.80):
p.drawRoundedRect(QRectF(hx - r*0.12, DT + r*0.01, r*0.22, r*0.10),
r*0.012, r*0.012)
# Nav lights
p.setPen(Qt.NoPen)
p.setBrush(QBrush(QColor(220, 40, 40, 232)))
p.drawEllipse(QPointF(-r*0.91, DT - r*0.08), r*0.028, r*0.028)
p.setBrush(QBrush(QColor(40, 205, 80, 232)))
p.drawEllipse(QPointF( r*0.90, DT - r*0.08), r*0.028, r*0.028)
def _boat_front_cargo(self, p, r, color):
"""Bulk carrier bow-on — wide boxy hull, full flare, tall stacked bridge."""
lw = max(1.0, r * 0.022)
h_fill = color.darker(152)
h_pen = color.darker(190)
s_fill = color.darker(160)
DK = -r * 0.12 # deck
WL = r * 0.10 # waterline
KL = r * 0.56 # keel
HW = r * 0.90 # hull half-width — bulk carriers are WIDE
# ── HULL below waterline — less V, more full-form ──────────
hull_v = QPainterPath()
hull_v.moveTo( 0, KL)
hull_v.lineTo(-r*0.36, WL + r*0.10)
hull_v.lineTo( r*0.36, WL + r*0.10)
hull_v.closeSubpath()
p.setPen(Qt.NoPen)
p.setBrush(QBrush(h_fill.darker(120)))
p.drawPath(hull_v)
# ── HULL TOPSIDES — wide, strong flare ─────────────────────
hull = QPainterPath()
hull.moveTo(-r*0.36, WL + r*0.10)
hull.quadTo(-r*0.70, WL + r*0.02, -HW, DK + r*0.08)
hull.lineTo(-HW, DK)
hull.lineTo( HW, DK)
hull.lineTo( HW, DK + r*0.08)
hull.quadTo( r*0.70, WL + r*0.02, r*0.36, WL + r*0.10)
hull.closeSubpath()
p.setPen(QPen(h_pen, lw))
p.setBrush(QBrush(h_fill))
p.drawPath(hull)
# Boot stripe — bold red
p.setPen(QPen(QColor(148, 12, 12, 245), max(2.5, r * 0.042)))
p.drawLine(QPointF(-HW + r*0.06, WL), QPointF(HW - r*0.06, WL))
# ── STACKED SUPERSTRUCTURE — 3 levels ──────────────────────
for bw, bt, bh in [
(r*0.56, DK - r*0.30, r*0.30), # accommodation (widest)
(r*0.42, DK - r*0.52, r*0.22), # bridge deck
(r*0.30, DK - r*0.70, r*0.18), # wheelhouse
]:
p.setPen(QPen(h_pen, lw * 0.82))
p.setBrush(QBrush(s_fill))
p.drawRect(QRectF(-bw, bt, bw * 2, bh))
# Bridge windows — prominent row
p.setPen(QPen(color.darker(122), max(1, r * 0.016)))
p.setBrush(QBrush(QColor(28, 48, 95, 232)))
win_y = DK - r*0.67
for wx in (-r*0.24, -r*0.12, 0, r*0.12, r*0.24):
p.drawRect(QRectF(wx - r*0.045, win_y, r*0.088, r*0.12))
# Funnel top (visible above wheelhouse)
p.setPen(QPen(h_pen, lw * 0.7))
p.setBrush(QBrush(h_fill.darker(124)))
p.drawRect(QRectF(-r*0.10, DK - r*0.88, r*0.20, r*0.18))
# ── MAST + YARDARM + RADAR ─────────────────────────────────
p.setPen(QPen(color.darker(128), max(1.5, r * 0.026)))
p.drawLine(QPointF(0, DK - r*0.70), QPointF(0, DK - r*0.96))
p.setPen(QPen(color.darker(132), max(1.0, r * 0.018)))
p.drawLine(QPointF(-r*0.22, DK - r*0.88), QPointF(r*0.22, DK - r*0.88))
p.setPen(QPen(h_pen, max(1, r * 0.014)))
p.setBrush(QBrush(color.darker(164)))
p.drawEllipse(QPointF(0, DK - r*0.94), r*0.046, r*0.032)
# ── NAV LIGHTS ─────────────────────────────────────────────
p.setPen(Qt.NoPen)
p.setBrush(QBrush(QColor(220, 40, 40, 240)))
p.drawEllipse(QPointF(-HW + r*0.02, DK + r*0.02), r*0.036, r*0.036)
p.setBrush(QBrush(QColor(40, 205, 80, 240)))
p.drawEllipse(QPointF( HW - r*0.02, DK + r*0.02), r*0.036, r*0.036)
# Keel stem
p.setPen(QPen(color.darker(120), max(1.0, r * 0.026)))
p.drawLine(QPointF(0, KL), QPointF(0, WL + r*0.08))
def _boat_top_cargo(self, p, r, color):
"""Bulk carrier plan view — bow at top, WIDE oval (bulk carriers are wide!),
bridge castle at stern, 4 cargo holds, 2 goalpost crane pairs."""
lw = max(1.0, r * 0.022)
h_fill = color.darker(152)
h_pen = color.darker(190)
s_fill = color.darker(160)
HW = r * 0.46 # hull half-width — bulk carriers have ~3:1 L/B ratio
# ── HULL — wide oval with raked bow ────────────────────────
hull = QPainterPath()
hull.moveTo( 0, -r*0.94) # bow tip
hull.cubicTo(-r*0.20, -r*0.88, -HW, -r*0.70, -HW, -r*0.30)
hull.lineTo(-HW, r*0.50) # port flat side
hull.quadTo(-HW, r*0.76, 0, r*0.86) # stern port
hull.quadTo( HW, r*0.76, HW, r*0.50) # stern stbd
hull.lineTo( HW, -r*0.30) # stbd flat side
hull.cubicTo( HW, -r*0.70, r*0.20, -r*0.88, 0, -r*0.94)
hull.closeSubpath()
p.setPen(QPen(h_pen, lw))
p.setBrush(QBrush(h_fill))
p.drawPath(hull)
# ── BRIDGE CASTLE at STERN (bottom) ────────────────────────
p.setPen(QPen(h_pen, lw * 0.82))
p.setBrush(QBrush(s_fill))
p.drawRoundedRect(QRectF(-r*0.32, r*0.52, r*0.64, r*0.26),
r*0.05, r*0.05)
# Funnel — circle on superstructure
p.setPen(QPen(h_pen, lw * 0.70))
p.setBrush(QBrush(h_fill.darker(122)))
p.drawEllipse(QPointF(0, r*0.70), r*0.086, r*0.072)
# ── CARGO HOLDS — 4 wide hatches ───────────────────────────
p.setPen(QPen(h_pen, max(0.9, r * 0.016)))
p.setBrush(QBrush(h_fill.lighter(116)))
for hy in (-r*0.72, -r*0.36, r*0.00, r*0.36):
p.drawRoundedRect(QRectF(-r*0.38, hy, r*0.76, r*0.28),
r*0.04, r*0.04)
# ── GOALPOST CRANE PAIRS — 2 sets of double H-frames ───────
crane_c = color.darker(126)
for cy_c in (-r*0.56, r*0.14):
# Two athwartship beams (top and bottom chord of goalpost)
p.setPen(QPen(crane_c, max(2.5, r * 0.044)))
p.drawLine(QPointF(-r*0.38, cy_c - r*0.07),
QPointF( r*0.38, cy_c - r*0.07))
p.drawLine(QPointF(-r*0.38, cy_c + r*0.07),
QPointF( r*0.38, cy_c + r*0.07))
# Centerline spine
p.setPen(QPen(crane_c, max(1.2, r * 0.018)))
p.drawLine(QPointF(0, cy_c - r*0.10), QPointF(0, cy_c + r*0.10))
# ── CENTERLINE dashed ───────────────────────────────────────
p.setPen(QPen(color.darker(148), max(0.8, r * 0.016), Qt.DashLine))
p.drawLine(QPointF(0, -r*0.88), QPointF(0, r*0.82))
# ── BOW DIRECTION ARROW ─────────────────────────────────────
arrow = QPainterPath()
arrow.moveTo( 0, -r*0.94)
arrow.lineTo(-r*0.09, -r*0.78)
arrow.lineTo( r*0.09, -r*0.78)
arrow.closeSubpath()
p.setBrush(QBrush(color))
p.setPen(Qt.NoPen)
p.drawPath(arrow)
# ── NAV LIGHTS ─────────────────────────────────────────────
p.setPen(Qt.NoPen)
p.setBrush(QBrush(QColor(220, 40, 40, 240)))
p.drawEllipse(QPointF(-HW + r*0.04, -r*0.48), r*0.040, r*0.040)
p.setBrush(QBrush(QColor(40, 205, 80, 240)))
p.drawEllipse(QPointF( HW - r*0.04, -r*0.48), r*0.040, r*0.040)
def _boat_top(self, p, r, color):
"""Modern superyacht plan view — bow at top, sleek teardrop hull."""
lw = max(1.0, r * 0.025)
h_fill = color.darker(148)
h_pen = color.darker(185)
c_fill = color.darker(162)
dk_fill = color.darker(155)
# ── HULL — sharp bow, wide transom ─────────────────────────
hull = QPainterPath()
hull.moveTo( 0, -r*0.94)
hull.cubicTo(-r*0.20, -r*0.86, -r*0.38, -r*0.50, -r*0.38, -r*0.08)
hull.cubicTo(-r*0.38, r*0.22, -r*0.32, r*0.50, -r*0.22, r*0.64)
hull.lineTo(-r*0.22, r*0.76)
hull.lineTo( r*0.22, r*0.76)
hull.lineTo( r*0.22, r*0.64)
hull.cubicTo( r*0.32, r*0.50, r*0.38, r*0.22, r*0.38, -r*0.08)
hull.cubicTo( r*0.38, -r*0.50, r*0.20, -r*0.86, 0, -r*0.94)
hull.closeSubpath()
p.setPen(QPen(h_pen, lw))
p.setBrush(QBrush(h_fill))
p.drawPath(hull)
# ── LONG SUPERSTRUCTURE ────────────────────────────────────
p.setPen(QPen(h_pen, lw * 0.85))
p.setBrush(QBrush(c_fill))
p.drawRoundedRect(QRectF(-r*0.22, -r*0.34, r*0.44, r*0.64),
r*0.06, r*0.06)
# ── BRIDGE (forward section of superstructure) ─────────────
p.setBrush(QBrush(c_fill.darker(112)))
p.drawRoundedRect(QRectF(-r*0.15, -r*0.30, r*0.30, r*0.22),
r*0.05, r*0.05)
# ── AFT DECK (open) ────────────────────────────────────────
p.setBrush(QBrush(dk_fill))
p.drawRoundedRect(QRectF(-r*0.18, r*0.36, r*0.36, r*0.26),
r*0.04, r*0.04)
# ── SWIM PLATFORM ──────────────────────────────────────────
p.setBrush(QBrush(h_fill.darker(110)))
p.drawRoundedRect(QRectF(-r*0.16, r*0.64, r*0.32, r*0.12),
r*0.03, r*0.03)
# ── FOREDECK HATCH ─────────────────────────────────────────
p.setPen(QPen(h_pen, lw * 0.80))
p.setBrush(QBrush(h_fill.darker(108)))
p.drawRoundedRect(QRectF(-r*0.06, -r*0.60, r*0.12, r*0.10),
r*0.02, r*0.02)
p.drawRoundedRect(QRectF(-r*0.06, -r*0.46, r*0.12, r*0.10),
r*0.02, r*0.02)
# ── CENTERLINE ─────────────────────────────────────────────
p.setPen(QPen(color.darker(148), max(0.8, r*0.016), Qt.DashLine))
p.drawLine(QPointF(0, -r*0.88), QPointF(0, r*0.70))
# ── BOW DIRECTION ARROW ─────────────────────────────────────
arrow = QPainterPath()
arrow.moveTo( 0, -r*0.94)
arrow.lineTo(-r*0.08, -r*0.78)
arrow.lineTo( r*0.08, -r*0.78)
arrow.closeSubpath()
p.setBrush(QBrush(color))
p.setPen(Qt.NoPen)
p.drawPath(arrow)
# ── NAV LIGHTS ─────────────────────────────────────────────
p.setBrush(QBrush(QColor(220, 40, 40, 240)))
p.drawEllipse(QPointF(-r*0.36, -r*0.22), r*0.038, r*0.038)
p.setBrush(QBrush(QColor(40, 205, 80, 240)))
p.drawEllipse(QPointF( r*0.36, -r*0.22), r*0.038, r*0.038)