import 'dart:async'; import 'dart:math' as math; import 'package:sensors_plus/sensors_plus.dart'; /// Statut de parallélisme retourné en temps réel. enum ParallelismStatus { /// Pas encore de données capteur disponibles. unknown, /// L'appareil est bien parallèle à la cible (pitch et roll dans les seuils). aligned, /// L'appareil est trop incliné — l'utilisateur doit repositionner. misaligned, } /// Données de parallélisme calculées à chaque frame capteur. class ParallelismData { final ParallelismStatus status; /// Inclinaison avant/arrière en degrés (0° = parfaitement vertical). final double pitchDegrees; /// Inclinaison gauche/droite en degrés (0° = parfaitement droit). final double rollDegrees; const ParallelismData({ required this.status, required this.pitchDegrees, required this.rollDegrees, }); /// Raccourci : vrai si l'appareil est bien aligné. bool get isAligned => status == ParallelismStatus.aligned; @override String toString() => 'ParallelismData(status: $status, pitch: ${pitchDegrees.toStringAsFixed(1)}°, roll: ${rollDegrees.toStringAsFixed(1)}°)'; } /// Service de détection du parallélisme par accéléromètre. /// /// Utilise le package sensors_plus pour lire l'accéléromètre en continu. /// Calcule le pitch (inclinaison avant/arrière) et le roll (inclinaison latérale). /// /// Un appareil tenu **à la verticale face à la cible** a : /// - pitch ≈ 0° (l'axe X est horizontal) /// - roll ≈ 0° (l'axe Y est vertical, gravité vers le bas) /// /// En mode **paysage** (appareil à l'horizontale), les axes sont permutés — /// le service gère les deux orientations automatiquement. class ParallelismService { /// Seuil d'angle maximum autorisé en degrés. /// En dessous → aligned, au-dessus → misaligned. final double thresholdDegrees; StreamSubscription? _subscription; final StreamController _controller = StreamController.broadcast(); ParallelismService({this.thresholdDegrees = 5.0}); /// Stream de données de parallélisme mis à jour en temps réel. Stream get stream => _controller.stream; /// Démarre l'écoute de l'accéléromètre. /// Appeler au moment d'afficher la caméra live. void start() { if (_subscription != null) return; // Déjà démarré _subscription = accelerometerEventStream( samplingPeriod: SensorInterval.normalInterval, // ~50 ms ).listen( _onAccelerometerEvent, onError: (e) { // En cas d'erreur capteur (simulateur sans IMU par exemple), // on émet un statut "unknown" pour ne pas bloquer l'UI. if (!_controller.isClosed) { _controller.add( const ParallelismData( status: ParallelismStatus.unknown, pitchDegrees: 0, rollDegrees: 0, ), ); } }, ); } /// Stoppe l'écoute de l'accéléromètre. /// Appeler quand l'écran caméra est fermé pour économiser la batterie. void stop() { _subscription?.cancel(); _subscription = null; } /// Libère toutes les ressources. void dispose() { stop(); _controller.close(); } void _onAccelerometerEvent(AccelerometerEvent event) { if (_controller.isClosed) return; // L'accéléromètre retourne (x, y, z) en m/s². // Au repos, la magnitude vaut ~9.81 (gravité terrestre). // // Convention capteur Android/iOS (portrait) : // x → axe horizontal (positif = droite) // y → axe vertical (positif = haut) // z → axe profondeur (positif = vers l'utilisateur) // // On normalise pour être indépendant de la gravité exacte. final double gx = event.x; final double gy = event.y; final double gz = event.z; final double magnitude = math.sqrt(gx * gx + gy * gy + gz * gz); if (magnitude < 1.0) { // Données aberrantes (quasi-nulles) — on ignore return; } // Normalisation final double nx = gx / magnitude; final double ny = gy / magnitude; final double nz = gz / magnitude; // Pitch : inclinaison avant/arrière // En portrait vertical face à l'utilisateur : ny ≈ -1, nz ≈ 0 → pitch ≈ 0° // On calcule l'angle entre le vecteur gravité projeté et l'axe Y pur. final double pitchRad = math.asin(nz.clamp(-1.0, 1.0)); final double pitchDeg = pitchRad * (180.0 / math.pi); // Roll : inclinaison latérale gauche/droite // En portrait droit : nx ≈ 0 → roll ≈ 0° final double rollRad = math.atan2(nx, -ny); final double rollDeg = rollRad * (180.0 / math.pi); final bool isAligned = pitchDeg.abs() <= thresholdDegrees && rollDeg.abs() <= thresholdDegrees; _controller.add( ParallelismData( status: isAligned ? ParallelismStatus.aligned : ParallelismStatus.misaligned, pitchDegrees: pitchDeg, rollDegrees: rollDeg, ), ); } }