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); }