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:
co-authored by
Claude Sonnet 5
parent
6dcb765b80
commit
b93bbab23b
@@ -0,0 +1,75 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user