""" 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)