diff --git a/pi/ui/lib/screens/dashboard_screen.dart b/pi/ui/lib/screens/dashboard_screen.dart index 52e07ab..428d57d 100644 --- a/pi/ui/lib/screens/dashboard_screen.dart +++ b/pi/ui/lib/screens/dashboard_screen.dart @@ -221,8 +221,8 @@ class _DashboardScreenState extends State { child: AccelGraph( ax: _dynamicAx, // Gravity-compensated lateral ay: _dynamicAy, // Gravity-compensated longitudinal - maxG: 1.0, - ghostTrackPeriod: const Duration(seconds: 3), + maxG: 0.8, + ghostTrackPeriod: const Duration(seconds: 4), ), ) ], diff --git a/pi/ui/lib/widgets/accel_graph.dart b/pi/ui/lib/widgets/accel_graph.dart index 7872974..85a6036 100644 --- a/pi/ui/lib/widgets/accel_graph.dart +++ b/pi/ui/lib/widgets/accel_graph.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; @@ -38,55 +37,46 @@ class _AccelGraphState extends State { double _ghostAx = 0; double _ghostAy = 0; double _ghostMagnitude = 0; - Timer? _ghostResetTimer; - @override - void initState() { - super.initState(); - _setupGhostTimer(); - } + // Timestamped history for sliding window + List<({DateTime time, double ax, double ay})> _history = []; @override void didUpdateWidget(AccelGraph oldWidget) { super.didUpdateWidget(oldWidget); - // Update ghost position if current magnitude exceeds previous peak final currentAx = widget.ax ?? 0; final currentAy = widget.ay ?? 0; - final currentMag = math.sqrt(currentAx * currentAx + currentAy * currentAy); + final now = DateTime.now(); - if (currentMag > _ghostMagnitude) { + // Only track history when ghostTrackPeriod is configured + if (widget.ghostTrackPeriod != null) { + // Add current reading to history + _history.add((time: now, ax: currentAx, ay: currentAy)); + + // Prune entries outside the window + final cutoff = now.subtract(widget.ghostTrackPeriod!); + _history.removeWhere((e) => e.time.isBefore(cutoff)); + + // Recalculate ghost as max magnitude from current window _ghostAx = currentAx; _ghostAy = currentAy; - _ghostMagnitude = currentMag; - } + _ghostMagnitude = 0; - // Restart timer if period changed - if (oldWidget.ghostTrackPeriod != widget.ghostTrackPeriod) { - _setupGhostTimer(); + for (final entry in _history) { + final mag = math.sqrt(entry.ax * entry.ax + entry.ay * entry.ay); + if (mag > _ghostMagnitude) { + _ghostAx = entry.ax; + _ghostAy = entry.ay; + _ghostMagnitude = mag; + } + } + } else { + // No window configured - clear history to save memory + _history.clear(); } } - void _setupGhostTimer() { - _ghostResetTimer?.cancel(); - if (widget.ghostTrackPeriod != null) { - _ghostResetTimer = Timer.periodic(widget.ghostTrackPeriod!, (_) { - setState(() { - // Reset ghost to current position - _ghostAx = widget.ax ?? 0; - _ghostAy = widget.ay ?? 0; - _ghostMagnitude = math.sqrt(_ghostAx * _ghostAx + _ghostAy * _ghostAy); - }); - }); - } - } - - @override - void dispose() { - _ghostResetTimer?.cancel(); - super.dispose(); - } - @override Widget build(BuildContext context) { final theme = AppTheme.of(context); @@ -117,6 +107,7 @@ class _AccelGraphState extends State { subdued: theme.subdued, background: theme.background, strokeWeight: strokeSize, + traceBuffer: _history.map((e) => Offset(e.ax, e.ay)).toList(), ), ), ), @@ -185,6 +176,7 @@ class _AccelGraphPainter extends CustomPainter { final Color subdued; final Color background; final double strokeWeight; + final List traceBuffer; _AccelGraphPainter({ required this.ax, @@ -197,6 +189,7 @@ class _AccelGraphPainter extends CustomPainter { required this.subdued, required this.background, required this.strokeWeight, + required this.traceBuffer, }); @override @@ -209,13 +202,13 @@ class _AccelGraphPainter extends CustomPainter { // No rectangular border - // Grid lines at 0.5G intervals + // Grid lines at 0.25G intervals final gridPaint = Paint() ..color = subdued - ..strokeWidth = strokeWeight * 0.6 + ..strokeWidth = strokeWeight * 0.4 ..style = PaintingStyle.stroke; - final gStep = 0.5; + final gStep = 0.25; for (double g = gStep; g < maxG; g += gStep) { final offset = (g / maxG) * halfSize; @@ -263,17 +256,40 @@ class _AccelGraphPainter extends CustomPainter { axisPaint, ); - // G-ring markers (circles at 1G and 2G for quick reference) + // G-ring markers (circles at every 0.5G for quick reference) final ringPaint = Paint() ..color = subdued - ..strokeWidth = strokeWeight + ..strokeWidth = strokeWeight * 0.5 ..style = PaintingStyle.stroke; - for (double g = 1.0; g <= maxG; g += 1.0) { + for (double g = 0.5; g <= maxG; g += 0.5) { final radius = (g / maxG) * halfSize; canvas.drawCircle(center, radius, ringPaint); } + // Trace line + if (traceBuffer.length >= 2) { + final tracePaint = Paint() + ..color = foreground + ..strokeWidth = strokeWeight * 0.4 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + final path = Path(); + for (int i = 0; i < traceBuffer.length; i++) { + final pt = traceBuffer[i]; + final x = center.dx + (pt.dx.clamp(-maxG, maxG) / maxG) * halfSize; + final y = center.dy - (pt.dy.clamp(-maxG, maxG) / maxG) * halfSize; + if (i == 0) { + path.moveTo(x, y); + } else { + path.lineTo(x, y); + } + } + canvas.drawPath(path, tracePaint); + } + // Ghost dot (if enabled and has data) if (showGhost) { final ghostX = center.dx + (ghostAx / maxG) * halfSize; @@ -281,7 +297,7 @@ class _AccelGraphPainter extends CustomPainter { final ghostRadius = halfSize * 0.08; final ghostPaint = Paint() - ..color = subdued.withValues(alpha: 0.5) + ..color = subdued ..strokeWidth = strokeWeight ..style = PaintingStyle.stroke; @@ -311,6 +327,7 @@ class _AccelGraphPainter extends CustomPainter { showGhost != oldDelegate.showGhost || maxG != oldDelegate.maxG || foreground != oldDelegate.foreground || - subdued != oldDelegate.subdued; + subdued != oldDelegate.subdued || + traceBuffer != oldDelegate.traceBuffer; } } diff --git a/pi/ui/lib/widgets/navigator_widget.dart b/pi/ui/lib/widgets/navigator_widget.dart index 31644aa..b16e446 100644 --- a/pi/ui/lib/widgets/navigator_widget.dart +++ b/pi/ui/lib/widgets/navigator_widget.dart @@ -88,8 +88,8 @@ class NavigatorWidgetState extends State animation: _shakeController, child: image, builder: (context, child) { - final shake = sin(_shakeController.value * pi * 6) * 10 * - (1 - _shakeController.value); // 6 oscillations, 4px amplitude, decay + final shake = sin(_shakeController.value * pi * 6) * 25 * + (1 - _shakeController.value); // 6 oscillations, 25px amplitude, decay return Transform.translate( offset: Offset(shake, 0), child: child, diff --git a/pi/ui/lib/widgets/whiskey_mark.dart b/pi/ui/lib/widgets/whiskey_mark.dart index 08593ff..9b08771 100644 --- a/pi/ui/lib/widgets/whiskey_mark.dart +++ b/pi/ui/lib/widgets/whiskey_mark.dart @@ -180,7 +180,7 @@ class _HorizonPainter extends CustomPainter { // Horizon line final linePaint = Paint() ..color = lineColor - ..strokeWidth = 2 + ..strokeWidth = borderWeight * 0.1 ..style = PaintingStyle.stroke; canvas.drawLine( @@ -189,12 +189,34 @@ class _HorizonPainter extends CustomPainter { linePaint, ); + // Pitch ladder lines (15° intervals) + final ladderPaint = Paint() + ..color = lineColor + ..strokeWidth = borderWeight * 0.4 + ..style = PaintingStyle.stroke; + + + + for (int deg = -75; deg <= 75; deg += 15) { + if (deg == 0) continue; // Skip horizon (already drawn) + + final ladderY = horizonY - (deg / 90) * radius; + final double widthMod = (100 - (deg < 0 ? -deg : deg)) / 100; + final ladderWidth = radius * 0.7 * widthMod; // longer ladder if close to horizon + + canvas.drawLine( + Offset(center.dx - ladderWidth, ladderY), + Offset(center.dx + ladderWidth, ladderY), + ladderPaint, + ); + } + canvas.restore(); // Draw circle border final borderPaint = Paint() - ..color = lineColor.withValues(alpha: 0.5) - ..strokeWidth = borderWeight + ..color = lineColor + ..strokeWidth = borderWeight * 1.1 ..style = PaintingStyle.stroke; canvas.drawCircle(center, radius - 1, borderPaint); @@ -202,7 +224,7 @@ class _HorizonPainter extends CustomPainter { // Draw center reference mark (fixed, doesn't rotate) final refPaint = Paint() ..color = lineColor - ..strokeWidth = borderWeight * 0.8 + ..strokeWidth = borderWeight ..style = PaintingStyle.stroke; // Small wings @@ -216,12 +238,24 @@ class _HorizonPainter extends CustomPainter { Offset(center.dx + radius * 0.3, center.dy), refPaint, ); - // Center vertical line + + // Center arrow + final refTipPaint = Paint() + ..color = lineColor + ..strokeWidth = borderWeight * 0.8 + ..style = PaintingStyle.stroke; + canvas.drawLine( - Offset(center.dx, center.dy - radius * 0.05), - Offset(center.dx, center.dy + radius * 0.1), - refPaint, + Offset(center.dx, center.dy), + Offset(center.dx + radius * 0.07, center.dy + radius * 0.1), + refTipPaint, ); + canvas.drawLine( + Offset(center.dx, center.dy), + Offset(center.dx - radius * 0.07, center.dy + radius * 0.1), + refTipPaint, + ); + canvas.restore(); }