import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../theme/autopilot_theme.dart'; import '../../theme/theme_provider.dart'; /// Full compass rose widget — the dominant visual element of the cockpit. /// /// Displays: /// - Rotating compass ring with degree markings /// - Heading arc in [AutopilotTheme.accentLight] /// - Set-point tick in [AutopilotTheme.setLight] /// - North mark in [AutopilotTheme.northColor] /// - Centre heading readout in [AutopilotTheme.textMain] with accent glow /// /// [headingDeg] is the current vessel heading (0–359.9°, magnetic). /// [setPointDeg] is the desired heading (the autopilot target). class CompassRose extends StatelessWidget { const CompassRose({ super.key, required this.headingDeg, this.setPointDeg, this.size = 280, }); final double headingDeg; final double? setPointDeg; final double size; @override Widget build(BuildContext context) { final theme = context.watch().current; return SizedBox( width: size, height: size, child: CustomPaint( painter: _CompassPainter( theme: theme, headingDeg: headingDeg, setPointDeg: setPointDeg, ), child: Center( child: _HeadingReadout(theme: theme, headingDeg: headingDeg), ), ), ); } } class _HeadingReadout extends StatelessWidget { const _HeadingReadout({required this.theme, required this.headingDeg}); final AutopilotTheme theme; final double headingDeg; @override Widget build(BuildContext context) { final text = headingDeg.toStringAsFixed(1).padLeft(5, ' '); return Text( '$text°', style: TextStyle( color: theme.textMain, fontSize: 36, fontWeight: FontWeight.w200, letterSpacing: 2, fontFeatures: const [FontFeature.tabularFigures()], shadows: theme.accentGlowRadius > 0 ? [Shadow(color: theme.accentGlowColor, blurRadius: theme.accentGlowRadius)] : null, ), ); } } class _CompassPainter extends CustomPainter { const _CompassPainter({ required this.theme, required this.headingDeg, this.setPointDeg, }); final AutopilotTheme theme; final double headingDeg; final double? setPointDeg; double _toRad(double deg) => deg * math.pi / 180; @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = size.width / 2 - 8; // Outer ring canvas.drawCircle( center, radius, Paint() ..color = theme.panelBorder ..style = PaintingStyle.stroke ..strokeWidth = 1, ); // Heading arc (±45° around top = current heading) canvas.drawArc( Rect.fromCircle(center: center, radius: radius - 8), _toRad(-90 - 45), _toRad(90), false, Paint() ..color = theme.accentLight ..style = PaintingStyle.stroke ..strokeWidth = 4 ..strokeCap = StrokeCap.round, ); // North mark (always at top of the ring, rotated opposite to heading) final northAngle = _toRad(-headingDeg - 90); final nx = center.dx + radius * math.cos(northAngle); final ny = center.dy + radius * math.sin(northAngle); canvas.drawCircle( Offset(nx, ny), 5, Paint()..color = theme.northColor, ); // Set-point tick if (setPointDeg != null) { final spAngle = _toRad(setPointDeg! - headingDeg - 90); canvas.drawLine( Offset( center.dx + (radius - 12) * math.cos(spAngle), center.dy + (radius - 12) * math.sin(spAngle), ), Offset( center.dx + radius * math.cos(spAngle), center.dy + radius * math.sin(spAngle), ), Paint() ..color = theme.setLight ..strokeWidth = 3 ..strokeCap = StrokeCap.round, ); } // Glow ring (dark themes only) if (theme.accentGlowRadius > 0) { canvas.drawCircle( center, radius - 8, Paint() ..color = theme.accentGlowColor ..style = PaintingStyle.stroke ..strokeWidth = theme.accentGlowRadius / 2 ..maskFilter = MaskFilter.blur(BlurStyle.normal, theme.accentGlowRadius / 2), ); } } @override bool shouldRepaint(_CompassPainter old) => old.headingDeg != headingDeg || old.setPointDeg != setPointDeg || old.theme != theme; }