Files
PilotVault/Fly App/lib/ui/fly/layers/video_layer.dart
T
tajniak81andClaude Opus 5.5 e49f318a0e Finish the Fly App's ActiveTrack and TapFly extras and letterbox the feed
Auto-detect: on aircraft that can sense subjects themselves, the
ActiveTrack panel offers Auto-detect; the subjects it finds show as dashed
boxes on the video, and tapping one then GO follows it by its index.

Trace gets a Backward switch (fly ahead of a subject walking towards the
aircraft, obstacle-sensing aircraft only) and TapFly Free a Reset heading
button. Settings > Vision gains Gesture mode and Palm control where the
flight assistant supports them, and the HUD shows when palm control is
flying the aircraft.

The live view used to stretch the feed over the screen. It now fits the
picture to the decoder's own size with black bars, and reports that size,
so the ActiveTrack / TapFly overlays, tap-to-focus / meter and the grid
all map into the picture and ignore taps on the bars.

Not yet run on a simulator or aircraft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:23:37 +02:00

145 lines
5.0 KiB
Dart

import 'package:flutter/foundation.dart' show ValueListenable;
import 'package:flutter/material.dart';
import '../../../fly/fly_prefs.dart';
import '../../../fly/modes/vision_modes.dart' show VisionGeometry;
import '../../dji_video_view.dart';
import '../../pv_icons.dart';
/// The live feed — or, with nothing connected, a painted stand-in so the HUD
/// can still be laid out and reviewed.
class VideoLayer extends StatelessWidget {
const VideoLayer({super.key, required this.connected});
final bool connected;
@override
Widget build(BuildContext context) =>
connected ? const DjiVideoView() : const CustomPaint(painter: FeedPlaceholderPainter());
}
/// The visual overlays drawn straight on the video: the grid and the centre
/// point, as set in the camera menu (`FlyPrefs`). Never takes touches.
///
/// The grid divides the picture, not the letterbox bars around it: [video] is
/// the feed's size, as the native view fits it.
class VisualOverlays extends StatelessWidget {
const VisualOverlays({super.key, this.video});
final ValueListenable<Size?>? video;
@override
Widget build(BuildContext context) {
final Widget grid = ValueListenableBuilder<GridStyle>(
valueListenable: FlyPrefs.grid,
builder: (BuildContext context, GridStyle g, _) =>
g == GridStyle.off ? const SizedBox.shrink() : CustomPaint(painter: GridPainter(diagonals: g == GridStyle.diagonals)),
);
return IgnorePointer(
child: Stack(fit: StackFit.expand, children: <Widget>[
if (video == null)
grid
else
LayoutBuilder(
builder: (BuildContext context, BoxConstraints c) => ValueListenableBuilder<Size?>(
valueListenable: video!,
builder: (BuildContext context, Size? v, Widget? child) =>
Stack(children: <Widget>[Positioned.fromRect(rect: VisionGeometry.contentRect(c.biggest, v), child: child!)]),
child: grid,
),
),
ValueListenableBuilder<bool>(
valueListenable: FlyPrefs.centerPoint,
builder: (BuildContext context, bool on, _) => on
? const Center(child: PVIcon('crosshair', size: 30, stroke: 1.1, color: Color(0xB3FFFFFF)))
: const SizedBox.shrink(),
),
]),
);
}
}
/// Rule-of-thirds grid, optionally with the two diagonals.
class GridPainter extends CustomPainter {
const GridPainter({this.diagonals = false});
final bool diagonals;
@override
void paint(Canvas canvas, Size size) {
final Paint p = Paint()
..color = const Color(0x33FFFFFF)
..strokeWidth = 1;
for (int i = 1; i < 3; i++) {
final double x = size.width * i / 3, y = size.height * i / 3;
canvas.drawLine(Offset(x, 0), Offset(x, size.height), p);
canvas.drawLine(Offset(0, y), Offset(size.width, y), p);
}
if (diagonals) {
canvas.drawLine(Offset.zero, Offset(size.width, size.height), p);
canvas.drawLine(Offset(size.width, 0), Offset(0, size.height), p);
}
}
@override
bool shouldRepaint(covariant GridPainter oldDelegate) => oldDelegate.diagonals != diagonals;
}
/// Painted placeholder feed: graded sky → ground, haze, a perspective grid and
/// a few distant buildings.
class FeedPlaceholderPainter extends CustomPainter {
const FeedPlaceholderPainter();
@override
void paint(Canvas canvas, Size size) {
final double w = size.width, h = size.height;
final double horizon = h * 0.52;
final Rect full = Offset.zero & size;
canvas.drawRect(
full,
Paint()
..shader = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[Color(0xFF2A4E86), Color(0xFF3E6199), Color(0xFF1C2A1E), Color(0xFF0E1710)],
stops: <double>[0.0, 0.51, 0.54, 1.0],
).createShader(full),
);
final Rect hazeRect = Rect.fromLTWH(0, horizon - 30, w, 60);
canvas.drawRect(
hazeRect,
Paint()
..shader = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[Color(0x808FB4D8), Color(0x008FB4D8)],
).createShader(hazeRect),
);
final Paint bld = Paint()..color = const Color(0xE612201A);
void building(double x, double y, double bw, double bh) => canvas.drawRect(Rect.fromLTWH(x * w, horizon + y, bw, bh), bld);
building(0.10, -40, 46, 40);
building(0.17, -52, 30, 52);
building(0.74, -46, 54, 46);
building(0.83, -34, 34, 34);
final Paint grid = Paint()
..color = const Color(0x297FE0B0)
..strokeWidth = 1;
for (int i = 1; i <= 5; i++) {
final double t = i / 5.0;
final double y = horizon + (h - horizon) * t * t;
canvas.drawLine(Offset(0, y), Offset(w, y), grid);
}
final double vx = w / 2;
for (int k = -6; k <= 6; k += 2) {
canvas.drawLine(Offset(vx + k * 10, horizon), Offset(vx + k * (w * 0.16), h), grid);
}
}
@override
bool shouldRepaint(covariant FeedPlaceholderPainter oldDelegate) => false;
}