- 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>
164 lines
4.8 KiB
Dart
164 lines
4.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Vertikales Drehrad zum Einstellen eines Werts per Swipe (HTML-
|
|
/// Demonstrator: .wheel/#altWheel/#spdWheel + setupWheel()). Der Tick an
|
|
/// der Bildschirmmitte (blauer Indikator) zeigt den aktuellen Wert; nach
|
|
/// oben ziehen erhoeht den Wert.
|
|
class ValueWheel extends StatefulWidget {
|
|
const ValueWheel({
|
|
super.key,
|
|
required this.value,
|
|
required this.min,
|
|
required this.max,
|
|
required this.step,
|
|
required this.pxPerStep,
|
|
required this.majorEvery,
|
|
required this.onChanged,
|
|
});
|
|
|
|
final double value;
|
|
final double min;
|
|
final double max;
|
|
final double step;
|
|
final double pxPerStep;
|
|
final int majorEvery;
|
|
final ValueChanged<double> onChanged;
|
|
|
|
@override
|
|
State<ValueWheel> createState() => _ValueWheelState();
|
|
}
|
|
|
|
class _ValueWheelState extends State<ValueWheel> {
|
|
double? _dragStartY;
|
|
double _dragStartValue = 0;
|
|
|
|
void _onDragStart(DragStartDetails details) {
|
|
_dragStartY = details.globalPosition.dy;
|
|
_dragStartValue = widget.value;
|
|
}
|
|
|
|
void _onDragUpdate(DragUpdateDetails details) {
|
|
final startY = _dragStartY;
|
|
if (startY == null) return;
|
|
final deltaY = startY - details.globalPosition.dy;
|
|
final deltaSteps = deltaY / widget.pxPerStep;
|
|
final raw = _dragStartValue + deltaSteps * widget.step;
|
|
var stepped = (raw / widget.step).round() * widget.step;
|
|
stepped = stepped.clamp(widget.min, widget.max);
|
|
widget.onChanged(stepped);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onVerticalDragStart: _onDragStart,
|
|
onVerticalDragUpdate: _onDragUpdate,
|
|
onVerticalDragEnd: (_) => _dragStartY = null,
|
|
child: Container(
|
|
width: 44,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(22),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.black.withValues(alpha: 0.9),
|
|
const Color(0xFF333333).withValues(alpha: 0.9),
|
|
Colors.black.withValues(alpha: 0.9),
|
|
],
|
|
),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(22),
|
|
child: ShaderMask(
|
|
blendMode: BlendMode.dstIn,
|
|
shaderCallback: (rect) => const LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Colors.transparent,
|
|
Colors.black,
|
|
Colors.black,
|
|
Colors.transparent,
|
|
],
|
|
stops: [0, 0.1, 0.9, 1],
|
|
).createShader(rect),
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Positioned.fill(
|
|
child: CustomPaint(
|
|
painter: _TapePainter(
|
|
value: widget.value,
|
|
min: widget.min,
|
|
max: widget.max,
|
|
step: widget.step,
|
|
pxPerStep: widget.pxPerStep,
|
|
majorEvery: widget.majorEvery,
|
|
),
|
|
),
|
|
),
|
|
Container(height: 2, color: const Color(0xFF4D9DFF)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TapePainter extends CustomPainter {
|
|
const _TapePainter({
|
|
required this.value,
|
|
required this.min,
|
|
required this.max,
|
|
required this.step,
|
|
required this.pxPerStep,
|
|
required this.majorEvery,
|
|
});
|
|
|
|
final double value;
|
|
final double min;
|
|
final double max;
|
|
final double step;
|
|
final double pxPerStep;
|
|
final int majorEvery;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final centerY = size.height / 2;
|
|
final centerX = size.width / 2;
|
|
final majorPaint = Paint()..color = Colors.white.withValues(alpha: 0.85);
|
|
final minorPaint = Paint()..color = Colors.white.withValues(alpha: 0.32);
|
|
|
|
final nearestGridIndex = ((value - min) / step).round();
|
|
final visibleHalfCount = (size.height / 2 / pxPerStep).ceil() + 1;
|
|
|
|
for (var i = -visibleHalfCount; i <= visibleHalfCount; i++) {
|
|
final gridIndex = nearestGridIndex + i;
|
|
final tickValue = min + gridIndex * step;
|
|
if (tickValue < min || tickValue > max) continue;
|
|
|
|
final y = centerY + (tickValue - value) / step * pxPerStep;
|
|
final isMajor = gridIndex % majorEvery == 0;
|
|
final halfWidth = size.width * (isMajor ? 0.35 : 0.19);
|
|
final thickness = isMajor ? 2.0 : 1.0;
|
|
|
|
canvas.drawRect(
|
|
Rect.fromCenter(
|
|
center: Offset(centerX, y),
|
|
width: halfWidth * 2,
|
|
height: thickness,
|
|
),
|
|
isMajor ? majorPaint : minorPaint,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _TapePainter oldDelegate) =>
|
|
oldDelegate.value != value ||
|
|
oldDelegate.min != min ||
|
|
oldDelegate.max != max ||
|
|
oldDelegate.step != step;
|
|
}
|