Files
maps-saas/packages/tactical_app/lib/widgets/optical_rangefinder_hud.dart
T

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;
}
}