- 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>
76 lines
2.6 KiB
Dart
76 lines
2.6 KiB
Dart
import 'package:dmc_app/domain/mission/route_geometry.dart';
|
|
import 'package:dmc_app/domain/waypoint/flat_waypoint_list.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
Waypoint _wp(double lat, double lon, {double alt = 60, double speed = 15}) =>
|
|
Waypoint(lat: lat, lon: lon, altitudeM: alt, speedMs: speed, catchRadiusM: 60);
|
|
|
|
void main() {
|
|
group('buildRouteGeometry', () {
|
|
const params = (minTurnRadius: 35.0, maxClimbRate: 5.0, maxDescentRate: 6.0);
|
|
|
|
test('leere Wegpunktliste liefert leere Geometrie', () {
|
|
final geometry = buildRouteGeometry(
|
|
const [],
|
|
minTurnRadius: params.minTurnRadius,
|
|
maxClimbRate: params.maxClimbRate,
|
|
maxDescentRate: params.maxDescentRate,
|
|
);
|
|
|
|
expect(geometry.segments, isEmpty);
|
|
expect(geometry.vertexBad, isEmpty);
|
|
});
|
|
|
|
test('zwei Wegpunkte ergeben eine einzelne Geradenverbindung', () {
|
|
final geometry = buildRouteGeometry(
|
|
[_wp(0, 0), _wp(0, 0.01)],
|
|
minTurnRadius: params.minTurnRadius,
|
|
maxClimbRate: params.maxClimbRate,
|
|
maxDescentRate: params.maxDescentRate,
|
|
);
|
|
|
|
expect(geometry.segments, hasLength(1));
|
|
expect(geometry.segments.single.bad, isFalse);
|
|
expect(geometry.vertexBad, [false, false]);
|
|
});
|
|
|
|
test('sanfte 90-Grad-Kurve mit ausreichend Platz ist fliegbar', () {
|
|
final geometry = buildRouteGeometry(
|
|
[_wp(0, 0), _wp(0, 0.01), _wp(0.01, 0.01)],
|
|
minTurnRadius: params.minTurnRadius,
|
|
maxClimbRate: params.maxClimbRate,
|
|
maxDescentRate: params.maxDescentRate,
|
|
);
|
|
|
|
expect(geometry.vertexBad[1], isFalse);
|
|
// Gerade -> Bogen -> Gerade.
|
|
expect(geometry.segments, hasLength(3));
|
|
expect(geometry.segments[1].points.length, greaterThan(2));
|
|
});
|
|
|
|
test('scharfe Kehrtwende ueberschreitet den maximalen Kurswinkel', () {
|
|
final geometry = buildRouteGeometry(
|
|
[_wp(0, 0), _wp(0, 0.01), _wp(0, 0.001)],
|
|
minTurnRadius: params.minTurnRadius,
|
|
maxClimbRate: params.maxClimbRate,
|
|
maxDescentRate: params.maxDescentRate,
|
|
);
|
|
|
|
expect(geometry.vertexBad[1], isTrue);
|
|
});
|
|
|
|
test('zu steiler Sinkflug markiert das Geradenstueck als bad', () {
|
|
// ~1113 m horizontal bei 15 m/s ~ 74s Flugzeit; 500 m Sinken darin
|
|
// ergibt eine Sinkrate weit ueber der maxDescentRate von 6 m/s.
|
|
final geometry = buildRouteGeometry(
|
|
[_wp(0, 0, alt: 500), _wp(0, 0.01, alt: 0)],
|
|
minTurnRadius: params.minTurnRadius,
|
|
maxClimbRate: params.maxClimbRate,
|
|
maxDescentRate: params.maxDescentRate,
|
|
);
|
|
|
|
expect(geometry.segments.single.bad, isTrue);
|
|
});
|
|
});
|
|
}
|