Add waypoint route line, snap-to-edit, and halo action menu
Full reticle interaction state machine (idle/snapped/editing), matching the HTML prototype's mode variable: - idle: reticle shows "+", tap drops a new waypoint at the map center. - snapped: after any pan ends, if the map center lands within the reticle's on-screen radius (converted to meters at the current zoom/latitude) of an existing waypoint, the view auto-recenters onto it and the reticle switches to a pencil icon. Tap to enter editing. - editing: reticle shows a checkmark; panning the map now live-updates the target waypoint's position (drag-to-reposition) instead of moving the camera "away" from it, and the HaloMenu radial menu appears. HaloMenu: three ring-wedge hit regions (custom ClipPath/CustomClipper, angles/radii taken from the prototype's buildRingWedgePath) for Loiter/Landing/Remove, with Material icons standing in for the demonstrator's hand-drawn SVGs. Loiter/Landing toggle the waypoint's action and drop back to snapped; Remove deletes it and returns to idle. MissionMap now draws a straight white polyline connecting all waypoints in order (the physically-grounded straight-line + fillet-arc geometry from doc 3.7/4.6 is still open - this is a straight-line placeholder) plus a dashed rubber-band preview from the last waypoint to the live map center while idle. Note on porting from Leaflet: flutter_map's MapController.move() emits MapEventMove, not MapEventMoveEnd, so (unlike the prototype's panTo()) the auto-recenter-on-snap doesn't re-trigger snap detection - no "autoPanning" guard flag was needed. Verified with flutter analyze, flutter test (three new tests: halo menu open/close, remove, loiter toggle - the wedge tap tests had to target the icon's exact center via tapAt(), since a wedge's bounding-box center falls outside its actual donut-segment hit area), and a full manual pass on the Pixel_10a emulator: idle -> add -> snap (auto-recenter confirmed) -> edit -> drag-reposition (confirmed via re-snap after further panning showing the moved position, not the original one) -> halo remove -> back to idle, plus the connecting line rendering correctly between two waypoints set far apart. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
db6b46140c
commit
6dcb765b80
@@ -5,8 +5,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:dmc_app/app_mode/app_mode_cubit.dart';
|
||||
import 'package:dmc_app/domain/waypoint/flat_waypoint_list.dart';
|
||||
import 'package:dmc_app/ui/app_shell.dart';
|
||||
import 'package:dmc_app/ui/providers/current_mission_provider.dart';
|
||||
import 'package:dmc_app/ui/widgets/halo_menu.dart';
|
||||
import 'package:dmc_app/ui/widgets/reticle_button.dart';
|
||||
|
||||
Widget _wrap(Widget child) => ProviderScope(
|
||||
@@ -72,4 +74,92 @@ void main() {
|
||||
expect(waypoints.single.lat, closeTo(52.3676, 0.0001));
|
||||
expect(waypoints.single.lon, closeTo(4.9041, 0.0001));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Zweiter Tap auf den frisch gesetzten Wegpunkt oeffnet das Halo-Menue',
|
||||
(tester) async {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: BlocProvider(
|
||||
create: (_) => AppModeCubit(),
|
||||
child: const MaterialApp(home: AppShell()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
// 1. Tap: idle -> snapped (legt Wegpunkt an).
|
||||
await tester.tap(find.byType(ReticleButton));
|
||||
await tester.pump();
|
||||
expect(find.byType(HaloMenu), findsNothing);
|
||||
|
||||
// 2. Tap: snapped -> editing.
|
||||
await tester.tap(find.byType(ReticleButton));
|
||||
await tester.pump();
|
||||
expect(find.byType(HaloMenu), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Halo-Remove entfernt den Wegpunkt und schliesst das Menue',
|
||||
(tester) async {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: BlocProvider(
|
||||
create: (_) => AppModeCubit(),
|
||||
child: const MaterialApp(home: AppShell()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byType(ReticleButton)); // idle -> snapped
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(ReticleButton)); // snapped -> editing
|
||||
await tester.pump();
|
||||
|
||||
// Die Wedge-Bounding-Box-Mitte liegt ausserhalb der Keilform (Donut-
|
||||
// Segment) - stattdessen exakt auf das (nicht hit-testbare) Icon tippen,
|
||||
// das per Konstruktion innerhalb des Keils sitzt.
|
||||
await tester.tapAt(tester.getCenter(find.byIcon(Icons.remove)));
|
||||
await tester.pump();
|
||||
|
||||
expect(container.read(currentMissionProvider), isEmpty);
|
||||
expect(find.byType(HaloMenu), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Halo-Loiter setzt die Execute-Aktion und schliesst das Menue',
|
||||
(tester) async {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: BlocProvider(
|
||||
create: (_) => AppModeCubit(),
|
||||
child: const MaterialApp(home: AppShell()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byType(ReticleButton)); // idle -> snapped
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(ReticleButton)); // snapped -> editing
|
||||
await tester.pump();
|
||||
|
||||
await tester.tapAt(tester.getCenter(find.byIcon(Icons.loop)));
|
||||
await tester.pump();
|
||||
|
||||
final waypoints = container.read(currentMissionProvider);
|
||||
expect(waypoints.single.action, WaypointAction.loiter);
|
||||
expect(find.byType(HaloMenu), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user