import 'package:flutter/material.dart'; import '../../../core/theme/app_theme.dart'; import '../../../data/models/shot.dart'; import '../../../data/models/target_type.dart'; class TargetOverlay extends StatelessWidget { final List shots; final double targetCenterX; final double targetCenterY; final double targetRadius; final TargetType targetType; final int ringCount; final List? ringRadii; // Individual ring radii multipliers final void Function(Shot shot)? onShotTapped; final void Function(double x, double y)? onAddShot; final double? groupingCenterX; final double? groupingCenterY; final double? groupingDiameter; final List? referenceImpacts; const TargetOverlay({ super.key, required this.shots, required this.targetCenterX, required this.targetCenterY, required this.targetRadius, required this.targetType, this.ringCount = 10, this.ringRadii, this.onShotTapped, this.onAddShot, this.groupingCenterX, this.groupingCenterY, this.groupingDiameter, this.referenceImpacts, }); @override Widget build(BuildContext context) { return GestureDetector( onTapUp: (details) { if (onAddShot != null) { final RenderBox box = context.findRenderObject() as RenderBox; final localPosition = details.localPosition; final relX = localPosition.dx / box.size.width; final relY = localPosition.dy / box.size.height; onAddShot!(relX, relY); } }, child: CustomPaint( painter: _TargetOverlayPainter( shots: shots, targetCenterX: targetCenterX, targetCenterY: targetCenterY, targetRadius: targetRadius, targetType: targetType, ringCount: ringCount, ringRadii: ringRadii, groupingCenterX: groupingCenterX, groupingCenterY: groupingCenterY, groupingDiameter: groupingDiameter, referenceImpacts: referenceImpacts, ), child: Stack( children: shots.map((shot) { return Positioned( left: 0, top: 0, right: 0, bottom: 0, child: LayoutBuilder( builder: (context, constraints) { final x = shot.x * constraints.maxWidth; final y = shot.y * constraints.maxHeight; return Stack( children: [ Positioned( left: x - 15, top: y - 15, child: GestureDetector( onTap: () => onShotTapped?.call(shot), child: Container( width: 30, height: 30, decoration: BoxDecoration( color: Colors.transparent, shape: BoxShape.circle, ), ), ), ), ], ); }, ), ); }).toList(), ), ), ); } } class _TargetOverlayPainter extends CustomPainter { final List shots; final double targetCenterX; final double targetCenterY; final double targetRadius; final TargetType targetType; final int ringCount; final List? ringRadii; final double? groupingCenterX; final double? groupingCenterY; final double? groupingDiameter; final List? referenceImpacts; _TargetOverlayPainter({ required this.shots, required this.targetCenterX, required this.targetCenterY, required this.targetRadius, required this.targetType, this.ringCount = 10, this.ringRadii, this.groupingCenterX, this.groupingCenterY, this.groupingDiameter, this.referenceImpacts, }); @override void paint(Canvas canvas, Size size) { // Draw target center indicator _drawTargetCenter(canvas, size); // Draw grouping circle if (groupingCenterX != null && groupingCenterY != null && groupingDiameter != null && shots.length > 1) { _drawGroupingCircle(canvas, size); } // Draw impacts for (final shot in shots) { _drawImpact(canvas, size, shot); } // Draw reference impacts (with different color) if (referenceImpacts != null) { for (final ref in referenceImpacts!) { _drawReferenceImpact(canvas, size, ref); } } } void _drawTargetCenter(Canvas canvas, Size size) { final centerX = targetCenterX * size.width; final centerY = targetCenterY * size.height; final minDim = size.width < size.height ? size.width : size.height; final maxRadius = targetRadius * minDim; final strokePaint = Paint() ..color = Colors.green.withValues(alpha: 0.5) ..style = PaintingStyle.stroke ..strokeWidth = 1; // Draw concentric rings based on ringCount (with individual radii if provided) for (int i = 0; i < ringCount; i++) { final ringMultiplier = (ringRadii != null && ringRadii!.length == ringCount) ? ringRadii![i] : (i + 1) / ringCount; final ringRadius = maxRadius * ringMultiplier; canvas.drawCircle(Offset(centerX, centerY), ringRadius, strokePaint); } // Draw score labels on rings (only if within visible area) final textPainter = TextPainter( textDirection: TextDirection.ltr, ); for (int i = 0; i < ringCount; i++) { // Calculate zone center (midpoint between this ring and previous) final currentMultiplier = (ringRadii != null && ringRadii!.length == ringCount) ? ringRadii![i] : (i + 1) / ringCount; final prevMultiplier = i == 0 ? 0.0 : (ringRadii != null && ringRadii!.length == ringCount) ? ringRadii![i - 1] : i / ringCount; final zoneRadius = maxRadius * (currentMultiplier + prevMultiplier) / 2; final score = 10 - i; // Only draw label if it's within the visible area final labelX = centerX + zoneRadius; if (labelX < 0 || labelX > size.width) continue; textPainter.text = TextSpan( text: '$score', style: TextStyle( color: Colors.green.withValues(alpha: 0.8), fontSize: 10, fontWeight: FontWeight.bold, shadows: const [ Shadow(color: Colors.black, blurRadius: 2), ], ), ); textPainter.layout(); // Draw label on the right side of each zone final labelY = centerY - textPainter.height / 2; if (labelY >= 0 && labelY <= size.height) { textPainter.paint(canvas, Offset(labelX - textPainter.width / 2, labelY)); } } // Draw crosshair at center final crosshairPaint = Paint() ..color = Colors.green.withValues(alpha: 0.7) ..strokeWidth = 1; canvas.drawLine( Offset(centerX - 10, centerY), Offset(centerX + 10, centerY), crosshairPaint, ); canvas.drawLine( Offset(centerX, centerY - 10), Offset(centerX, centerY + 10), crosshairPaint, ); } void _drawGroupingCircle(Canvas canvas, Size size) { final centerX = groupingCenterX! * size.width; final centerY = groupingCenterY! * size.height; final diameter = groupingDiameter! * size.width.clamp(0, size.height); // Draw filled circle final fillPaint = Paint() ..color = AppTheme.groupingCircleColor ..style = PaintingStyle.fill; canvas.drawCircle(Offset(centerX, centerY), diameter / 2, fillPaint); // Draw outline final strokePaint = Paint() ..color = AppTheme.groupingCenterColor ..style = PaintingStyle.stroke ..strokeWidth = 2; canvas.drawCircle(Offset(centerX, centerY), diameter / 2, strokePaint); // Draw center point final centerPaint = Paint() ..color = AppTheme.groupingCenterColor ..style = PaintingStyle.fill; canvas.drawCircle(Offset(centerX, centerY), 4, centerPaint); } void _drawImpact(Canvas canvas, Size size, Shot shot) { final x = shot.x * size.width; final y = shot.y * size.height; // Draw outer circle (white outline for visibility) final outlinePaint = Paint() ..color = AppTheme.impactOutlineColor ..style = PaintingStyle.stroke ..strokeWidth = 3; canvas.drawCircle(Offset(x, y), 10, outlinePaint); // Draw impact marker final impactPaint = Paint() ..color = AppTheme.impactColor ..style = PaintingStyle.fill; canvas.drawCircle(Offset(x, y), 8, impactPaint); // Draw score number final textPainter = TextPainter( text: TextSpan( text: '${shot.score}', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, ); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, y - textPainter.height / 2), ); } void _drawReferenceImpact(Canvas canvas, Size size, Shot ref) { final x = ref.x * size.width; final y = ref.y * size.height; // Draw outer circle (white outline for visibility) final outlinePaint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 3; canvas.drawCircle(Offset(x, y), 12, outlinePaint); // Draw reference marker (purple) final refPaint = Paint() ..color = Colors.deepPurple ..style = PaintingStyle.fill; canvas.drawCircle(Offset(x, y), 10, refPaint); // Draw "R" to indicate reference final textPainter = TextPainter( text: const TextSpan( text: 'R', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, ); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, y - textPainter.height / 2), ); } @override bool shouldRepaint(covariant _TargetOverlayPainter oldDelegate) { return shots != oldDelegate.shots || targetCenterX != oldDelegate.targetCenterX || targetCenterY != oldDelegate.targetCenterY || targetRadius != oldDelegate.targetRadius || ringCount != oldDelegate.ringCount || ringRadii != oldDelegate.ringRadii || groupingCenterX != oldDelegate.groupingCenterX || groupingCenterY != oldDelegate.groupingCenterY || groupingDiameter != oldDelegate.groupingDiameter || referenceImpacts != oldDelegate.referenceImpacts; } }