fix: parallelisme service permissif et correctif capture screen
This commit is contained in:
@@ -36,7 +36,8 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
|
||||
// NOUVEAU : Service IMU de parallélisme
|
||||
final ParallelismService _parallelismService = ParallelismService(
|
||||
thresholdDegrees: 5.0, // ±5° max autorisés
|
||||
alignThreshold: 15.0, // Seuil pour passer au vert
|
||||
misalignThreshold: 22.0, // Seuil pour repasser à l'orange
|
||||
);
|
||||
|
||||
String? _selectedImagePath;
|
||||
@@ -815,4 +816,4 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ enum ParallelismStatus {
|
||||
/// Pas encore de données capteur disponibles.
|
||||
unknown,
|
||||
|
||||
/// L'appareil est bien parallèle à la cible (pitch et roll dans les seuils).
|
||||
/// L'appareil est bien parallèle à la cible.
|
||||
aligned,
|
||||
|
||||
/// L'appareil est trop incliné — l'utilisateur doit repositionner.
|
||||
/// L'appareil est trop incliné.
|
||||
misaligned,
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ class ParallelismData {
|
||||
required this.rollDegrees,
|
||||
});
|
||||
|
||||
/// Raccourci : vrai si l'appareil est bien aligné.
|
||||
bool get isAligned => status == ParallelismStatus.aligned;
|
||||
|
||||
@override
|
||||
@@ -40,62 +39,69 @@ class ParallelismData {
|
||||
|
||||
/// 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).
|
||||
/// Implémente une hystérésis à deux seuils pour éviter le clignotement :
|
||||
///
|
||||
/// 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)
|
||||
/// État actuel = misaligned → passe à aligned si angle < [alignThreshold]
|
||||
/// État actuel = aligned → passe à misaligned si angle > [misalignThreshold]
|
||||
///
|
||||
/// En mode **paysage** (appareil à l'horizontale), les axes sont permutés —
|
||||
/// le service gère les deux orientations automatiquement.
|
||||
/// Cela crée une "zone de confort" entre les deux seuils où le statut
|
||||
/// ne change pas — le vert reste vert même si la main tremble légèrement.
|
||||
///
|
||||
/// Valeurs par défaut recommandées :
|
||||
/// alignThreshold = 15° (seuil d'entrée dans le vert — assez souple)
|
||||
/// misalignThreshold = 22° (seuil de sortie du vert — tolérant au tremblement)
|
||||
class ParallelismService {
|
||||
/// Seuil d'angle maximum autorisé en degrés.
|
||||
/// En dessous → aligned, au-dessus → misaligned.
|
||||
final double thresholdDegrees;
|
||||
/// Angle max pour passer de misaligned → aligned.
|
||||
final double alignThreshold;
|
||||
|
||||
/// Angle à dépasser pour passer de aligned → misaligned.
|
||||
/// Doit être > alignThreshold pour créer la zone d'hystérésis.
|
||||
final double misalignThreshold;
|
||||
|
||||
StreamSubscription<AccelerometerEvent>? _subscription;
|
||||
final StreamController<ParallelismData> _controller =
|
||||
StreamController<ParallelismData>.broadcast();
|
||||
StreamController<ParallelismData>.broadcast();
|
||||
|
||||
ParallelismService({this.thresholdDegrees = 5.0});
|
||||
/// État interne mémorisé entre deux frames (cœur de l'hystérésis).
|
||||
ParallelismStatus _currentStatus = ParallelismStatus.unknown;
|
||||
|
||||
ParallelismService({
|
||||
this.alignThreshold = 15.0,
|
||||
this.misalignThreshold = 22.0,
|
||||
}) : assert(
|
||||
misalignThreshold > alignThreshold,
|
||||
'misalignThreshold doit être supérieur à alignThreshold',
|
||||
);
|
||||
|
||||
/// Stream de données de parallélisme mis à jour en temps réel.
|
||||
Stream<ParallelismData> 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é
|
||||
if (_subscription != null) return;
|
||||
|
||||
_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.
|
||||
onError: (_) {
|
||||
// Simulateur ou capteur absent — on reste en "unknown" sans bloquer l'UI
|
||||
if (!_controller.isClosed) {
|
||||
_controller.add(
|
||||
const ParallelismData(
|
||||
status: ParallelismStatus.unknown,
|
||||
pitchDegrees: 0,
|
||||
rollDegrees: 0,
|
||||
),
|
||||
);
|
||||
_currentStatus = ParallelismStatus.unknown;
|
||||
_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;
|
||||
_currentStatus = ParallelismStatus.unknown;
|
||||
}
|
||||
|
||||
/// Libère toutes les ressources.
|
||||
void dispose() {
|
||||
stop();
|
||||
_controller.close();
|
||||
@@ -104,53 +110,53 @@ class ParallelismService {
|
||||
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;
|
||||
}
|
||||
if (magnitude < 1.0) return; // Données aberrantes
|
||||
|
||||
// Normalisation
|
||||
// Normalisation par la magnitude réelle (indépendant de g exact)
|
||||
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);
|
||||
// Pitch : inclinaison avant/arrière (téléphone qui pointe vers le haut/bas)
|
||||
final double pitchDeg = math.asin(nz.clamp(-1.0, 1.0)) * (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);
|
||||
// Roll : inclinaison latérale (téléphone penché à gauche/droite)
|
||||
final double rollDeg = math.atan2(nx, -ny) * (180.0 / math.pi);
|
||||
|
||||
final bool isAligned =
|
||||
pitchDeg.abs() <= thresholdDegrees &&
|
||||
rollDeg.abs() <= thresholdDegrees;
|
||||
// Angle combiné = le pire des deux axes
|
||||
final double worstAngle = math.max(pitchDeg.abs(), rollDeg.abs());
|
||||
|
||||
_controller.add(
|
||||
ParallelismData(
|
||||
status: isAligned
|
||||
? ParallelismStatus.aligned
|
||||
: ParallelismStatus.misaligned,
|
||||
pitchDegrees: pitchDeg,
|
||||
rollDegrees: rollDeg,
|
||||
),
|
||||
);
|
||||
// ── Hystérésis ──────────────────────────────────────────────────────────
|
||||
// Premier appel : on décide selon alignThreshold uniquement
|
||||
if (_currentStatus == ParallelismStatus.unknown) {
|
||||
_currentStatus = worstAngle <= alignThreshold
|
||||
? ParallelismStatus.aligned
|
||||
: ParallelismStatus.misaligned;
|
||||
}
|
||||
// Déjà misaligned → devient aligned seulement si on passe sous alignThreshold
|
||||
else if (_currentStatus == ParallelismStatus.misaligned) {
|
||||
if (worstAngle <= alignThreshold) {
|
||||
_currentStatus = ParallelismStatus.aligned;
|
||||
}
|
||||
}
|
||||
// Déjà aligned → devient misaligned seulement si on dépasse misalignThreshold
|
||||
else if (_currentStatus == ParallelismStatus.aligned) {
|
||||
if (worstAngle > misalignThreshold) {
|
||||
_currentStatus = ParallelismStatus.misaligned;
|
||||
}
|
||||
// Entre alignThreshold et misalignThreshold → on garde le vert, on ne change rien
|
||||
}
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
_controller.add(ParallelismData(
|
||||
status: _currentStatus,
|
||||
pitchDegrees: pitchDeg,
|
||||
rollDegrees: rollDeg,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user