This commit is contained in:
2026-08-08 10:57:54 +08:00
parent d0d48c1ffc
commit 604af909b0
184 changed files with 12521 additions and 12722 deletions
@@ -0,0 +1,62 @@
import '../../../data/local/models/models.dart';
/// Computes archery score from a normalized hit coordinate.
///
/// Normalized coordinates use the range [-1, 1] where (0, 0) is the target
/// center and radius 1.0 is the outer scoring edge. Hits outside radius 1.0
/// count as a miss (M, score 0).
class ScoreMath {
const ScoreMath._();
/// Returns the score [0..10] for a hit at [normalizedX]/[normalizedY].
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;
}
/// Euclidean distance from center for normalized coords.
static double distance(double normalizedX, double normalizedY) {
final sq = normalizedX * normalizedX + normalizedY * normalizedY;
if (sq <= 0) return 0.0;
return _sqrt(sq);
}
/// Converts a pixel tap (relative to a square target of [size]) into a
/// normalized coordinate in [-1, 1] and returns the resulting [HitPoint].
static HitPoint hitPointFromPixels(
double localX,
double localY,
double size, {
required int arrowIndex,
required int endIndex,
}) {
final half = size / 2;
final nx = (localX - half) / half;
final ny = (localY - half) / half;
final score = scoreFor(nx, ny);
return HitPoint(
arrowIndex: arrowIndex,
endIndex: endIndex,
normalizedX: nx,
normalizedY: ny,
score: score,
isMiss: score == 0,
);
}
}
double _sqrt(double x) {
// Newton's method; precise enough for scoring distances.
if (x < 0) return double.nan;
if (x == 0) return 0.0;
var r = x;
for (var i = 0; i < 20; i++) {
r = (r + x / r) / 2;
}
return r;
}
@@ -0,0 +1,100 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import '../../../core/providers/app_providers.dart';
import '../../../data/local/models/models.dart';
/// All local [PointRecord]s (drafts + completed), newest first.
final pointRecordsProvider =
AsyncNotifierProvider<PointRecordsNotifier, List<PointRecord>>(
PointRecordsNotifier.new,
);
/// Loads a single record by id (used by the edit/view page).
final recordByIdProvider =
FutureProvider.family<PointRecord?, int>((ref, id) async {
final isar = await ref.watch(isarProvider.future);
return isar.pointRecords.get(id);
});
class PointRecordsNotifier extends AsyncNotifier<List<PointRecord>> {
@override
Future<List<PointRecord>> build() async {
final isar = await ref.watch(isarProvider.future);
return isar.pointRecords.where().sortByCreatedAtDesc().findAll();
}
Future<Isar> _isar() => ref.read(isarProvider.future);
/// Returns the current draft record, if any.
Future<PointRecord?> currentDraft() async {
final isar = await _isar();
return isar.pointRecords
.filter()
.statusEqualTo(PointRecordStatus.draft)
.sortByCreatedAtDesc()
.findFirst();
}
/// Creates a new draft from scoring parameters and returns it.
Future<PointRecord> createDraft({
required BowType bowType,
required TargetFaceType targetFaceType,
required int distanceMeters,
required int endCount,
required int arrowsPerEnd,
String? title,
}) async {
final isar = await _isar();
final draft = PointRecord.createDraft(
bowType: bowType,
targetFaceType: targetFaceType,
distanceMeters: distanceMeters,
endCount: endCount,
arrowsPerEnd: arrowsPerEnd,
title: title,
);
await isar.writeTxn(() => isar.pointRecords.put(draft));
state = AsyncData([draft, ...?state.value]);
return draft;
}
/// Replaces a record (used by the edit page to persist ongoing changes).
Future<void> upsert(PointRecord record) async {
final isar = await _isar();
final updated = record..updatedAt = DateTime.now();
await isar.writeTxn(() => isar.pointRecords.put(updated));
state = AsyncData(state.value?.map((e) => e.id == updated.id ? updated : e).toList() ?? [updated]);
}
/// Marks a draft as completed and persists it. Does NOT touch trial count.
Future<PointRecord> complete(PointRecord record) async {
final completed = record.markCompleted();
final isar = await _isar();
await isar.writeTxn(() => isar.pointRecords.put(completed));
state = AsyncData(
state.value?.map((e) => e.id == completed.id ? completed : e).toList() ?? [completed],
);
return completed;
}
/// Deletes a single record by id.
Future<void> delete(int id) async {
final isar = await _isar();
await isar.writeTxn(() => isar.pointRecords.delete(id));
state = AsyncData(state.value?.where((e) => e.id != id).toList() ?? const []);
}
/// Removes any existing draft records (used when the user picks
/// "重新计分" on the home draft-confirmation dialog).
Future<void> clearDrafts() async {
final isar = await _isar();
final drafts = await isar.pointRecords
.filter()
.statusEqualTo(PointRecordStatus.draft)
.findAll();
if (drafts.isEmpty) return;
await isar.writeTxn(() => isar.pointRecords.deleteAll(drafts.map((e) => e.id).toList()));
state = AsyncData(state.value?.where((e) => !drafts.any((d) => d.id == e.id)).toList() ?? const []);
}
}