Files
PilotVault/Fly App/lib/fly/modes/vision_modes.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

174 lines
5.4 KiB
Dart

import 'dart:ui' show Offset, Rect, Size;
import 'package:flutter/foundation.dart';
/// What the pilot has chosen in an ActiveTrack or TapFly session, before and
/// while the operator runs: shared by the video overlay and the mode panel.
class VisionSession extends ChangeNotifier {
TrackMode trackMode = TrackMode.trace;
TapFlyDirection direction = TapFlyDirection.forward;
double speed = 5;
/// A box drawn (or tapped) on the video, 0..1, waiting for GO.
Rect? pendingBox;
/// When [pendingBox] is a subject the aircraft found itself: its index.
int? pendingSubject;
/// A TapFly target, 0..1, waiting for GO.
Offset? pendingPoint;
/// Trace may fly backwards ahead of a subject coming towards it; null until read.
bool? retreat;
void setRetreat(bool? on) {
retreat = on;
notifyListeners();
}
/// Picks an auto-sensed subject: its box with its index.
void pickSubject(int index, Rect box) {
pendingBox = box;
pendingSubject = index;
notifyListeners();
}
void setTrackMode(TrackMode m) {
trackMode = m;
notifyListeners();
}
void setDirection(TapFlyDirection d) {
direction = d;
notifyListeners();
}
void setSpeed(double v) {
speed = v;
notifyListeners();
}
void setPendingBox(Rect? r) {
pendingBox = r;
pendingSubject = null;
notifyListeners();
}
void setPendingPoint(Offset? p) {
pendingPoint = p;
notifyListeners();
}
void clearPending() {
if (pendingBox == null && pendingPoint == null) return;
pendingBox = null;
pendingSubject = null;
pendingPoint = null;
notifyListeners();
}
}
/// ActiveTrack's follow styles (plan P7b, GO 4's Trace / Profile / Spotlight).
enum TrackMode {
/// Follows behind (or ahead of) the subject.
trace('Trace', 'TRACE'),
/// Flies alongside it.
profile('Profile', 'PROFILE'),
/// Stays put and keeps the camera on it; the pilot flies.
spotlight('Spotlight', 'SPOTLIGHT');
const TrackMode(this.label, this.sdk);
final String label;
final String sdk;
static TrackMode? fromSdk(String? s) {
for (final TrackMode m in values) {
if (m.sdk == s) return m;
}
return null;
}
}
/// TapFly's directions (GO 4's Forward / Backward / Free).
enum TapFlyDirection {
forward('Forward', 'FORWARD'),
backward('Backward', 'BACKWARD'),
/// Flies towards the point while the pilot turns the aircraft freely.
free('Free', 'FREE');
const TapFlyDirection(this.label, this.sdk);
final String label;
final String sdk;
}
/// The live-view geometry the touch layers share: screen points to the
/// operators' 0..1 picture coordinates.
///
/// The native view letterboxes the feed to its own aspect ratio, so the
/// picture is [contentRect] — centred in the view, with black bars on two
/// sides. Every mapping goes through that rect; a touch on a bar is no touch.
abstract final class VisionGeometry {
/// A drawn box smaller than this (px, either side) is taken as a tap.
static const double minDrag = 24;
/// The box a tap makes, as a fraction of the picture: a person-sized window.
static const Size tapBox = Size(0.12, 0.22);
/// Where the picture of size [video] sits inside [view] (aspect fit,
/// centred); the whole view while the feed size is unknown.
static Rect contentRect(Size view, Size? video) {
if (video == null || video.isEmpty || view.isEmpty) return Offset.zero & view;
final double aspect = video.width / video.height;
final double w, h;
if (view.width / view.height > aspect) {
h = view.height;
w = h * aspect;
} else {
w = view.width;
h = w / aspect;
}
return Rect.fromLTWH((view.width - w) / 2, (view.height - h) / 2, w, h);
}
/// The 0..1 box between two corners of a drag, clipped to the picture; null
/// when it is too small to be a deliberate box, or misses the picture.
static Rect? dragBox(Offset a, Offset b, Rect content) {
if ((a.dx - b.dx).abs() < minDrag || (a.dy - b.dy).abs() < minDrag) return null;
final Rect r = Rect.fromPoints(a, b).intersect(content);
if (r.width < minDrag || r.height < minDrag) return null;
return Rect.fromLTRB(
(r.left - content.left) / content.width,
(r.top - content.top) / content.height,
(r.right - content.left) / content.width,
(r.bottom - content.top) / content.height,
);
}
/// A [tapBox] centred on a tap, kept inside the picture; null on a bar.
static Rect? tapBoxAt(Offset p, Rect content) {
final Offset? n = normalise(p, content);
if (n == null) return null;
final double cx = n.dx.clamp(tapBox.width / 2, 1 - tapBox.width / 2);
final double cy = n.dy.clamp(tapBox.height / 2, 1 - tapBox.height / 2);
return Rect.fromCenter(center: Offset(cx, cy), width: tapBox.width, height: tapBox.height);
}
/// [p] as 0..1 of the picture; null when it falls on a bar.
static Offset? normalise(Offset p, Rect content) {
if (!content.contains(p)) return null;
return Offset((p.dx - content.left) / content.width, (p.dy - content.top) / content.height);
}
static Rect toScreen(Rect r, Rect content) => Rect.fromLTRB(
content.left + r.left * content.width,
content.top + r.top * content.height,
content.left + r.right * content.width,
content.top + r.bottom * content.height,
);
static Offset pointToScreen(Offset p, Rect content) => Offset(content.left + p.dx * content.width, content.top + p.dy * content.height);
}