Add fillet flight path geometry and Alt/Speed wheels

- RouteGeometry (lib/domain/mission/route_geometry.dart): physically
  grounded flight path - straight segments + tangential arcs at each
  course change, sized from the drone's minimum turn radius (doc 3.7/4.6),
  replacing a naive spline that would suggest unrealistic turn radii.
  Marks a vertex "bad" (turn angle >= 160°, tangent length exceeding 90%
  of either adjacent leg, or exceeding the waypoint's catch radius) and a
  leg "bad" when the required climb/descent rate exceeds the drone's
  max climb/descent rate. Pure Dart, no Flutter dependency, so it stays
  usable if the mission domain is ever split into its own package (4.22).
  Wind-based turn-radius correction from the prototype isn't ported yet -
  the wind system (doc 3.8) doesn't exist in the Flutter app.
- MissionMap now draws each RouteSegment as its own Polyline, colored red
  when bad instead of a single plain white line; waypoint markers turn red
  too when their vertex is bad.
- ValueWheel: vertical drag-to-adjust tape control (HTML prototype's
  #altWheel/#spdWheel), custom-painted tick marks, blue center indicator,
  gradient fade at top/bottom. Wired into PlanScreen on both screen edges
  with Alt/Speed readouts.
- curAlt/curSpeed now live in PlanScreen state instead of fixed constants:
  dropping a new waypoint uses whatever the wheels are currently set to;
  entering editing mode on an existing waypoint loads its values into the
  wheels; adjusting a wheel while editing writes live into that waypoint
  (provider gained setAltitude/setSpeed to match the existing
  moveWaypoint/toggleAction pattern).

Added dedicated unit tests for the geometry (empty list, straight line,
feasible 90° turn producing a line-arc-line segment sequence, an
infeasible near-180° turn, and an infeasible descent rate) rather than
relying on eyeballing it on the emulator - this is exactly the kind of
ported-math correctness that's hard to verify visually but easy to get
subtly wrong. All 11 tests (previous 6 + these 5) and flutter analyze
pass. Also manually verified the wheels on the Pixel_10a emulator: drag
changes the value and the on-screen readout in real time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Constantin Leue
2026-07-27 23:13:24 +02:00
co-authored by Claude Sonnet 5
parent 6dcb765b80
commit b93bbab23b
6 changed files with 617 additions and 20 deletions
+12 -7
View File
@@ -3,6 +3,8 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../domain/mission/route_geometry.dart';
/// Vollbild-OSM-Karte, entspricht dem Leaflet-`#map` im HTML-Demonstrator:
/// randlos, ohne Zoom-Buttons (Pinch-to-Zoom/Pan reicht), Standard-OSM-Tiles.
class MissionMap extends StatelessWidget {
@@ -10,7 +12,7 @@ class MissionMap extends StatelessWidget {
super.key,
this.mapController,
this.waypointMarkers = const [],
this.routeLine,
this.routeSegments = const [],
this.rubberBandLine,
this.onMapReady,
});
@@ -20,10 +22,9 @@ class MissionMap extends StatelessWidget {
/// Kreis-Marker fuer die Wegpunkte der aktuellen Mission (Doku 3.7).
final List<CircleMarker> waypointMarkers;
/// Verbindungslinie zwischen den Wegpunkten. Bewusst eine gerade Strecke -
/// die physikalisch fundierte Geraden+Kreisbogen-Geometrie (Doku 3.7/4.6)
/// ist ein separater, noch offener Schritt.
final List<LatLng>? routeLine;
/// Geraden + Kreisbogen-Segmente des Flugpfads (Doku 3.7/4.6); rot
/// markierte Segmente ueberschreiten Steig-/Sinkrate oder Kurvenradius.
final List<RouteSegment> routeSegments;
/// Gestrichelte Vorschaulinie vom letzten Wegpunkt zur aktuellen
/// Kartenmitte, waehrend noch kein neuer Punkt bestaetigt wurde.
@@ -55,8 +56,12 @@ class MissionMap extends StatelessWidget {
),
PolylineLayer(
polylines: [
if (routeLine != null && routeLine!.length > 1)
Polyline(points: routeLine!, strokeWidth: 3, color: Colors.white),
for (final segment in routeSegments)
Polyline(
points: segment.points,
strokeWidth: 3,
color: segment.bad ? const Color(0xFFE2574A) : Colors.white,
),
if (rubberBandLine != null)
Polyline(
points: rubberBandLine!,