781 lines
32 KiB
Dart
781 lines
32 KiB
Dart
import 'package:camera/camera.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import '../controllers/optical_rangefinder_controller.dart';
|
|
import '../models/angle_unit.dart';
|
|
import '../services/camera_sensor_calibration_service.dart';
|
|
|
|
/// ============================================================================
|
|
/// [OpticalRangefinderHud] - واجهة قياس المسافة البصري وشبكة التصويب التكتيكية
|
|
/// ============================================================================
|
|
class OpticalRangefinderHud extends StatelessWidget {
|
|
final AngleUnit angleUnit;
|
|
final VoidCallback onClose;
|
|
final Function(double distanceMeters, double headingDeg, double pitchDeg)? onRangeLocked;
|
|
|
|
const OpticalRangefinderHud({
|
|
super.key,
|
|
required this.angleUnit,
|
|
required this.onClose,
|
|
this.onRangeLocked,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final OpticalRangefinderController ctrl = Get.put(OpticalRangefinderController());
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.black,
|
|
body: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
ctrl.updateScreenDimensions(Size(constraints.maxWidth, constraints.maxHeight));
|
|
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
// ── 1. Camera Live Stream with Pinch-to-Zoom ─────────────
|
|
Obx(() {
|
|
if (!ctrl.isCameraReady.value || ctrl.cameraController == null) {
|
|
return const Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CircularProgressIndicator(color: Color(0xFF00F0FF)),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'جاري فحص وتهيئة مستشعر الكاميرا البصري...',
|
|
style: TextStyle(color: Colors.white70, fontSize: 13),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
double baseScale = 1.0;
|
|
|
|
return GestureDetector(
|
|
onScaleStart: (_) {
|
|
baseScale = ctrl.currentZoom.value;
|
|
},
|
|
onScaleUpdate: (details) {
|
|
final newZoom = baseScale * details.scale;
|
|
ctrl.setZoom(newZoom);
|
|
},
|
|
child: Center(
|
|
child: CameraPreview(ctrl.cameraController!),
|
|
),
|
|
);
|
|
}),
|
|
|
|
// ── 2. Tactical Reticule Overlay (شبكة التصويب الميدانية) ─
|
|
Obx(() {
|
|
return _buildTacticalReticule(context, ctrl, constraints.maxWidth, constraints.maxHeight);
|
|
}),
|
|
|
|
// ── 3. Top Operations Bar ────────────────────────────────
|
|
Positioned(
|
|
top: 44,
|
|
left: 16,
|
|
right: 16,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
// Close Button
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF090E17).withAlpha(220),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.white24),
|
|
),
|
|
child: IconButton(
|
|
icon: const Icon(Icons.close, color: Colors.white),
|
|
onPressed: onClose,
|
|
tooltip: 'إغلاق والعودة للخريطة',
|
|
),
|
|
),
|
|
|
|
// Title Badge
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF090E17).withAlpha(220),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFF00F0FF).withAlpha(120)),
|
|
),
|
|
child: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.center_focus_strong, color: Color(0xFF00F0FF), size: 16),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'قياس المسافة البصري (بدون GPS)',
|
|
style: TextStyle(color: Colors.white, fontSize: 12.5, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Sensor Inspector Button
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF090E17).withAlpha(220),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.white24),
|
|
),
|
|
child: IconButton(
|
|
icon: const Icon(Icons.sensors, color: Color(0xFF38BDF8)),
|
|
onPressed: () => _openSensorInspectorDialog(context, ctrl),
|
|
tooltip: 'فحص ومعايرة مستشعر الكاميرا',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// ── 4. Main Telemetry HUD Card ───────────────────────────
|
|
Positioned(
|
|
top: 104,
|
|
left: 20,
|
|
right: 20,
|
|
child: Obx(() {
|
|
return _buildMainTelemetryCard(ctrl);
|
|
}),
|
|
),
|
|
|
|
// ── 5. Right Zoom Slider Widget ──────────────────────────
|
|
Positioned(
|
|
right: 14,
|
|
top: 230,
|
|
bottom: 210,
|
|
child: Obx(() {
|
|
return _buildVerticalZoomBar(ctrl);
|
|
}),
|
|
),
|
|
|
|
// ── 6. Bottom Target Selector & Actions ──────────────────
|
|
Positioned(
|
|
bottom: 20,
|
|
left: 14,
|
|
right: 14,
|
|
child: Obx(() {
|
|
return _buildBottomControls(context, ctrl);
|
|
}),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Reticule Drawing ───────────────────────────────────────────────────────
|
|
|
|
Widget _buildTacticalReticule(
|
|
BuildContext context,
|
|
OpticalRangefinderController ctrl,
|
|
double width,
|
|
double height,
|
|
) {
|
|
final centerY = height / 2.0;
|
|
final centerX = width / 2.0;
|
|
final bracketH = (ctrl.reticuleHeightRatio.value * height).clamp(20.0, height * 0.75);
|
|
final halfH = bracketH / 2.0;
|
|
|
|
return Stack(
|
|
children: [
|
|
// Center crosshair with mil marks
|
|
CustomPaint(
|
|
size: Size(width, height),
|
|
painter: _MilReticulePainter(
|
|
centerX: centerX,
|
|
centerY: centerY,
|
|
bracketHalfHeight: halfH,
|
|
bracketWidth: 120.0,
|
|
rollDeg: ctrl.rollDeg.value,
|
|
),
|
|
),
|
|
|
|
// Interactive Drag Handles for Brackets
|
|
Positioned(
|
|
top: centerY - halfH - 24,
|
|
left: centerX - 60,
|
|
width: 120,
|
|
height: 48,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.translucent,
|
|
onVerticalDragUpdate: (details) {
|
|
final newHalf = halfH - details.delta.dy;
|
|
ctrl.setReticuleHeightRatio((newHalf * 2.0) / height);
|
|
},
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF00F0FF).withAlpha(200),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: const Text('قمة الهدف ▲', style: TextStyle(color: Colors.black, fontSize: 10, fontWeight: FontWeight.bold)),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
Positioned(
|
|
top: centerY + halfH - 24,
|
|
left: centerX - 60,
|
|
width: 120,
|
|
height: 48,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.translucent,
|
|
onVerticalDragUpdate: (details) {
|
|
final newHalf = halfH + details.delta.dy;
|
|
ctrl.setReticuleHeightRatio((newHalf * 2.0) / height);
|
|
},
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF00F0FF).withAlpha(200),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: const Text('قاعدة الهدف ▼', style: TextStyle(color: Colors.black, fontSize: 10, fontWeight: FontWeight.bold)),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ── Telemetry Card ─────────────────────────────────────────────────────────
|
|
|
|
Widget _buildMainTelemetryCard(OpticalRangefinderController ctrl) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF090E17).withAlpha(235),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF00F0FF).withAlpha(150), width: 1.2),
|
|
boxShadow: const [
|
|
BoxShadow(color: Color(0x66000000), blurRadius: 16),
|
|
BoxShadow(color: Color(0x3300F0FF), blurRadius: 8),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Distance Display
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
|
textBaseline: TextBaseline.alphabetic,
|
|
children: [
|
|
const Text(
|
|
'المسافة البصرية:',
|
|
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12, fontWeight: FontWeight.bold),
|
|
),
|
|
Text(
|
|
ctrl.formatDistance(),
|
|
style: const TextStyle(
|
|
color: Color(0xFF00F0FF),
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
const Divider(color: Colors.white12, height: 1),
|
|
const SizedBox(height: 8),
|
|
|
|
// Heading & Pitch Row
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
_buildMetricChip(
|
|
icon: Icons.navigation,
|
|
label: 'الاتجاه:',
|
|
value: ctrl.formatHeading(angleUnit),
|
|
color: const Color(0xFF38BDF8),
|
|
),
|
|
_buildMetricChip(
|
|
icon: Icons.height,
|
|
label: 'الميل:',
|
|
value: '${ctrl.pitchDeg.value.toStringAsFixed(1)}°',
|
|
color: const Color(0xFFFBBF24),
|
|
),
|
|
_buildMetricChip(
|
|
icon: Icons.zoom_in,
|
|
label: 'التقريب:',
|
|
value: '${ctrl.currentZoom.value.toStringAsFixed(1)}x',
|
|
color: const Color(0xFF4ADE80),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMetricChip({
|
|
required IconData icon,
|
|
required String label,
|
|
required String value,
|
|
required Color color,
|
|
}) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 13, color: color),
|
|
const SizedBox(width: 4),
|
|
Text(label, style: const TextStyle(color: Color(0xFF64748B), fontSize: 10.5)),
|
|
const SizedBox(width: 4),
|
|
Text(value, style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold)),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ── Vertical Zoom Bar ──────────────────────────────────────────────────────
|
|
|
|
Widget _buildVerticalZoomBar(OpticalRangefinderController ctrl) {
|
|
return Container(
|
|
width: 42,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF090E17).withAlpha(220),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: Colors.white24),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
const Text('10x', style: TextStyle(color: Colors.white54, fontSize: 9)),
|
|
Expanded(
|
|
child: RotatedBox(
|
|
quarterTurns: 3,
|
|
child: SliderTheme(
|
|
data: SliderThemeData(
|
|
trackHeight: 3,
|
|
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
|
|
activeTrackColor: const Color(0xFF00F0FF),
|
|
inactiveTrackColor: Colors.white24,
|
|
thumbColor: Colors.white,
|
|
),
|
|
child: Slider(
|
|
value: ctrl.currentZoom.value,
|
|
min: ctrl.minZoom.value,
|
|
max: ctrl.maxZoom.value,
|
|
onChanged: (val) => ctrl.setZoom(val),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const Text('1x', style: TextStyle(color: Colors.white54, fontSize: 9)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Bottom Controls & Target Selector ──────────────────────────────────────
|
|
|
|
Widget _buildBottomControls(BuildContext context, OpticalRangefinderController ctrl) {
|
|
final currentTarget = ctrl.selectedTarget.value;
|
|
final isCustom = ctrl.isCustomTarget.value;
|
|
final heightText = isCustom
|
|
? '${ctrl.customHeight.value.toStringAsFixed(1)} م'
|
|
: '${currentTarget.heightMeters.toStringAsFixed(1)} م';
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF090E17).withAlpha(240),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: Colors.white12),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Target Selector Header Row
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () => _openTargetPickerSheet(context, ctrl),
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF020617),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: const Color(0xFF0071E3).withAlpha(150)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.tune, color: Color(0xFF38BDF8), size: 16),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('الهدف المرجعي المعتمد:', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5)),
|
|
Text(
|
|
isCustom ? 'ارتفاع مخصص ($heightText)' : '${currentTarget.titleAr} ($heightText)',
|
|
style: const TextStyle(color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.bold),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Icon(Icons.arrow_drop_down, color: Colors.white70),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
// Quick Lock Button
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF0071E3),
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
onPressed: () {
|
|
if (onRangeLocked != null) {
|
|
onRangeLocked!(
|
|
ctrl.calculatedDistanceMeters.value,
|
|
ctrl.headingDeg.value,
|
|
ctrl.pitchDeg.value,
|
|
);
|
|
}
|
|
Get.snackbar(
|
|
'🎯 تم تثبيت المدى والاتجاه',
|
|
'المسافة: ${ctrl.formatDistance()} • ${ctrl.formatHeading(angleUnit)}',
|
|
backgroundColor: const Color(0xFF0F172A),
|
|
colorText: Colors.white,
|
|
snackPosition: SnackPosition.TOP,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.lock, size: 14),
|
|
label: const Text('تثبيت المدى', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold)),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
// Quick Presets Row
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[0]), // Infantry
|
|
const SizedBox(width: 6),
|
|
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[1]), // Light Vehicle
|
|
const SizedBox(width: 6),
|
|
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[3]), // Tank
|
|
const SizedBox(width: 6),
|
|
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[7]), // Minaret
|
|
const SizedBox(width: 6),
|
|
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[8]), // Grand Minaret
|
|
const SizedBox(width: 6),
|
|
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[4]), // Flagpole
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickPresetChip(OpticalRangefinderController ctrl, TargetPreset preset) {
|
|
final isSelected = !ctrl.isCustomTarget.value && ctrl.selectedTarget.value.id == preset.id;
|
|
return InkWell(
|
|
onTap: () => ctrl.selectTarget(preset),
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? const Color(0xFF0071E3) : const Color(0xFF020617),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: isSelected ? const Color(0xFF00F0FF) : Colors.white12),
|
|
),
|
|
child: Text(
|
|
'${preset.titleAr.split(' ').first} (${preset.heightMeters.toStringAsFixed(1)}م)',
|
|
style: TextStyle(
|
|
color: isSelected ? Colors.white : const Color(0xFF94A3B8),
|
|
fontSize: 10.5,
|
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Modals & Sheets ────────────────────────────────────────────────────────
|
|
|
|
void _openTargetPickerSheet(BuildContext context, OpticalRangefinderController ctrl) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: const Color(0xFF090E17),
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
side: BorderSide(color: Color(0xFF0071E3), width: 1.5),
|
|
),
|
|
builder: (ctx) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(18),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'مكتبة الأهداف التكتيكية الميدانية',
|
|
style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close, color: Colors.white54, size: 18),
|
|
onPressed: () => Navigator.pop(ctx),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Flexible(
|
|
child: ListView(
|
|
shrinkWrap: true,
|
|
children: [
|
|
...CameraSensorCalibrationService.standardTargets.map((preset) {
|
|
return ListTile(
|
|
leading: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(Icons.adjust, color: Color(0xFF00F0FF), size: 18),
|
|
),
|
|
title: Text(preset.titleAr, style: const TextStyle(color: Colors.white, fontSize: 12.5)),
|
|
subtitle: Text(
|
|
'التصنيف: ${preset.categoryAr} • الارتفاع: ${preset.heightMeters} متر',
|
|
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
|
|
),
|
|
trailing: const Icon(Icons.chevron_left, color: Colors.white24),
|
|
onTap: () {
|
|
ctrl.selectTarget(preset);
|
|
Navigator.pop(ctx);
|
|
},
|
|
);
|
|
}),
|
|
const Divider(color: Colors.white12),
|
|
ListTile(
|
|
leading: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(Icons.edit, color: Color(0xFFFBBF24), size: 18),
|
|
),
|
|
title: const Text('إدخال ارتفاع يدوي مخصص...', style: TextStyle(color: Color(0xFFFBBF24), fontSize: 12.5, fontWeight: FontWeight.bold)),
|
|
subtitle: const Text('لتحديد ارتفاع أي معلم أو جبل أو سارية بالأمتار بدقة', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10)),
|
|
onTap: () {
|
|
Navigator.pop(ctx);
|
|
_showCustomHeightDialog(context, ctrl);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showCustomHeightDialog(BuildContext context, OpticalRangefinderController ctrl) {
|
|
final textCtrl = TextEditingController(text: ctrl.customHeight.value.toStringAsFixed(1));
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
backgroundColor: const Color(0xFF090E17),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
side: const BorderSide(color: Color(0xFF0071E3)),
|
|
),
|
|
title: const Text('تعيين ارتفاع الهدف التكتيكي', style: TextStyle(color: Colors.white, fontSize: 14)),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text(
|
|
'أدخل الارتفاع التقديري الحقيقي للمعلم بالأمتار لحساب المدى البصري بدقة:',
|
|
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 11),
|
|
),
|
|
const SizedBox(height: 14),
|
|
TextField(
|
|
controller: textCtrl,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
|
decoration: InputDecoration(
|
|
suffixText: 'متر',
|
|
suffixStyle: const TextStyle(color: Color(0xFF00F0FF)),
|
|
filled: true,
|
|
fillColor: const Color(0xFF020617),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('إلغاء', style: TextStyle(color: Colors.white54)),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0071E3)),
|
|
onPressed: () {
|
|
final val = double.tryParse(textCtrl.text);
|
|
if (val != null && val > 0) {
|
|
ctrl.setCustomTargetHeight(val);
|
|
}
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: const Text('حفظ واحتساب', style: TextStyle(color: Colors.white)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _openSensorInspectorDialog(BuildContext context, OpticalRangefinderController ctrl) {
|
|
final profile = ctrl.sensorProfile.value;
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
backgroundColor: const Color(0xFF090E17),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(18),
|
|
side: const BorderSide(color: Color(0xFF38BDF8), width: 1.5),
|
|
),
|
|
title: const Row(
|
|
children: [
|
|
Icon(Icons.memory, color: Color(0xFF38BDF8), size: 20),
|
|
SizedBox(width: 8),
|
|
Text('فحص ومعايرة مستشعر الكاميرا', style: TextStyle(color: Colors.white, fontSize: 13.5, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildInspectorRow('اسم المستشعر:', profile.deviceName),
|
|
const Divider(color: Colors.white12, height: 12),
|
|
_buildInspectorRow('البعد البؤري الفيزيائي:', '${profile.focalLengthMm.toStringAsFixed(2)} mm'),
|
|
const Divider(color: Colors.white12, height: 12),
|
|
_buildInspectorRow('المكافئ البؤري (35mm Equiv):', '${profile.focalLength35mmEquiv.toStringAsFixed(1)} mm'),
|
|
const Divider(color: Colors.white12, height: 12),
|
|
_buildInspectorRow('زاوية الرؤية الأفقية (HFOV):', '${profile.horizontalFovDegrees.toStringAsFixed(1)}°'),
|
|
const Divider(color: Colors.white12, height: 12),
|
|
_buildInspectorRow('أبعاد المستشعر (W x H):', '${profile.sensorWidthMm.toStringAsFixed(2)} x ${profile.sensorHeightMm.toStringAsFixed(2)} mm'),
|
|
const Divider(color: Colors.white12, height: 12),
|
|
_buildInspectorRow('حالة المعايرة البصرية:', 'معاير تلقائياً 100% (Calibrated)'),
|
|
],
|
|
),
|
|
actions: [
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0071E3)),
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('تم', style: TextStyle(color: Colors.white)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInspectorRow(String label, String value) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(label, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 11)),
|
|
Text(value, style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Painter ──────────────────────────────────────────────────────────────────
|
|
|
|
class _MilReticulePainter extends CustomPainter {
|
|
final double centerX;
|
|
final double centerY;
|
|
final double bracketHalfHeight;
|
|
final double bracketWidth;
|
|
final double rollDeg;
|
|
|
|
_MilReticulePainter({
|
|
required this.centerX,
|
|
required this.centerY,
|
|
required this.bracketHalfHeight,
|
|
required this.bracketWidth,
|
|
required this.rollDeg,
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final reticulePaint = Paint()
|
|
..color = const Color(0xFF00F0FF).withAlpha(200)
|
|
..strokeWidth = 1.5
|
|
..style = PaintingStyle.stroke;
|
|
|
|
final bracketPaint = Paint()
|
|
..color = const Color(0xFFFBBF24)
|
|
..strokeWidth = 2.5
|
|
..style = PaintingStyle.stroke;
|
|
|
|
final centerPaint = Paint()
|
|
..color = const Color(0xFF00F0FF)
|
|
..style = PaintingStyle.fill;
|
|
|
|
// Center dot
|
|
canvas.drawCircle(Offset(centerX, centerY), 3.0, centerPaint);
|
|
|
|
// Crosshair horizontal and vertical lines with mil marks
|
|
const tickLen = 6.0;
|
|
for (int i = -4; i <= 4; i++) {
|
|
if (i == 0) continue;
|
|
final x = centerX + i * 20.0;
|
|
canvas.drawLine(Offset(x, centerY - tickLen), Offset(x, centerY + tickLen), reticulePaint);
|
|
final y = centerY + i * 20.0;
|
|
canvas.drawLine(Offset(centerX - tickLen, y), Offset(centerX + tickLen, y), reticulePaint);
|
|
}
|
|
|
|
// Top Bracket
|
|
final topY = centerY - bracketHalfHeight;
|
|
canvas.drawLine(Offset(centerX - bracketWidth / 2, topY), Offset(centerX + bracketWidth / 2, topY), bracketPaint);
|
|
canvas.drawLine(Offset(centerX - bracketWidth / 2, topY), Offset(centerX - bracketWidth / 2, topY + 12), bracketPaint);
|
|
canvas.drawLine(Offset(centerX + bracketWidth / 2, topY), Offset(centerX + bracketWidth / 2, topY + 12), bracketPaint);
|
|
|
|
// Bottom Bracket
|
|
final botY = centerY + bracketHalfHeight;
|
|
canvas.drawLine(Offset(centerX - bracketWidth / 2, botY), Offset(centerX + bracketWidth / 2, botY), bracketPaint);
|
|
canvas.drawLine(Offset(centerX - bracketWidth / 2, botY), Offset(centerX - bracketWidth / 2, botY - 12), bracketPaint);
|
|
canvas.drawLine(Offset(centerX + bracketWidth / 2, botY), Offset(centerX + bracketWidth / 2, botY - 12), bracketPaint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _MilReticulePainter oldDelegate) {
|
|
return oldDelegate.bracketHalfHeight != bracketHalfHeight ||
|
|
oldDelegate.bracketWidth != bracketWidth ||
|
|
oldDelegate.rollDeg != rollDeg;
|
|
}
|
|
}
|