336 lines
11 KiB
Dart
336 lines
11 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
import '../../../../services/tilt_service.dart';
|
|
|
|
/// Rúd-libella buborék — kis kör alakú widget a kitűzőpanel fejlécébe.
|
|
///
|
|
/// Színküszöbök a DŐLÉSBŐL SZÁMOLT VÍZSZINTES HIBA szerint (a rúdmagasság
|
|
/// figyelembevételével), mert terepen ez a beszédes érték:
|
|
/// zöld < 1,5 cm · sárga < 3,5 cm · piros felette.
|
|
/// Mozgás közben és kalibrálatlanul szürke/kérdőjeles.
|
|
/// Koppintásra részletes nézet: nagy buborék, számok, rúdmagasság,
|
|
/// vezetett (1 vagy 2 lépéses) kalibráció.
|
|
class TiltBubble extends StatelessWidget {
|
|
const TiltBubble({super.key, this.size = 34});
|
|
|
|
final double size;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!Get.isRegistered<TiltService>()) return const SizedBox.shrink();
|
|
|
|
return Obx(() {
|
|
final t = TiltService.to;
|
|
final inactive = !t.isCalibrated.value || t.isMoving.value;
|
|
|
|
final errCm = t.horizontalErrorCm;
|
|
final color = inactive
|
|
? Colors.grey
|
|
: errCm < 1.5
|
|
? Colors.green
|
|
: errCm < 3.5
|
|
? Colors.orange
|
|
: Colors.red;
|
|
|
|
return Tooltip(
|
|
message: !t.isCalibrated.value
|
|
? 'Libella: nincs kalibrálva — koppints!'
|
|
: t.isMoving.value
|
|
? 'Libella: mozgásban'
|
|
: 'Dőlés ${t.tiltDeg.value.toStringAsFixed(1)}° '
|
|
'≈ ${errCm.toStringAsFixed(1)} cm',
|
|
child: InkWell(
|
|
onTap: () => _showDetail(context),
|
|
customBorder: const CircleBorder(),
|
|
child: SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: _BubbleFace(
|
|
size: size,
|
|
color: color,
|
|
inactive: inactive,
|
|
uncalibrated: !t.isCalibrated.value,
|
|
// A buborék a "lejtés" irányába csúszik — skála: 1 fok
|
|
// dőlés ≈ a sugár ~25%-a, telítésig.
|
|
dx: t.bubbleX.value,
|
|
dy: t.bubbleY.value,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
// Részletes nézet + kalibráció
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
Future<void> _showDetail(BuildContext context) async {
|
|
final t = TiltService.to;
|
|
final poleCtrl =
|
|
TextEditingController(text: t.poleHeight.value.toStringAsFixed(2));
|
|
|
|
await Get.dialog(AlertDialog(
|
|
title: const Text('Rúd-libella'),
|
|
content: SingleChildScrollView(
|
|
child: Obx(() {
|
|
final errCm = t.horizontalErrorCm;
|
|
final inactive = !t.isCalibrated.value || t.isMoving.value;
|
|
final color = inactive
|
|
? Colors.grey
|
|
: errCm < 1.5
|
|
? Colors.green
|
|
: errCm < 3.5
|
|
? Colors.orange
|
|
: Colors.red;
|
|
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
SizedBox(
|
|
width: 140,
|
|
height: 140,
|
|
child: _BubbleFace(
|
|
size: 140,
|
|
color: color,
|
|
inactive: inactive,
|
|
uncalibrated: !t.isCalibrated.value,
|
|
dx: t.bubbleX.value,
|
|
dy: t.bubbleY.value,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
!t.isCalibrated.value
|
|
? 'Nincs kalibrálva'
|
|
: t.isMoving.value
|
|
? 'Mozgásban — állj meg a leolvasáshoz'
|
|
: '${t.tiltDeg.value.toStringAsFixed(2)}° ≈ '
|
|
'${errCm.toStringAsFixed(1)} cm vízszintes hiba',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
color: inactive ? Colors.grey : color),
|
|
),
|
|
Text(
|
|
'@ ${t.poleHeight.value.toStringAsFixed(2)} m rúdmagasság',
|
|
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: poleCtrl,
|
|
keyboardType:
|
|
const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: const InputDecoration(
|
|
labelText: 'Rúdmagasság (m)',
|
|
isDense: true,
|
|
),
|
|
onSubmitted: (v) {
|
|
final m = double.tryParse(v.replaceAll(',', '.'));
|
|
if (m != null) t.setPoleHeight(m);
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'A jelzés csak akkor érvényes, ha a telefon MEREVEN, '
|
|
'tartóban van a rúdon. Tartó-áthelyezés után kalibrálj újra.',
|
|
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
actions: [
|
|
if (TiltService.to.isCalibrated.value)
|
|
TextButton(
|
|
onPressed: () => TiltService.to.clearCalibration(),
|
|
child: const Text('Kalibráció törlése'),
|
|
),
|
|
TextButton(onPressed: Get.back, child: const Text('Bezár')),
|
|
FilledButton.icon(
|
|
icon: const Icon(Icons.architecture, size: 18),
|
|
label: const Text('Kalibrálás'),
|
|
onPressed: () async {
|
|
final m = double.tryParse(poleCtrl.text.replaceAll(',', '.'));
|
|
if (m != null) await TiltService.to.setPoleHeight(m);
|
|
Get.back();
|
|
await _calibrationFlow();
|
|
},
|
|
),
|
|
],
|
|
));
|
|
}
|
|
|
|
/// Vezetett kalibráció: 1. lépés kötelező, 2. (180°-os átforgatásos)
|
|
/// opcionális — utóbbi a tartó ferdeségét is kiejti.
|
|
Future<void> _calibrationFlow() async {
|
|
final t = TiltService.to;
|
|
|
|
final step1Ok = await _captureStep(
|
|
title: 'Kalibráció — 1/2',
|
|
text: 'Állítsd a rudat FÜGGŐLEGESRE a szelencés libella szerint, '
|
|
'és tartsd mozdulatlanul. A mérés 2 másodpercig tart.',
|
|
);
|
|
if (step1Ok != true) return;
|
|
final v1 = await t.captureVector();
|
|
HapticFeedback.mediumImpact();
|
|
|
|
final doStep2 = await Get.dialog<bool>(AlertDialog(
|
|
title: const Text('Kalibráció — pontosítás?'),
|
|
content: const Text(
|
|
'A pontosabb (tartó-ferdeséget is kiejtő) kalibrációhoz forgasd '
|
|
'át a telefont a RÚD TENGELYE körül 180°-kal (a rúd maradjon '
|
|
'függőleges), majd folytasd. Ki is hagyhatod — akkor az 1. '
|
|
'lépés lesz a referencia.'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(result: false),
|
|
child: const Text('Kihagyom')),
|
|
FilledButton(
|
|
onPressed: () => Get.back(result: true),
|
|
child: const Text('Átforgattam — folytatás')),
|
|
],
|
|
));
|
|
|
|
if (doStep2 == true) {
|
|
final v2 = await t.captureVector();
|
|
HapticFeedback.mediumImpact();
|
|
await t.saveReference(v1, v2);
|
|
} else {
|
|
await t.saveReference(v1);
|
|
}
|
|
|
|
Get.snackbar('Libella', 'Kalibráció kész — a buborék él.',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: const Color(0xFF2E7D32),
|
|
colorText: const Color(0xFFFFFFFF));
|
|
}
|
|
|
|
Future<bool?> _captureStep({required String title, required String text}) {
|
|
return Get.dialog<bool>(AlertDialog(
|
|
title: Text(title),
|
|
content: Text(text),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(result: false),
|
|
child: const Text('Mégse')),
|
|
FilledButton(
|
|
onPressed: () => Get.back(result: true),
|
|
child: const Text('Indítás')),
|
|
],
|
|
));
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
class _BubbleFace extends StatelessWidget {
|
|
final double size;
|
|
final Color color;
|
|
final bool inactive;
|
|
final bool uncalibrated;
|
|
final double dx;
|
|
final double dy;
|
|
|
|
const _BubbleFace({
|
|
required this.size,
|
|
required this.color,
|
|
required this.inactive,
|
|
required this.uncalibrated,
|
|
required this.dx,
|
|
required this.dy,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Skála: sin(4°) dőlésnél ér a széléhez.
|
|
final r = size / 2;
|
|
final scale = (r - size * 0.14) / 0.07;
|
|
var ox = dx * scale;
|
|
var oy = -dy * scale; // képernyő-y lefelé nő
|
|
final d = (ox * ox + oy * oy);
|
|
final maxR = r - size * 0.14;
|
|
if (d > maxR * maxR) {
|
|
final f = maxR / math.sqrt(d);
|
|
ox *= f;
|
|
oy *= f;
|
|
}
|
|
|
|
return CustomPaint(
|
|
painter: _BubblePainter(
|
|
color: color,
|
|
inactive: inactive,
|
|
ox: ox,
|
|
oy: oy,
|
|
dotRadius: size * 0.13,
|
|
),
|
|
child: uncalibrated
|
|
? Center(
|
|
child: Text('?',
|
|
style: TextStyle(
|
|
fontSize: size * 0.45,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.grey)))
|
|
: null,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BubblePainter extends CustomPainter {
|
|
final Color color;
|
|
final bool inactive;
|
|
final double ox;
|
|
final double oy;
|
|
final double dotRadius;
|
|
|
|
_BubblePainter({
|
|
required this.color,
|
|
required this.inactive,
|
|
required this.ox,
|
|
required this.oy,
|
|
required this.dotRadius,
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final c = Offset(size.width / 2, size.height / 2);
|
|
final r = size.width / 2;
|
|
|
|
canvas.drawCircle(
|
|
c,
|
|
r - 1,
|
|
Paint()
|
|
..color = color.withOpacity(inactive ? 0.06 : 0.12)
|
|
..style = PaintingStyle.fill);
|
|
canvas.drawCircle(
|
|
c,
|
|
r - 1,
|
|
Paint()
|
|
..color = color.withOpacity(inactive ? 0.5 : 0.9)
|
|
..strokeWidth = 1.6
|
|
..style = PaintingStyle.stroke);
|
|
// Belső célkör
|
|
canvas.drawCircle(
|
|
c,
|
|
r * 0.35,
|
|
Paint()
|
|
..color = color.withOpacity(0.5)
|
|
..strokeWidth = 1
|
|
..style = PaintingStyle.stroke);
|
|
|
|
if (!inactive) {
|
|
canvas.drawCircle(c + Offset(ox, oy), dotRadius, Paint()..color = color);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(_BubblePainter old) =>
|
|
old.ox != ox ||
|
|
old.oy != oy ||
|
|
old.color != color ||
|
|
old.inactive != inactive;
|
|
}
|