950 lines
41 KiB
Python
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)
|