premier app version beta
This commit is contained in:
343
lib/features/analysis/widgets/target_overlay.dart
Normal file
343
lib/features/analysis/widgets/target_overlay.dart
Normal file
@@ -0,0 +1,343 @@
|
||||
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<Shot> shots;
|
||||
final double targetCenterX;
|
||||
final double targetCenterY;
|
||||
final double targetRadius;
|
||||
final TargetType targetType;
|
||||
final int ringCount;
|
||||
final List<double>? 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<Shot>? 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<Shot> shots;
|
||||
final double targetCenterX;
|
||||
final double targetCenterY;
|
||||
final double targetRadius;
|
||||
final TargetType targetType;
|
||||
final int ringCount;
|
||||
final List<double>? ringRadii;
|
||||
final double? groupingCenterX;
|
||||
final double? groupingCenterY;
|
||||
final double? groupingDiameter;
|
||||
final List<Shot>? 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user