Libella megjelnítése mérőrúdhoz, transzverzális rajzolása az aktuális kitűzendő ponthoz
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user