Files
PilotVault/Fly App/lib/ui/pv_icons.dart
T
tajniak81andClaude Opus 5.5 6ebc567e97 Put every flight command behind a SafetyGate
Take-off, land, RTH, the landing cancel/confirm and Stop now all go
through one SafetyGate (lib/fly/safety_gate.dart). The gate sorts its
preconditions into blockers and warnings: connection, flight state,
battery against the thresholds, IMU warm-up, compass, critical
diagnostics, GPS, home point, SD card and the RTH battery budget. It
checks again right before sending.

The left rail is now a state machine (lib/fly/action_rail_state.dart):
Take-off becomes Land, then Cancel or Confirm landing. RTH becomes
Cancel RTH. A Stop button shows while an intelligent mode is flying.
Moving the aircraft needs a slide-to-confirm dialog, which can offer
Precision Takeoff or set the RTH altitude. A blocked command shows why
and has no slider. When the pilot is more than 30 m from home, RTH asks
whether to return to them or to the original home point.

The HUD gains an error stack. Its cards can be dismissed, it fits the
space it has, and a new warning triggers haptic feedback. It also gets a
smart-RTH countdown banner with Cancel / Go home now, a forced-landing
banner, and an Aircraft Status sheet. The sheet opens from the status
chip and appears before each connection's first take-off. It offers
Calibrate compass/IMU and Format SD.

On the bridge side this adds precisionTakeOff, setHomeLocation, compass
and IMU calibration start/stop, formatStorage, and compass health in the
telemetry tick.

The classic flight screen stays available until the simulator run passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:24:17 +02:00

