修复白屏BUG、UI 边界溢出 BUG
This commit is contained in:
@@ -9,14 +9,26 @@ class ScoreMath {
|
||||
const ScoreMath._();
|
||||
|
||||
/// Returns the score [0..10] for a hit at [normalizedX]/[normalizedY].
|
||||
///
|
||||
/// Uses standard FITA ring proportions (as fraction of target radius):
|
||||
/// Ring 10: 0 - 5%
|
||||
/// Ring 9: 5% - 15%
|
||||
/// Ring 8: 15% - 25%
|
||||
/// ...
|
||||
/// Ring 1: 85% - 100%
|
||||
/// Miss: > 100%
|
||||
static int scoreFor(double normalizedX, double normalizedY) {
|
||||
final d = distance(normalizedX, normalizedY);
|
||||
if (d > 1.0) return 0; // miss
|
||||
final ring = (d * 10).floor();
|
||||
var score = 10 - ring;
|
||||
if (score < 1) score = 1;
|
||||
if (score > 10) score = 10;
|
||||
return score;
|
||||
if (d > 1.0) return 0;
|
||||
|
||||
if (d <= 0.05) return 10;
|
||||
|
||||
final k = ((d - 0.05) / 0.1).floor();
|
||||
final ring = 9 - k;
|
||||
|
||||
if (ring < 1) return 1;
|
||||
if (ring > 9) return 9;
|
||||
return ring;
|
||||
}
|
||||
|
||||
/// Euclidean distance from center for normalized coords.
|
||||
|
||||
@@ -98,7 +98,9 @@ class _PointBookCreatePageState extends ConsumerState<PointBookCreatePage> {
|
||||
|
||||
Future<void> _start() async {
|
||||
setState(() => _creating = true);
|
||||
final draft = await ref.read(pointRecordsProvider.notifier).createDraft(
|
||||
final draft = await ref
|
||||
.read(pointRecordsProvider.notifier)
|
||||
.createDraft(
|
||||
bowType: _bowType,
|
||||
targetFaceType: _targetFace,
|
||||
distanceMeters: _distance,
|
||||
@@ -187,11 +189,16 @@ class _NumberPicker extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 13)),
|
||||
Flexible(child: Text(label, style: const TextStyle(fontSize: 13))),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
iconSize: 20,
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
final i = options.indexOf(value);
|
||||
if (i > 0) onChanged(options[i - 1]);
|
||||
@@ -206,6 +213,10 @@ class _NumberPicker extends StatelessWidget {
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
iconSize: 20,
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
final i = options.indexOf(value);
|
||||
if (i < options.length - 1) onChanged(options[i + 1]);
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:arcx/features/config/application/app_config_controller.dart';
|
||||
import 'package:arcx/data/local/models/models.dart';
|
||||
import 'package:arcx/features/scoring/application/score_math.dart';
|
||||
import 'package:arcx/features/scoring/application/scoring_service.dart';
|
||||
import 'package:arcx/features/scoring/presentation/widgets/target_face_painter.dart';
|
||||
import 'package:arcx/features/scoring/presentation/widgets/hit_point_painter.dart';
|
||||
|
||||
class PointBookEditPage extends ConsumerStatefulWidget {
|
||||
const PointBookEditPage({
|
||||
@@ -70,6 +70,7 @@ class _PointBookEditPageState extends ConsumerState<PointBookEditPage> {
|
||||
final isComplete = placed >= totalArrows;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_ProgressHeader(
|
||||
record: record,
|
||||
@@ -82,52 +83,61 @@ class _PointBookEditPageState extends ConsumerState<PointBookEditPage> {
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final size = constraints.maxWidth;
|
||||
return GestureDetector(
|
||||
onTapUp: widget.readOnly || isComplete || _saving
|
||||
? null
|
||||
: (details) => _onTap(details, size, record),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CustomPaint(
|
||||
painter: TargetFacePainter(
|
||||
hitPoints: _hitPoints,
|
||||
highlightIndex: _hitPoints.isEmpty
|
||||
? null
|
||||
: _hitPoints.length - 1,
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final size = constraints.maxWidth;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapUp: widget.readOnly || isComplete || _saving
|
||||
? null
|
||||
: (details) => _onTap(details, size, record),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/target_face.png',
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
CustomPaint(
|
||||
size: Size(size, size),
|
||||
painter: HitPointPainter(
|
||||
hitPoints: _hitPoints,
|
||||
highlightIndex: _hitPoints.isEmpty
|
||||
? null
|
||||
: _hitPoints.length - 1,
|
||||
),
|
||||
),
|
||||
if (widget.readOnly && _hitPoints.isEmpty)
|
||||
Text(
|
||||
'无落点数据',
|
||||
style: TextStyle(color: AppTheme.textSecondary),
|
||||
),
|
||||
if (!widget.readOnly && isComplete)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'已射完,点击右上角保存',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.readOnly && _hitPoints.isEmpty)
|
||||
Text(
|
||||
'无落点数据',
|
||||
style: TextStyle(color: AppTheme.textSecondary),
|
||||
),
|
||||
if (!widget.readOnly && isComplete)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'已射完,点击右上角保存',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../data/local/models/models.dart';
|
||||
|
||||
/// Paints hit points on the target face image.
|
||||
/// Assumes the underlying image is a square with normalized coordinates
|
||||
/// where (0, 0) is the center and radius 1.0 is the outer edge.
|
||||
class HitPointPainter extends CustomPainter {
|
||||
HitPointPainter({
|
||||
this.hitPoints = const <HitPoint>[],
|
||||
this.highlightIndex,
|
||||
this.hitColor = const Color(0xFF1A1A1A),
|
||||
});
|
||||
|
||||
final List<HitPoint> hitPoints;
|
||||
final int? highlightIndex;
|
||||
final Color hitColor;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (hitPoints.isEmpty) return;
|
||||
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final radius = size.shortestSide / 2;
|
||||
|
||||
for (var i = 0; i < hitPoints.length; i++) {
|
||||
final hp = hitPoints[i];
|
||||
final point = Offset(
|
||||
center.dx + hp.normalizedX * radius,
|
||||
center.dy + hp.normalizedY * radius,
|
||||
);
|
||||
final isHighlight = i == highlightIndex;
|
||||
final dotRadius = isHighlight ? radius * 0.045 : radius * 0.03;
|
||||
|
||||
// White outer ring for visibility
|
||||
canvas.drawCircle(
|
||||
point,
|
||||
dotRadius + 2,
|
||||
Paint()..color = Colors.white,
|
||||
);
|
||||
// Inner colored dot
|
||||
canvas.drawCircle(
|
||||
point,
|
||||
dotRadius,
|
||||
Paint()..color = isHighlight ? const Color(0xFF16A34A) : hitColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant HitPointPainter oldDelegate) {
|
||||
return oldDelegate.hitPoints.length != hitPoints.length ||
|
||||
oldDelegate.highlightIndex != highlightIndex ||
|
||||
oldDelegate.hitColor != hitColor;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../data/local/models/models.dart';
|
||||
|
||||
/// Paints a standard 10-ring archery target face and optionally overlays
|
||||
/// recorded [HitPoint]s. Coordinates use the normalized convention where
|
||||
/// (0, 0) is the center and radius 1.0 is the outer scoring edge.
|
||||
class TargetFacePainter extends CustomPainter {
|
||||
TargetFacePainter({
|
||||
this.hitPoints = const <HitPoint>[],
|
||||
this.highlightIndex,
|
||||
this.showAllHits = true,
|
||||
this.hitColor = const Color(0xFF1A1A1A),
|
||||
this.ringStrokeWidth = 1.0,
|
||||
});
|
||||
|
||||
final List<HitPoint> hitPoints;
|
||||
final int? highlightIndex;
|
||||
final bool showAllHits;
|
||||
final Color hitColor;
|
||||
final double ringStrokeWidth;
|
||||
|
||||
// Ring colors from center outward: 10,9 gold | 8,7 red | 6,5 blue | 4,3 black | 2,1 white
|
||||
static const _ringColors = <Color>[
|
||||
Color(0xFFFFD23F), // 10
|
||||
Color(0xFFFFD23F), // 9
|
||||
Color(0xFFE5484D), // 8
|
||||
Color(0xFFE5484D), // 7
|
||||
Color(0xFF3B82C4), // 6
|
||||
Color(0xFF3B82C4), // 5
|
||||
Color(0xFF2B2B2B), // 4
|
||||
Color(0xFF2B2B2B), // 3
|
||||
Color(0xFFF5F5F5), // 2
|
||||
Color(0xFFF5F5F5), // 1
|
||||
];
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final radius = size.shortestSide / 2;
|
||||
|
||||
// Draw rings from outermost (1) inward to (10).
|
||||
for (var i = _ringColors.length - 1; i >= 0; i--) {
|
||||
final ringRadius = radius * (i + 1) / _ringColors.length;
|
||||
final paint = Paint()..color = _ringColors[i];
|
||||
canvas.drawCircle(center, ringRadius, paint);
|
||||
}
|
||||
|
||||
// X ring (center dot).
|
||||
canvas.drawCircle(
|
||||
center,
|
||||
radius * 0.05,
|
||||
Paint()..color = const Color(0xFF8A6D00),
|
||||
);
|
||||
|
||||
// Ring separator lines.
|
||||
final stroke = Paint()
|
||||
..color = const Color(0x55000000)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = ringStrokeWidth;
|
||||
for (var i = 1; i <= _ringColors.length; i++) {
|
||||
canvas.drawCircle(center, radius * i / _ringColors.length, stroke);
|
||||
}
|
||||
|
||||
if (!showAllHits) return;
|
||||
|
||||
for (var i = 0; i < hitPoints.length; i++) {
|
||||
final hp = hitPoints[i];
|
||||
final point = Offset(
|
||||
center.dx + hp.normalizedX * radius,
|
||||
center.dy + hp.normalizedY * radius,
|
||||
);
|
||||
final isHighlight = i == highlightIndex;
|
||||
final dotRadius = isHighlight ? radius * 0.045 : radius * 0.03;
|
||||
|
||||
canvas.drawCircle(
|
||||
point,
|
||||
dotRadius + 2,
|
||||
Paint()..color = Colors.white,
|
||||
);
|
||||
canvas.drawCircle(
|
||||
point,
|
||||
dotRadius,
|
||||
Paint()..color = isHighlight ? const Color(0xFF16A34A) : hitColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant TargetFacePainter oldDelegate) {
|
||||
return oldDelegate.hitPoints.length != hitPoints.length ||
|
||||
oldDelegate.highlightIndex != highlightIndex ||
|
||||
oldDelegate.hitColor != hitColor;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user