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>
145 lines
5.0 KiB
Dart
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;
|
|
}
|