136 lines
7.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
/// The v2 UI-kit line-icon library.
///
/// Ported verbatim from the `P = {…}` SVG-path map in
/// `Design/PilotVault Project logo/ui_kits/fly/index-v2.html`. Every icon is a
/// single `d` attribute drawn on a 24×24 grid with round caps/joins, matching
/// the kit's `<Icon>` component. Rendered through [PVIcon].
class PvPaths {
PvPaths._();
static const Map<String, String> d = <String, String>{
// nav / system
'chevronLeft': 'M15 6l-6 6 6 6',
'chevronRight': 'M9 6l6 6-6 6',
'chevronDown': 'M6 9l6 6 6-6',
'close': 'M6 6l12 12M18 6L6 18',
'more': 'M5 12h.01M12 12h.01M19 12h.01',
'home': 'M3 10.5 12 3l9 7.5M5 9.5V21h14V9.5',
'settings':
'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z',
// flight status
'satellite':
'M13 7 9 3 5 7l4 4M17 11l4 4-4 4-4-4M8.5 8.5 3 14l7 7 5.5-5.5M16 16l1-1M18 3a3 3 0 0 1 3 3',
'battery': 'M3 8h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H3a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1zM22 11v2',
'radio':
'M4.9 19.1a10 10 0 0 1 0-14.2M7.8 16.2a6 6 0 0 1 0-8.4M16.2 7.8a6 6 0 0 1 0 8.4M19.1 4.9a10 10 0 0 1 0 14.2M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2z',
'rc': 'M7 8h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3zM8 4v4M16 4v4M9 14h2M8 13v2M15 13h.01M17 15h.01',
'obstacle': 'M12 3 2 20h20L12 3zM12 10v4M12 17h.01',
'compass': 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM15.5 8.5l-2 5-5 2 2-5 5-2z',
'gimbal': 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 8v8M8 12h8',
'crosshair': 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM22 12h-4M6 12H2M12 6V2M12 22v-4',
'drone':
'M6 6l3.5 3.5M18 6l-3.5 3.5M6 18l3.5-3.5M18 18l-3.5-3.5M6 6a2.5 2.5 0 1 0-.01-.01M18 6a2.5 2.5 0 1 0-.01 0M6 18a2.5 2.5 0 1 0-.01 0M18 18a2.5 2.5 0 1 0-.01 0M9.5 9.5h5v5h-5z',
'takeoff': 'M12 20V8m0 0-4 4m4-4 4 4M4 4h16',
'land': 'M12 4v12m0 0-4-4m4 4 4-4M4 20h16',
'stop': 'M7 7h10v10H7z',
'alert': 'M12 3 2 20h20L12 3zM12 10v4M12 17h.01',
'chevronsRight': 'M6 6l6 6-6 6M13 6l6 6-6 6',
'rth': 'M12 3a9 9 0 1 0 9 9M12 3v6l4 2M20 4l-3 1 1-3',
'map': 'M9 4 3 6v14l6-2 6 2 6-2V4l-6 2-6-2zM9 4v14M15 6v14',
'pin': 'M12 21s7-6.4 7-12A7 7 0 0 0 5 9c0 5.6 7 12 7 12zM12 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4z',
'route': 'M6 19a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 13V9a4 4 0 0 1 4-4h4',
// camera
'video': 'M23 7l-7 5 7 5V7zM3 5h11a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z',
'image': 'M3 3h18v18H3zM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM21 15l-5-5L5 21',
'aperture':
'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM14.3 8 20 8M12.6 4.5 9.8 9.3M7 6 10 11.2M9.7 16 4 16M11.4 19.5l2.8-4.8M17 18l-3-5.2',
'iso': 'M4 7v10M8 7c-2 0-2 5 0 5s2 5 0 5M14 7a3 5 0 0 1 0 10 3 5 0 0 1 0-10M19 7v10',
'shutter': 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 12l6-3M12 12l-3 6M12 12l-3-6M12 12l6 3',
'ev': 'M4 6h10M4 12h7M4 18h10M17 9v6M20 12h-6',
'wb': 'M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM12 2v3M12 19v3M5 12H2M22 12h-3',
'timer': 'M12 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM12 10v4l2 2M9 2h6',
'burst': 'M7 8h10v10H7zM5 6h10M5 10v6',
'film': 'M3 3h18v18H3zM7 3v18M17 3v18M3 8h4M3 16h4M17 8h4M17 16h4',
'pano': 'M2 7l20-3v16L2 17V7zM2 7v10',
'grid': 'M3 3h18v18H3zM3 9h18M3 15h18M9 3v18M15 3v18',
'histogram': 'M4 20V10M9 20V4M14 20v-8M19 20v-6',
'focus':
'M3 8V5a2 2 0 0 1 2-2h3M16 3h3a2 2 0 0 1 2 2v3M21 16v3a2 2 0 0 1-2 2h-3M8 21H5a2 2 0 0 1-2-2v-3M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
'slowmo': 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM10 9v6l5-3z',
'hyperlapse': 'M12 8v4l3 2M4 12a8 8 0 1 1 3 6M4 20v-4h4',
'master': 'M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8-5.2-2.7-5.2 2.7 1-5.8L3.5 9.2l5.9-.9L12 3z',
'hdr': 'M4 8v8M4 12h4M8 8v8M12 8v8h2a3 3 0 0 0 0-6M12 12h2M18 8v8M18 12h2l1 4',
// quickshots
'dronie': 'M12 4a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM4 20l8-8 8 8',
'rocket': 'M12 3c4 2 5 7 5 10l-2 3H9l-2-3c0-3 1-8 5-10zM12 9v.01M8 17l-3 4M16 17l3 4',
'circle': 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z',
'helix': 'M6 3c12 3-12 15 0 18M6 6c9 2.2-9 11.6 0 13.5',
'boomerang': 'M4 20C4 10 10 4 20 4c0 10-6 16-16 16zM4 20l6-6',
'asteroid': 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM3 12h18M8 4c-2 2.5-2 13.5 0 16M16 4c2 2.5 2 13.5 0 16',
// home / library / misc
'academy': 'M22 10 12 5 2 10l10 5 10-5zM6 12v5c0 1 2.7 2.5 6 2.5s6-1.5 6-2.5v-5',
'album': 'M3 3h18v18H3zM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM21 15l-5-5L5 21',
'gauge': 'M12 15a3 3 0 1 0 0-6M3.5 18a9 9 0 1 1 17 0',
'user': 'M20 21a8 8 0 1 0-16 0M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
'sdcard': 'M18 2H8L4 6v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zM10 6v2M13 6v2M16 6v2',
'link': 'M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1',
'play': 'M8 5v14l11-7z',
'pause': 'M8 5v14M16 5v14',
'sun': 'M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4',
'bell': 'M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 0 1-3.46 0',
'share': 'M4 12v8a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-8M16 6l-4-4-4 4M12 2v13',
'search': 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.3-4.3',
'check': 'M20 6 9 17l-5-5',
'shield': 'M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z',
'download': 'M12 3v12m0 0-4-4m4 4 4-4M4 21h16',
'play2': 'M8 5v14l11-7z',
};
}
/// A single v2 line icon. Strokes one of [PvPaths.d] on a 24×24 grid, round
/// caps/joins, matching the design's `<Icon>` component.
class PVIcon extends StatelessWidget {
const PVIcon(
this.name, {
super.key,
this.size = 20,
this.color = Colors.white,
this.stroke = 2,
this.fill = false,
});
final String name;
final double size;
final Color color;
final double stroke;
/// When true (e.g. play triangles), the path is filled with [color] and drawn
/// with no stroke — matches the kit's `fill="#fff" stroke="#fff"` usage.
final bool fill;
@override
Widget build(BuildContext context) {
final String d = PvPaths.d[name] ?? '';
final String hex = _hex(color);
final String fillAttr = fill ? hex : 'none';
final String strokeAttr = fill ? 'none' : hex;
final String svg =
'<svg xmlns="http://www.w3.org/2000/svg" width="$size" height="$size" '
'viewBox="0 0 24 24" fill="$fillAttr" stroke="$strokeAttr" '
'stroke-width="$stroke" stroke-linecap="round" stroke-linejoin="round">'
'<path d="$d"/></svg>';
return SvgPicture.string(svg, width: size, height: size);
}
static String _hex(Color c) {
final int r = (c.r * 255).round();
final int g = (c.g * 255).round();
final int b = (c.b * 255).round();
String two(int v) => v.toRadixString(16).padLeft(2, '0');
return '#${two(r)}${two(g)}${two(b)}';
}
}